Merge v3-t32: pi-durable patch (P3-a), headless harness with channel input (P3-b) (T32)
# Conflicts: # host/src/harness/mod.rs
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
//! 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", "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(())
|
||||
}
|
||||
Reference in New Issue
Block a user