console: one sequential loop; host tests in one binary

The console reads a line, runs it to the end, and prints the result,
with and without a terminal. Notifications print while a command runs.
Ctrl-C during a command stops only the wait. 314 lines (was 480).

All host integration tests are modules of cli/tests/host/, so they run
in one parallel pool, and the probes build once. Warm, the 184 host
tests take 24 s (was about 45 s as seven binaries in sequence), and
cargo test --workspace takes 45 s.
This commit is contained in:
2026-10-23 09:14:26 +01:00
parent e3935031ac
commit 1617dfc1ee
12 changed files with 377 additions and 559 deletions
+2 -34
View File
@@ -111,40 +111,8 @@ impl Drop for Llm {
fn fixtures() -> &'static PathBuf {
static TARGET: OnceLock<PathBuf> = OnceLock::new();
TARGET.get_or_init(|| {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.to_owned();
let target = PathBuf::from(concat!(env!("CARGO_TARGET_TMPDIR"), "/guests"));
for args in [
vec![
"build",
"--offline",
"-p",
"agent",
"--target",
"wasm32-wasip2",
],
vec![
"build",
"--offline",
"--manifest-path",
"sdk/tests/plugins/probe/Cargo.toml",
"--target",
"wasm32-wasip2",
],
] {
assert!(
std::process::Command::new("cargo")
.args(args)
.current_dir(&root)
.env("CARGO_TARGET_DIR", &target)
.status()
.unwrap()
.success()
);
}
target.join("wasm32-wasip2/debug")
super::common::build("agent", "agent");
super::common::sdk_probe().parent().unwrap().to_owned()
})
}
struct TestWorld(PathBuf);
@@ -1,9 +1,9 @@
//! Native build and WASI libtest integration checks under PROTOCOL.md section 11.
use super::common::CACHE;
use sausage::{Host, world::World};
use serde_json::{Value, json};
use std::path::{Path, PathBuf};
use tokio::sync::OnceCell;
const CACHE: &str = concat!(env!("CARGO_TARGET_TMPDIR"), "/cache");
struct TestWorld(PathBuf);
impl TestWorld {
/// Creates a fresh native build world.
+58 -34
View File
@@ -4,7 +4,6 @@ use serde_json::{Value, json};
use std::{
path::{Path, PathBuf},
process::Stdio,
sync::OnceLock,
time::Duration,
};
use tokio::{
@@ -12,7 +11,7 @@ use tokio::{
process::{Child, Command},
};
const CACHE: &str = concat!(env!("CARGO_TARGET_TMPDIR"), "/cache");
use super::common::{CACHE, probe};
fn binary() -> Command {
let mut command = Command::new(env!("CARGO_BIN_EXE_sausage"));
@@ -50,31 +49,6 @@ fn signal(child: &Child, signal: nix::sys::signal::Signal) {
)
.unwrap();
}
fn probe() -> PathBuf {
static WASM: OnceLock<PathBuf> = OnceLock::new();
WASM.get_or_init(|| {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
let target = root.join("target/core/console-guests");
assert!(
std::process::Command::new("cargo")
.args([
"build",
"--offline",
"--target",
"wasm32-wasip2",
"--manifest-path"
])
.arg(root.join("cli/tests/plugins/probe/Cargo.toml"))
.arg("--target-dir")
.arg(&target)
.status()
.unwrap()
.success()
);
target.join("wasm32-wasip2/debug/probe.wasm")
})
.clone()
}
fn install_probe(world: &Path) {
std::fs::create_dir_all(world.join("plugins/probe")).unwrap();
std::fs::copy(probe(), world.join("plugins/probe/plugin.wasm")).unwrap();
@@ -137,7 +111,7 @@ async fn init_builds_agent() {
}
assert_eq!(
std::fs::read(world.join("plugins/agent/source/Cargo.lock")).unwrap(),
include_bytes!("../../Cargo.lock")
include_bytes!("../../../Cargo.lock")
);
assert!(!world.join("state/cargo/target").exists());
assert!(
@@ -307,6 +281,60 @@ async fn console_error_stops_host() {
assert!(stopped > 0);
}
/// SIGINT stops only the wait, so ps can still find the live call.
#[tokio::test]
async fn interrupt_leaves_call_running() {
let (_folder, world) = world();
install_probe(&world);
let mut child = binary()
.arg("run")
.arg(&world)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
let mut input = child.stdin.take().unwrap();
input.write_all(b"probe.busy\n").await.unwrap();
let pid = tokio::time::timeout(Duration::from_secs(20), async {
loop {
if world.join("state/core/journal.sqlite").exists() {
let db =
rusqlite::Connection::open(world.join("state/core/journal.sqlite")).unwrap();
if let Ok(pid) = db.query_row(
"SELECT pid FROM journal WHERE plugin='probe' AND line='start busy {}'",
[],
|r| r.get::<_, String>(0),
) {
break pid;
}
}
tokio::task::yield_now().await;
}
})
.await
.unwrap();
signal(&child, nix::sys::signal::Signal::SIGINT);
input
.write_all(format!("ps\nkill {pid}\nquit\n").as_bytes())
.await
.unwrap();
drop(input);
let output = tokio::time::timeout(Duration::from_secs(20), child.wait_with_output())
.await
.unwrap()
.unwrap();
let text = String::from_utf8(output.stdout).unwrap();
assert_eq!(output.status.code(), Some(0), "{text}");
assert!(
text.contains("stopped waiting; the call continues, see ps"),
"{text}"
);
assert!(
text.contains(&format!("{pid} probe busy console running")),
"{text}"
);
}
/// Piped calls wait for activation, and EOF or quit preserves the last result.
#[tokio::test]
async fn piped_console_runs_dependent_commands_in_order() {
@@ -345,14 +373,10 @@ async fn piped_console_runs_dependent_commands_in_order() {
);
assert!(!stdout.contains("error:"), "{stdout}");
assert!(stdout.contains(r#""ordered": true"#), "{stdout}");
assert!(
stdout.find(&format!("> {activate}")).unwrap()
< stdout.find(&format!("> {call}")).unwrap(),
"{stdout}"
);
assert!(!stdout.contains("> core.activate"), "{stdout}");
if !ending.is_empty() {
assert!(
stdout.find(r#""ordered": true"#).unwrap() < stdout.find("> quit").unwrap(),
stdout.find(r#""ordered": true"#).unwrap() < stdout.find("Goodbye.").unwrap(),
"{stdout}"
);
}
+43
View File
@@ -0,0 +1,43 @@
//! Persistent caches and guest builds shared by the host integration modules.
use std::{
path::{Path, PathBuf},
sync::OnceLock,
};
pub const CACHE: &str = concat!(env!("CARGO_TARGET_TMPDIR"), "/cache");
pub fn build(package: &str, binary: &str) -> PathBuf {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
let target = Path::new(concat!(env!("CARGO_TARGET_TMPDIR"), "/guests"));
assert!(
std::process::Command::new("cargo")
.args([
"build",
"--offline",
"--target",
"wasm32-wasip2",
"--manifest-path"
])
.arg(root.join(package).join("Cargo.toml"))
.arg("--target-dir")
.arg(target)
.status()
.unwrap()
.success()
);
target
.join("wasm32-wasip2/debug")
.join(format!("{binary}.wasm"))
}
pub fn probe() -> PathBuf {
static WASM: OnceLock<PathBuf> = OnceLock::new();
WASM.get_or_init(|| build("cli/tests/plugins/probe", "probe"))
.clone()
}
pub fn sdk_probe() -> PathBuf {
static WASM: OnceLock<PathBuf> = OnceLock::new();
WASM.get_or_init(|| build("sdk/tests/plugins/probe", "sdk-probe"))
.clone()
}
@@ -1,38 +1,12 @@
//! Console command contracts against a temporary world and the raw probe component.
use super::common::probe as fixture;
use sausage::{
Host,
console::{execute_line, notification_text},
world::World,
};
use serde_json::{Value, json};
use std::{path::PathBuf, sync::OnceLock};
fn fixture() -> PathBuf {
static WASM: OnceLock<PathBuf> = OnceLock::new();
WASM.get_or_init(|| {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.to_owned();
let target = PathBuf::from(concat!(env!("CARGO_TARGET_TMPDIR"), "/guests"));
let status = std::process::Command::new("cargo")
.args([
"build",
"--offline",
"--target",
"wasm32-wasip2",
"--manifest-path",
])
.arg(root.join("cli/tests/plugins/probe/Cargo.toml"))
.arg("--target-dir")
.arg(&target)
.status()
.unwrap();
assert!(status.success());
target.join("wasm32-wasip2/debug/probe.wasm")
})
.clone()
}
use std::path::PathBuf;
struct TestWorld(PathBuf);
impl TestWorld {
fn new() -> Self {
@@ -75,6 +49,11 @@ async fn rpc_call() {
let result = command(&host, "probe.echo {\"answer\":42}").await;
assert!(result.contains("\n \"answer\": 42\n"));
assert!(command(&host, "absent.echo {}").await.contains("error:"));
assert_eq!(
serde_json::from_str::<Value>(&command(&host, r#"agent.chat {"text":"explicit"}"#).await)
.unwrap(),
json!({"text":"explicit"})
);
assert_eq!(
command(&host, "probe.echo not-json").await,
"probe.echo not-json"
@@ -111,6 +90,7 @@ async fn prose_is_not_a_command() {
"ls too many words",
"log -n nope",
"README.md says hello",
"agent.chat not-json",
"e.g. write notes",
] {
assert_eq!(command(&host, line).await, line);
@@ -185,6 +165,8 @@ async fn log() {
std::fs::copy(fixture(), world.0.join("plugins/data-logger/plugin.wasm")).unwrap();
let host = world.start().await;
host.call("data-logger.echo", json!({})).await.unwrap();
// Finish discovery before comparing two snapshots of the journal.
command(&host, "methods data-logger").await;
let pid = host.call("probe.serve", json!({})).await.unwrap();
for (line, filters) in [
("log".to_owned(), json!({})),
+2 -27
View File
@@ -1,33 +1,8 @@
//! End-to-end host behavior with a raw WASI guest.
use super::common::probe as fixture;
use sausage::{Host, world::World};
use serde_json::{Value, json};
use std::{path::PathBuf, sync::OnceLock};
fn fixture() -> PathBuf {
static WASM: OnceLock<PathBuf> = OnceLock::new();
WASM.get_or_init(|| {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.to_path_buf();
let target = PathBuf::from(concat!(env!("CARGO_TARGET_TMPDIR"), "/guests"));
let status = std::process::Command::new("cargo")
.args([
"build",
"--offline",
"--target",
"wasm32-wasip2",
"--manifest-path",
])
.arg(root.join("cli/tests/plugins/probe/Cargo.toml"))
.arg("--target-dir")
.arg(&target)
.status()
.unwrap();
assert!(status.success());
target.join("wasm32-wasip2/debug/probe.wasm")
})
.clone()
}
use std::path::PathBuf;
struct TestWorld(PathBuf);
impl TestWorld {
fn new() -> Self {
+9
View File
@@ -0,0 +1,9 @@
//! Host integration contracts run in one parallel libtest pool.
mod activation;
mod agent;
mod cargo;
mod cli;
mod common;
mod console;
mod core;
mod sdk;
+12 -30
View File
@@ -6,36 +6,16 @@ use std::{path::PathBuf, sync::OnceLock, time::Duration};
fn fixtures() -> &'static PathBuf {
static TARGET: OnceLock<PathBuf> = OnceLock::new();
TARGET.get_or_init(|| {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.to_owned();
let target = PathBuf::from(concat!(env!("CARGO_TARGET_TMPDIR"), "/guests"));
for package in [
"examples/notes",
"examples/consumer",
"examples/www",
"examples/secrets",
"sdk/tests/plugins/probe",
"sdk/tests/plugins/consumer",
for (package, binary) in [
("examples/notes", "notes"),
("examples/consumer", "consumer"),
("examples/www", "www"),
("examples/secrets", "secrets"),
("sdk/tests/plugins/consumer", "sdk-consumer"),
] {
assert!(
std::process::Command::new("cargo")
.args([
"build",
"--offline",
"--target",
"wasm32-wasip2",
"--manifest-path"
])
.arg(root.join(package).join("Cargo.toml"))
.env("CARGO_TARGET_DIR", &target)
.status()
.unwrap()
.success()
);
super::common::build(package, binary);
}
target.join("wasm32-wasip2/debug")
super::common::sdk_probe().parent().unwrap().to_owned()
})
}
struct TestWorld(PathBuf);
@@ -133,8 +113,10 @@ async fn discovery_is_compatible() {
let host = world.start().await;
let actual = call(&host, "notes.rpc.discover", json!({})).await;
let consumer = call(&host, "consumer.rpc.discover", json!({})).await;
let expected: Value =
serde_json::from_str(include_str!("../../examples/consumer/imports/notes.json")).unwrap();
let expected: Value = serde_json::from_str(include_str!(
"../../../examples/consumer/imports/notes.json"
))
.unwrap();
assert_eq!(actual, expected);
assert_eq!(actual["openrpc"], "1.2.6");
assert!(actual["components"]["schemas"]["AddParams"].is_object());