colibri/crates/colibri-daemon/tests/glasspane_integration.rs
Sam & Claude 06601a09c4
Some checks failed
CI / rust (pull_request) Has been cancelled
CI / markdown (pull_request) Has been cancelled
CI / port (pull_request) Has been cancelled
CI / agent-jail-pkgs (pull_request) Has been cancelled
refactor: clear pi-era residue from the harness-neutral agent path
Sweep for stale naming/defaults left over from the pi-only era (the same
class of bug as the pi_binary compile break):

- socket.rs: non-local spawn defaulted the binary to `hermes-agent` (a binary
  that does not exist) and hardcoded `--mode json` (invalid for zot) — a
  reachable latent bug via `colibri spawn-agent <provider>`. Default to zot and
  derive argv from a single default_agent_args() helper, shared with autospawn
  (removes the duplicated zot-vs-pi arg logic).
- glasspane/tui/client/daemon: rename the stale wire field `pi_session_id` →
  `session_id` (zot agents have session ids too). `#[serde(alias =
  "pi_session_id")]` keeps deserializing legacy/persisted snapshots.
- contracts + runtime_inventory: record `zot` version alongside `pi` for
  complete agent provenance (detect_zot_version()).
- Harness-neutral stale comments ("Pi agent", "hermes-agent" example,
  COLIBRI_PI_BINARY in a test doc).
- cmd_spawn_agent: #[allow(clippy::too_many_arguments)] — this lint was already
  failing `clippy -D warnings`, i.e. the CI gate was red on main and thus
  unenforceable. The gate (scripts/ci-checks.sh) now passes green.
- AGENTS.md: document that ci-checks.sh passing is mandatory before merge while
  no Actions runner enforces .forgejo/workflows/ci.yml.

Validated: ./scripts/ci-checks.sh green (fmt, clippy -D warnings, tests, md).

Stacks on #157 (zot-rpc driver) and #156 (0.12 build fix).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 18:04:45 +02:00

162 lines
5.9 KiB
Rust

// Integration test: colibri-daemon ↔ colibri-glasspane ↔ colibri-client
//
// Proves the 5-state transition pipeline end-to-end:
// Pi JSONL → PiJsonlIngestor → fold_pi_events → GlasspaneSnapshot
//
// Attribution: Sam & Hermes (2026-05-27)
#[cfg(test)]
mod integration {
use colibri_glasspane::{AgentState, GlasspaneSnapshot, Pane, PiJsonlIngestor};
use std::time::{Duration, SystemTime};
fn now() -> SystemTime {
SystemTime::UNIX_EPOCH + Duration::from_secs(1716800000)
}
/// Full lifecycle: idle → working → done
#[test]
fn lifecycle_idle_working_done_via_ingestor() {
let mut ingestor = PiJsonlIngestor::default();
let t = now();
let lines = [
r#"{"type":"session","id":"s1","timestamp":"2026-05-27T10:00:00Z"}"#,
r#"{"type":"turn_start","timestamp":"2026-05-27T10:00:01Z"}"#,
r#"{"type":"message_update","timestamp":"2026-05-27T10:00:02Z"}"#,
r#"{"type":"turn_end","timestamp":"2026-05-27T10:00:03Z"}"#,
];
for (i, line) in lines.iter().enumerate() {
ingestor.ingest_line_at(line, t + Duration::from_secs(i as u64));
}
assert_eq!(ingestor.state(), AgentState::Done);
assert_eq!(ingestor.session_id(), Some("s1"));
}
/// Agent stays idle through unknown events (no crash, forward-compatible)
#[test]
fn agent_idle_through_unrecognized_event() {
let mut ingestor = PiJsonlIngestor::default();
let t = now();
ingestor.ingest_line_at(
r#"{"type":"session","id":"s2","timestamp":"2026-05-27T10:01:00Z"}"#,
t,
);
ingestor.ingest_line_at(
r#"{"type":"tool_error","timestamp":"2026-05-27T10:01:01Z","error":"API key invalid"}"#,
t + Duration::from_secs(1),
);
// tool_error is not a recognized state-changing event — ingestor stays at Idle
// but should NOT crash
assert!(ingestor.state() == AgentState::Idle || ingestor.state() == AgentState::Error);
}
/// Unknown event preserves current state (forward-compatible)
#[test]
fn unknown_event_preserves_state() {
let mut ingestor = PiJsonlIngestor::default();
let t = now();
ingestor.ingest_line_at(
r#"{"type":"session","id":"s3","timestamp":"2026-05-27T10:02:00Z"}"#,
t,
);
ingestor.ingest_line_at(
r#"{"type":"future_event_v99","payload":"unknown"}"#,
t + Duration::from_secs(1),
);
// Should not crash
assert_eq!(ingestor.state(), AgentState::Idle);
// Follow-up event still works
ingestor.ingest_line_at(r#"{"type":"turn_start"}"#, t + Duration::from_secs(2));
assert_eq!(ingestor.state(), AgentState::Working);
}
/// Snapshot round-trips and counts states correctly
#[test]
fn snapshot_counts_multiple_panes() {
let snapshot = GlasspaneSnapshot::new(
"test-host",
"2026-05-27T10:00:00Z",
vec![
Pane {
id: "a".into(),
agent: "pi".into(),
state: AgentState::Working,
session_id: Some("s1".into()),
last_event_at: Some("2026-05-27T10:00:00Z".into()),
cwd: None,
stalled: false,
},
Pane {
id: "b".into(),
agent: "pi".into(),
state: AgentState::Blocked,
session_id: Some("s2".into()),
last_event_at: Some("2026-05-27T10:00:01Z".into()),
cwd: None,
stalled: false,
},
Pane {
id: "c".into(),
agent: "pi".into(),
state: AgentState::Idle,
session_id: None,
last_event_at: None,
cwd: None,
stalled: false,
},
],
);
let json = serde_json::to_string(&snapshot).unwrap();
let parsed: GlasspaneSnapshot = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.panes.len(), 3);
assert_eq!(parsed.count(AgentState::Working), 1);
assert_eq!(parsed.count(AgentState::Blocked), 1);
assert_eq!(parsed.count(AgentState::Idle), 1);
}
/// Snapshot -> JSON -> client readable
#[test]
fn snapshot_serializes_for_client() {
let snapshot = GlasspaneSnapshot::new(
"test-host",
"2026-05-27T10:00:00Z",
vec![Pane {
id: "p1".into(),
agent: "pi".into(),
state: AgentState::Working,
session_id: Some("sess-1".into()),
last_event_at: Some("2026-05-27T10:00:00Z".into()),
cwd: None,
stalled: false,
}],
);
let json = serde_json::to_string_pretty(&snapshot).unwrap();
assert!(json.contains("\"id\""));
assert!(json.contains("\"working\""));
assert!(json.contains("\"sess-1\""));
}
/// Empty ingestion → Idle state
#[test]
fn default_ingestor_is_idle() {
let ingestor = PiJsonlIngestor::default();
assert_eq!(ingestor.state(), AgentState::Idle);
assert!(ingestor.session_id().is_none());
}
/// Queue update blocks agent
#[test]
fn queue_update_blocks_agent() {
let mut ingestor = PiJsonlIngestor::default();
let t = now();
ingestor.ingest_line_at(
r#"{"type":"session","id":"s4","timestamp":"2026-05-27T10:03:00Z"}"#,
t,
);
ingestor.ingest_line_at(
r#"{"type":"queue_update","timestamp":"2026-05-27T10:03:01Z","queued":3}"#,
t + Duration::from_secs(1),
);
assert_eq!(ingestor.state(), AgentState::Blocked);
}
}