//! Patched pi-durable input placement, hold boundaries, and headless channel lifecycle. use sausage::{ cred::Cred, harness::{ input::{self, Input}, run_module, run_module_with_input, }, world::World, }; use std::{ process::{Command, Stdio}, sync::Arc, time::{Duration, Instant}, }; const BUNDLE: &str = include_str!("../../js/foundation-test-bundle.js"); #[test] fn bundle_has_patched_hooks() { for bundle in [BUNDLE, include_str!("../../js/bundle.js")] { assert!(bundle.contains("handlers.beforeInput")); assert!(bundle.contains("input: true")); assert!(bundle.contains("#held")); } assert!(!include_str!("../../js/patches/pi-durable-1.5.0.patch").contains("dist/env/node")); } async fn probe(case: &str) -> anyhow::Result<()> { let dir = tempfile::tempdir()?; let world = World::open(dir.path())?; let exec = sausage::process::exec(World::open(dir.path())?)?; run_module( world, exec, &format!("globalThis.foundationCase = {case:?};\n{BUNDLE}"), ) .await } #[tokio::test(flavor = "current_thread")] async fn input_placement() -> anyhow::Result<()> { probe("placement").await } #[tokio::test(flavor = "current_thread")] async fn followups_start_one_run() -> anyhow::Result<()> { probe("followups").await } #[tokio::test(flavor = "current_thread")] async fn response_thinking_level() -> anyhow::Result<()> { probe("thinking").await } #[tokio::test(flavor = "current_thread")] async fn hold_parks_at_phase_boundary() -> anyhow::Result<()> { probe("hold").await } #[tokio::test(flavor = "current_thread")] async fn headless_child() -> anyhow::Result<()> { if std::env::var_os("HEADLESS_CHILD").is_none() { return Ok(()); } let dir = tempfile::tempdir()?; let world = World::open(dir.path())?; let cred = Arc::new(Cred::open(&world.host_dir)?); let exec = sausage::process::exec(World::open(dir.path())?)?; let (sender, receiver) = input::channel(); sender.send(Input::Submit { conversation: 1, content: serde_json::json!("channel input"), request_id: "channel-request".into(), })?; sender.send(Input::Stop)?; tokio::time::timeout( Duration::from_secs(30), run_module_with_input( world, exec, cred, receiver, &format!("globalThis.foundationCase = 'headless';\n{BUNDLE}"), ), ) .await??; Ok(()) } #[test] fn channel_with_closed_stdin() -> anyhow::Result<()> { let mut child = Command::new(std::env::current_exe()?) .args([ "--exact", "harness_foundation::headless_child", "--nocapture", ]) .env("HEADLESS_CHILD", "1") .stdin(Stdio::null()) .spawn()?; let deadline = Instant::now() + Duration::from_secs(40); loop { if let Some(status) = child.try_wait()? { assert!(status.success()); break; } if Instant::now() > deadline { child.kill()?; child.wait()?; anyhow::bail!("Headless channel did not stop"); } std::thread::yield_now(); } Ok(()) } #[test] fn cli_stays_up_after_stdin_eof() -> anyhow::Result<()> { let dir = tempfile::tempdir()?; let mut child = Command::new(env!("CARGO_BIN_EXE_sausage")) .arg("run") .arg(dir.path()) .args(["--api-listen", "127.0.0.1:0"]) .env("PI_PROXY_BASE_URL", "https://proxy.invalid") .env("PI_PROXY_API_KEY", "faux-token") .env_remove("SAUSAGE_USERLAND") .stdin(Stdio::null()) .spawn()?; let deadline = Instant::now() + Duration::from_secs(30); loop { if let Some(status) = child.try_wait()? { anyhow::bail!("CLI exited at EOF: {status}"); } if let Ok(db) = rusqlite::Connection::open(dir.path().join("host/harness.sqlite")) && db .query_row("SELECT count(*) FROM conversations", [], |r| { r.get::<_, i64>(0) }) .unwrap_or(0) > 0 { break; } if Instant::now() > deadline { child.kill()?; child.wait()?; anyhow::bail!("CLI did not initialize"); } std::thread::yield_now(); } // EOF shutdown previously took one credential tick. Check beyond that tick. let alive_until = Instant::now() + Duration::from_secs(2); while Instant::now() < alive_until { if let Some(status) = child.try_wait()? { anyhow::bail!("CLI exited after EOF: {status}"); } std::thread::yield_now(); } assert!( Command::new("kill") .args(["-TERM", &child.id().to_string()]) .status()? .success() ); let deadline = Instant::now() + Duration::from_secs(10); loop { if let Some(status) = child.try_wait()? { assert!(status.success()); break; } if Instant::now() > deadline { child.kill()?; child.wait()?; anyhow::bail!("CLI did not stop cleanly"); } std::thread::yield_now(); } Ok(()) }