seed: carry restart approval policy into new worlds

Make legacy core.exit share core.restart's supervisor gate and pending
request. Seed the stable agent prompt and boot comments with the approval
and shutdown contract, and check real init output under a deadline.
Cover both aliases' gate, response grace, coalescing and exit status.

Card: SAU-135
This commit is contained in:
SAU-135 implementer
2026-10-26 10:29:12 +00:00
parent 8c46d9b9d0
commit 168795b408
10 changed files with 114 additions and 90 deletions
+48 -13
View File
@@ -19,6 +19,7 @@ fn binary() -> Command {
.arg("--cache-dir")
.arg(CACHE)
.env_remove("SAUSAGE_HEADLESS")
.env_remove("SAUSAGE_LOOP")
.kill_on_drop(true);
command
}
@@ -54,9 +55,15 @@ fn install_probe(world: &Path) {
std::fs::copy(probe(), world.join("plugins/probe/plugin.wasm")).unwrap();
}
/// A failed agent build leaves no world. Bare sausage retries and publishes a complete world.
/// Failed init leaves no world; successful init seeds the restart policy and a working agent.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn init_builds_agent() {
tokio::time::timeout(Duration::from_secs(300), init_builds_agent_inner())
.await
.expect("init and seeded-agent checks exceeded 300 seconds");
}
async fn init_builds_agent_inner() {
let temporary = tempfile::tempdir().unwrap();
let world = temporary.path().join("world");
// Legacy configuration variables must not affect the templates.
@@ -81,7 +88,7 @@ async fn init_builds_agent() {
assert!(!failed.status.success());
assert!(String::from_utf8_lossy(&failed.stderr).contains("nonexistent-sausage-rustc"));
assert!(!world.exists());
let output = configured().output().await.unwrap();
let output = configured().arg("init").output().await.unwrap();
assert!(
output.status.success(),
"stdout: {}\nstderr: {}",
@@ -91,6 +98,22 @@ async fn init_builds_agent() {
assert!(
String::from_utf8_lossy(&output.stdout).contains("first agent build takes a few minutes")
);
// Bare sausage reopens the completed world instead of trying to recreate it.
assert!(configured().output().await.unwrap().status.success());
let prompt = std::fs::read_to_string(world.join("plugins/agent/source/src/prompt.md")).unwrap();
assert!(prompt.contains("HARD RULE: the mediator and all agents NEVER call `core.restart`"));
assert!(prompt.contains("without the person's explicit approval each time"));
assert!(prompt.contains("alias `core.exit`"));
for text in [
"`#loop`",
"SAUSAGE_LOOP=1",
"answers before",
"exit code 75",
] {
assert!(prompt.contains(text), "seeded prompt missing {text}");
}
let boot = std::fs::read_to_string(world.join("config/boot.yaml")).unwrap();
assert!(boot.contains("SAUSAGE_LOOP=1"));
let mut config: Value =
serde_yaml_ng::from_slice(&std::fs::read(world.join("config/agent.yaml")).unwrap())
.unwrap();
@@ -249,27 +272,39 @@ async fn signals_work_during_boot() {
/// Piped input supports commands, quit, and the restart exit code.
#[tokio::test]
async fn piped_console_and_exit_codes() {
for (line, expected) in [("help\n", 0), ("quit\n", 0), ("core.exit\n", 75)] {
for (line, supervised, expected) in [
("help\n", false, 0),
("quit\n", false, 0),
("core.restart\n", false, 0),
("core.exit\n", false, 0),
("core.restart\n", true, 75),
("core.exit\n", true, 75),
] {
let (_folder, world) = world();
let mut child = binary()
.arg("run")
.arg(world)
let mut command = binary();
command.arg("run").arg(world);
if supervised {
command.env("SAUSAGE_LOOP", "1");
}
let mut child = command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
child
.stdin
.take()
.unwrap()
.write_all(line.as_bytes())
.await
.unwrap();
// Supervised calls must finish via restart, not a racing console EOF.
let mut input = child.stdin.take().unwrap();
input.write_all(line.as_bytes()).await.unwrap();
if !supervised {
drop(input);
}
let output = tokio::time::timeout(Duration::from_secs(15), child.wait_with_output())
.await
.unwrap()
.unwrap();
assert_eq!(output.status.code(), Some(expected));
if !supervised && line.starts_with("core.") {
assert!(String::from_utf8_lossy(&output.stdout).contains("SAUSAGE_LOOP not set"));
}
if expected == 0 {
assert!(!output.stdout.is_empty());
}
+3 -28
View File
@@ -1014,19 +1014,6 @@ async fn stop_closes_stdin_and_waits_for_exit() {
h.stop().await;
}
/// PROTOCOL 9: core exit requests restart from its handler.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn core_exit_requests_restart_from_its_handler() {
let w = TestWorld::new();
let h = w.start().await;
assert_eq!(call(&h, "core.exit", json!({})).await, json!({}));
tokio::time::timeout(std::time::Duration::from_secs(1), h.exit_requested())
.await
.unwrap();
h.stop().await;
}
/// PROTOCOL 3.3: process ids are uuidv7 and versions are sha256.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn process_ids_are_uuidv7_and_versions_are_sha256() {
@@ -1288,19 +1275,6 @@ async fn journal_has_wal_and_identity_indexes() {
h.stop().await;
}
/// PROTOCOL 9: core exit works through core call.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn core_exit_works_through_core_call() {
let w = TestWorld::new();
let h = w.start().await;
assert_eq!(
call(&h, "core.call", json!({"method":"core.exit","params":{}})).await,
json!({})
);
assert!(h.restart_requested());
h.stop().await;
}
/// PROTOCOL 6: acquisitions from different processes keep their FIFO positions.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn lock_acquisitions_from_different_processes_are_fifo() {
@@ -1989,9 +1963,9 @@ async fn core_restart_cleanly_stops_detached_caller() {
.unwrap();
}
/// PROTOCOL 9: core.exit from boot ends the host executable with status 75.
/// PROTOCOL 9: supervised core.exit from boot uses the restart shutdown path and code 75.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn core_exit_sets_executable_status_75() {
async fn core_exit_alias_sets_executable_status_75() {
let w = TestWorld::new();
std::fs::write(
w.0.join("config/boot.yaml"),
@@ -2006,6 +1980,7 @@ async fn core_exit_sets_executable_status_75() {
.arg(concat!(env!("CARGO_TARGET_TMPDIR"), "/cache"))
.arg("run")
.arg(&w.0)
.env("SAUSAGE_LOOP", "1")
.stdin(std::process::Stdio::null())
.kill_on_drop(true)
.output(),