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>
162 lines
5.9 KiB
Rust
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);
|
|
}
|
|
}
|