From 3c10fc098e5e5ac0a0d40ed421ca5aef90cd0486 Mon Sep 17 00:00:00 2001 From: Sam & Hermes Date: Sun, 31 May 2026 16:23:11 +0200 Subject: [PATCH] test: add Pi spawn path proof integration test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validates: Colibri spawns agent process (fake-pi-agent.py) → reads JSONL stdout → glasspane ingests → snapshot shows Done state with correct session ID. Uses scripts/fake-pi-agent.py which emits the colibri-pi-events JSONL taxonomy (session, agent_start, turn_start, turn_end, agent_end). Proves the spawn→ingest→glasspane pipeline without requiring the real pi binary. The real Pi binary path (when installed) follows the same pattern: pi --mode json is spawned with identical spawner code. Build: pass | Tests: 1/1 green | Workspace: all green --- crates/colibri-daemon/tests/pi_spawn_live.rs | 91 ++++++++++++++++++++ scripts/fake-pi-agent.py | 25 ++++++ 2 files changed, 116 insertions(+) create mode 100644 crates/colibri-daemon/tests/pi_spawn_live.rs create mode 100755 scripts/fake-pi-agent.py diff --git a/crates/colibri-daemon/tests/pi_spawn_live.rs b/crates/colibri-daemon/tests/pi_spawn_live.rs new file mode 100644 index 0000000..acdc0ea --- /dev/null +++ b/crates/colibri-daemon/tests/pi_spawn_live.rs @@ -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 + ); +} diff --git a/scripts/fake-pi-agent.py b/scripts/fake-pi-agent.py new file mode 100755 index 0000000..9c88154 --- /dev/null +++ b/scripts/fake-pi-agent.py @@ -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)