test: zot-rpc driver smoke (end-to-end, ZOT_BIN-gated) #160

Merged
clawdie merged 1 commit from zot-rpc-smoke-test into main 2026-06-24 01:09:26 +02:00

View file

@ -0,0 +1,110 @@
//! zot-rpc driver smoke — end-to-end proof of the colibri#143 spawn driver.
//!
//! Spawns a real `zot rpc` subprocess through the Colibri `Spawner` (with
//! `rpc_stdin`), sends a prompt over the driver's `RpcSender`, and reads the
//! agent's stdout JSONL back. Validates the full path: stdin piped → framed
//! prompt delivered → zot runs the loop → events stream out.
//!
//! A placeholder DeepSeek key is enough: zot still acks the request, echoes the
//! prompt as `user_message`, runs a turn, and emits `done` (the turn fails 401,
//! which is the deterministic no-key outcome — we assert on the driver wiring,
//! not on a successful completion).
//!
//! Ignored by default — needs a built zot binary. Run with:
//! ZOT_BIN=/path/to/zot cargo test -p colibri-daemon --test zot_rpc_smoke \
//! -- --ignored --nocapture
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use colibri_daemon::spawner::{AgentSpawnConfig, Provider, Spawner};
use colibri_daemon::DaemonConfig;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::time::timeout;
#[tokio::test]
#[ignore = "needs a built zot binary; set ZOT_BIN"]
async fn zot_rpc_driver_delivers_prompt_and_streams_events() {
let Ok(zot_bin) = std::env::var("ZOT_BIN") else {
eprintln!("ZOT_BIN not set; skipping zot rpc driver smoke");
return;
};
assert!(
std::path::Path::new(&zot_bin).exists(),
"ZOT_BIN does not exist: {zot_bin}"
);
let mut env = HashMap::new();
// Placeholder key: the turn 401s deterministically; the driver wiring (our
// subject) still runs fully and emits the ack → user_message → done.
env.insert(
"DEEPSEEK_API_KEY".to_string(),
"placeholder-not-real".to_string(),
);
let cfg = AgentSpawnConfig {
binary: zot_bin,
args: vec![
"rpc".to_string(),
"--provider".to_string(),
"deepseek".to_string(),
"--model".to_string(),
"deepseek-v4-pro".to_string(),
],
env,
// Local provider → no API-key gate / fallback routing in the spawner.
provider: Provider::Local,
model: "deepseek-v4-pro".to_string(),
// The subject under test: pipe stdin and keep the writer.
rpc_stdin: true,
..Default::default()
};
let spawner = Spawner::new(Arc::new(DaemonConfig::from_env()));
let handle = spawner.spawn(cfg).await.expect("spawn zot rpc");
// The driver must expose an RpcSender for an rpc_stdin agent.
let sender = handle
.rpc_sender()
.expect("rpc agent must expose an RpcSender");
let prompt = "say hi from the colibri rpc driver";
let req_id = sender.send_prompt(prompt).await.expect("send rpc prompt");
assert_eq!(req_id, "1", "first request id should be 1");
let stdout = handle.take_stdout().await.expect("rpc agent stdout");
let mut lines = BufReader::new(stdout).lines();
let mut saw_response_ack = false;
let mut saw_our_prompt = false;
let mut saw_done = false;
// Read until `done` (or timeout). The no-key turn is fast.
let read = timeout(Duration::from_secs(30), async {
while let Ok(Some(line)) = lines.next_line().await {
eprintln!("zot> {line}");
if line.contains(r#""type":"response""#) && line.contains(r#""success":true"#) {
saw_response_ack = true;
}
if line.contains(r#""type":"user_message""#) && line.contains(prompt) {
saw_our_prompt = true;
}
if line.contains(r#""type":"done""#) {
saw_done = true;
break;
}
}
})
.await;
assert!(read.is_ok(), "timed out waiting for zot rpc `done`");
let _ = handle.kill().await;
assert!(saw_response_ack, "missing the response ack to our request");
assert!(
saw_our_prompt,
"zot did not echo our prompt — the driver's stdin prompt was not delivered"
);
assert!(saw_done, "missing the terminal `done` event");
}