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