host: add supervisor-gated clean core.restart RPC

Return success before scheduling the existing shutdown path, coalesce pending
requests, and mark #loop invocations with SAUSAGE_LOOP=1. Keep core.exit
compatibility and document explicit per-call approval for all agents.

Card: SAU-134
This commit is contained in:
SAU-134 implementer
2026-10-26 09:16:10 +00:00
parent 895be183c9
commit 8c46d9b9d0
10 changed files with 257 additions and 9 deletions
+58
View File
@@ -1931,6 +1931,64 @@ async fn detached_processes_need_explicit_termination() {
}
}
/// PROTOCOL 9: restart answers the detached caller, then uses normal stop and status 75.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn core_restart_cleanly_stops_detached_caller() {
tokio::time::timeout(std::time::Duration::from_secs(30), async {
let w = TestWorld::new();
std::fs::write(
w.0.join("config/boot.yaml"),
br#"[{"method":"probe.restart","params":{}}]"#,
)
.unwrap();
let output = tokio::process::Command::new(env!("CARGO_BIN_EXE_sausage"))
.env("SAUSAGE_LOOP", "1")
.arg("--headless")
.arg("--cache-dir")
.arg(concat!(env!("CARGO_TARGET_TMPDIR"), "/cache"))
.arg("run")
.arg(&w.0)
.stdin(std::process::Stdio::null())
.kill_on_drop(true)
.output()
.await
.unwrap();
assert_eq!(
output.status.code(),
Some(75),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
std::fs::read_to_string(w.0.join("state/restart-answer")).unwrap(),
"received"
);
assert_eq!(
std::fs::read_to_string(w.0.join("state/restart-stop")).unwrap(),
"persisted after EOF"
);
let db = rusqlite::Connection::open(w.0.join("state/core/journal.sqlite")).unwrap();
let count: i64 = db
.query_row(
"SELECT count(*) FROM journal WHERE line = 'restart stop hook finished'",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(count, 1);
let killed: i64 = db
.query_row(
"SELECT count(*) FROM journal WHERE line = 'kill'",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(killed, 0);
})
.await
.unwrap();
}
/// PROTOCOL 9: core.exit from boot ends the host executable with status 75.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn core_exit_sets_executable_status_75() {
+15
View File
@@ -447,6 +447,21 @@ fn dispatch(request: &Value, lines: &mut Lines) -> Option<Value> {
burst(request);
return None;
}
"restart" => {
assert_eq!(
lock(&json!({"path":"/state/restart-stop"}), lines),
json!({"held":true})
);
assert_eq!(
outgoing_call(&json!({"method":"core.restart", "params":{}}), lines),
json!({})
);
std::fs::write("/state/restart-answer", "received").unwrap();
serve(request, lines);
std::fs::write("/state/restart-stop", "persisted after EOF").unwrap();
eprintln!("restart stop hook finished");
return None;
}
"serve" => {
serve(request, lines);
return None;