test: add Pi spawn path proof integration test #6
2 changed files with 116 additions and 0 deletions
91
crates/colibri-daemon/tests/pi_spawn_live.rs
Normal file
91
crates/colibri-daemon/tests/pi_spawn_live.rs
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
//! Pi spawn path proof — integration test.
|
||||||
|
//!
|
||||||
|
//! Validates: Colibri spawns agent → reads JSONL stdout → glasspane ingests → snapshot correct.
|
||||||
|
//! Uses scripts/fake-pi-agent.py which emits the colibri-pi-events JSONL taxonomy.
|
||||||
|
//!
|
||||||
|
//! With real Pi binary (when installed):
|
||||||
|
//! COLIBRI_PI_BINARY=pi cargo test -p colibri-daemon --test pi_spawn_live -- --nocapture
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::Stdio;
|
||||||
|
use std::time::SystemTime;
|
||||||
|
|
||||||
|
use colibri_glasspane::DEFAULT_STALL_AFTER;
|
||||||
|
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||||
|
use tokio::process::Command;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn pi_spawn_path_produces_correct_glasspane_state() {
|
||||||
|
let script = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.parent()
|
||||||
|
.unwrap()
|
||||||
|
.parent()
|
||||||
|
.unwrap()
|
||||||
|
.join("scripts")
|
||||||
|
.join("fake-pi-agent.py");
|
||||||
|
|
||||||
|
assert!(script.exists(), "fake-pi-agent.py not found at {script:?}");
|
||||||
|
|
||||||
|
let mut supervisor = colibri_glasspane::PaneSupervisor::new();
|
||||||
|
let pane_id = "pi-spawn-proof";
|
||||||
|
supervisor.attach_pane_at(pane_id, "fake-pi", SystemTime::now());
|
||||||
|
|
||||||
|
let mut child = Command::new(PathBuf::from("python3"))
|
||||||
|
.arg(&script)
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.spawn()
|
||||||
|
.expect("failed to spawn fake-pi-agent.py");
|
||||||
|
|
||||||
|
let stdout = child.stdout.take().expect("no stdout");
|
||||||
|
let mut reader = BufReader::new(stdout).lines();
|
||||||
|
let mut accepted = 0usize;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match reader.next_line().await {
|
||||||
|
Ok(Some(line)) => {
|
||||||
|
if supervisor
|
||||||
|
.ingest_line_at(pane_id, &line, SystemTime::now())
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
accepted += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None) => break,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("JSONL read error: {e}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = child.wait().await.expect("child wait failed");
|
||||||
|
assert!(status.success(), "fake-pi-agent.py exited with {status}");
|
||||||
|
assert!(
|
||||||
|
accepted >= 5,
|
||||||
|
"expected >=5 accepted JSONL lines, got {accepted}"
|
||||||
|
);
|
||||||
|
|
||||||
|
let snapshot = supervisor.snapshot_at("test-host", SystemTime::now(), DEFAULT_STALL_AFTER);
|
||||||
|
let pane = snapshot
|
||||||
|
.panes
|
||||||
|
.iter()
|
||||||
|
.find(|p| p.id == pane_id)
|
||||||
|
.expect("pane not found");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
pane.state,
|
||||||
|
colibri_glasspane::AgentState::Done,
|
||||||
|
"expected Done after full agent run"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
pane.pi_session_id.is_some(),
|
||||||
|
"expected pi_session_id from session event, got {:?}",
|
||||||
|
pane.pi_session_id
|
||||||
|
);
|
||||||
|
|
||||||
|
eprintln!(
|
||||||
|
"✓ Pi spawn proof: {} accepted, state={:?}, session={:?}",
|
||||||
|
accepted, pane.state, pane.pi_session_id
|
||||||
|
);
|
||||||
|
}
|
||||||
25
scripts/fake-pi-agent.py
Executable file
25
scripts/fake-pi-agent.py
Executable file
|
|
@ -0,0 +1,25 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Fake Pi agent — emits JSONL in the colibri-pi-events format.
|
||||||
|
Used by Colibri integration tests to validate the spawn → JSONL → glasspane path.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
|
messages = [
|
||||||
|
{"type": "session", "id": f"pi-test-{int(time.time())}", "cwd": "/tmp"},
|
||||||
|
{"type": "agent_start"},
|
||||||
|
{"type": "turn_start"},
|
||||||
|
{"type": "message_start"},
|
||||||
|
{"type": "message_update", "delta": "Processing task..."},
|
||||||
|
{"type": "message_end"},
|
||||||
|
{"type": "turn_end", "stop": "end_turn"},
|
||||||
|
{"type": "agent_end"},
|
||||||
|
]
|
||||||
|
|
||||||
|
for msg in messages:
|
||||||
|
sys.stdout.write(json.dumps(msg) + "\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
time.sleep(0.01)
|
||||||
|
|
||||||
|
sys.exit(0)
|
||||||
Loading…
Add table
Reference in a new issue