host: review fixes for cargo.build, the test runner, the console, and main; tests for every PROTOCOL.md rule
cargo.build and the runner (review T22R): builds use the network again; core.unlock and core.notify in order under the runner; quiet test output; manifest preparation keeps [workspace], target tables, renamed and optional entries; cargo.rpc.discover; -32004 with data.output; retention of 10 builds; light, empty test worlds with an in-memory journal; only CARGO_/RUST_/SAUSAGE_ variables; stdin is null; the runner through --config. A compile cache that cannot be created is not fatal. Activation replaces plugin.wasm without a gap. Console and main: init ships the workspace Cargo.lock and creates the world atomically; each console command runs in its own task, so ps, stop, and kill work during a slow call; SAUSAGE_HEADLESS; the terminal mode is restored; signals during boot; chat text is not taken as a command. Tests: 20 more tests for PROTOCOL.md sections 2 to 10; the cargo tests choose the libtest binary from cargo's JSON output and time out.
This commit is contained in:
+252
-1
@@ -1438,7 +1438,7 @@ async fn core_lifetime_events_enter_the_journal() {
|
||||
let h = w.start().await;
|
||||
let stopped = call(&h, "probe.serve", json!({"n":1})).await;
|
||||
call(&h, "core.stop", json!({"pid":stopped})).await;
|
||||
let killed = call(&h, "probe.serve", json!({"n":2})).await;
|
||||
let killed = call(&h, "probe.tree", json!({"depth":0})).await;
|
||||
call(&h, "core.kill", json!({"pid":killed})).await;
|
||||
let entries = call(&h, "core.log", json!({"plugin":"probe","limit":100})).await;
|
||||
let lines: Vec<_> = entries
|
||||
@@ -1706,3 +1706,254 @@ async fn filesystem_native_processes_and_lock_snapshot() {
|
||||
assert!(view.stat(&format!("/proc/{pid}"), true).await.is_err());
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 2: invalid plugin names cannot route calls, and core cannot be shadowed.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn plugin_names_and_core_reservation() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
for name in ["Upper", "bad!", "with space", "é", "", "../probe"] {
|
||||
h.register_native(
|
||||
name,
|
||||
std::sync::Arc::new(|_, _, _| Box::pin(async { Ok(json!("bad")) })),
|
||||
);
|
||||
assert_eq!(
|
||||
h.call(&format!("{name}.echo"), json!({}))
|
||||
.await
|
||||
.unwrap_err()["code"],
|
||||
-32601
|
||||
);
|
||||
}
|
||||
h.register_native(
|
||||
"core",
|
||||
std::sync::Arc::new(|_, _, _| Box::pin(async { Ok(json!("bad")) })),
|
||||
);
|
||||
assert_eq!(
|
||||
h.call("core.unknown", json!({})).await.unwrap_err()["code"],
|
||||
-32601
|
||||
);
|
||||
for name in ["a-z_09", "0"] {
|
||||
h.register_native(
|
||||
name,
|
||||
std::sync::Arc::new(|_, _, _| Box::pin(async { Ok(json!(true)) })),
|
||||
);
|
||||
assert_eq!(call(&h, &format!("{name}.echo"), json!({})).await, true);
|
||||
}
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 3.1: a batch on guest stdout does not execute any of its requests.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn batches_are_discarded() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
assert_eq!(call(&h, "probe.batch", json!({})).await, true);
|
||||
h.stop().await;
|
||||
assert!(!w.0.join("state/batch").exists());
|
||||
assert_eq!(
|
||||
call(&h, "core.log", json!({"grep":"discarded stdout"}))
|
||||
.await
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
/// PROTOCOL 3.3: a second response to an answered call cannot replace its result.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn late_responses_are_dropped() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
assert_eq!(call(&h, "probe.late_answer", json!({})).await, 42);
|
||||
assert_eq!(
|
||||
call(&h, "probe.echo", json!({"next":true})).await,
|
||||
json!({"next":true})
|
||||
);
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 3.4: core.call notifications forget detached children after the caller ends.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn notification_core_call_forgets_detached_child() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
let parent = call(&h, "probe.forget", json!({})).await;
|
||||
let child: Value = tokio::time::timeout(std::time::Duration::from_secs(10), async {
|
||||
loop {
|
||||
if let Ok(bytes) = std::fs::read(w.0.join("state/forgotten"))
|
||||
&& let Ok(value) = serde_json::from_slice::<Value>(&bytes)
|
||||
{
|
||||
break value;
|
||||
}
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
tokio::time::timeout(std::time::Duration::from_secs(10), async {
|
||||
while h.processes().contains_key(parent["pid"].as_str().unwrap()) {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(h.processes()[child["pid"].as_str().unwrap()].detached);
|
||||
assert!(
|
||||
h.processes()[child["pid"].as_str().unwrap()]
|
||||
.parent
|
||||
.is_none()
|
||||
);
|
||||
call(&h, "core.stop", json!({"pid":child["pid"]})).await;
|
||||
assert!(!h.processes().contains_key(child["pid"].as_str().unwrap()));
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 3.6: three generations that ignore EOF stop together within one five-second deadline.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn attached_tree_stops_in_parallel() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
let root = call(&h, "probe.tree", json!({"depth":2})).await;
|
||||
assert_eq!(
|
||||
h.processes()
|
||||
.values()
|
||||
.filter(|p| p.method == "tree")
|
||||
.count(),
|
||||
3
|
||||
);
|
||||
let before = std::time::Instant::now();
|
||||
call(&h, "core.stop", json!({"pid":root})).await;
|
||||
tokio::time::timeout(std::time::Duration::from_secs(2), async {
|
||||
while h.processes().values().any(|p| p.method == "tree") {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(before.elapsed() >= std::time::Duration::from_millis(4900));
|
||||
assert!(before.elapsed() < std::time::Duration::from_secs(7));
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 3.8: a nested call receives its caller's process ID as SAUSAGE_PARENT.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn nested_parent_environment() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
let result = call(&h, "probe.nested_environment", json!({})).await;
|
||||
assert_eq!(result["child"]["parent"], result["caller"]);
|
||||
assert_ne!(result["child"]["pid"], result["caller"]);
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 8 and 3.8: boot services receive core as SAUSAGE_PARENT.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn service_parent_environment() {
|
||||
let w = TestWorld::new();
|
||||
std::fs::write(
|
||||
w.0.join("config/boot.json"),
|
||||
br#"[{"method":"probe.forgotten","params":{}}]"#,
|
||||
)
|
||||
.unwrap();
|
||||
let h = w.start().await;
|
||||
let env: Value =
|
||||
serde_json::from_slice(&std::fs::read(w.0.join("state/forgotten")).unwrap()).unwrap();
|
||||
assert_eq!(env["parent"], "core");
|
||||
assert_eq!(env["plugin"], "probe");
|
||||
assert!(h.processes()[env["pid"].as_str().unwrap()].detached);
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 4: info.json exposes every field of the live process snapshot.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn info_json_has_all_process_fields() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
let params = json!({"marker":"complete"});
|
||||
let pid = call(&h, "probe.serve", params.clone()).await;
|
||||
let text = call(
|
||||
&h,
|
||||
"probe.read",
|
||||
json!({"path":format!("/proc/{}/info.json", pid.as_str().unwrap())}),
|
||||
)
|
||||
.await;
|
||||
let info: Value = serde_json::from_str(text.as_str().unwrap()).unwrap();
|
||||
let expected = serde_json::to_value(&h.processes()[pid.as_str().unwrap()]).unwrap();
|
||||
assert_eq!(info, expected);
|
||||
for field in [
|
||||
"plugin",
|
||||
"version",
|
||||
"method",
|
||||
"params",
|
||||
"parent",
|
||||
"detached",
|
||||
"start_time",
|
||||
"state",
|
||||
] {
|
||||
assert!(info.get(field).is_some(), "{field}");
|
||||
}
|
||||
assert_eq!(info["params"], params);
|
||||
assert_eq!(info["parent"], "console");
|
||||
assert_eq!(info["state"], "running");
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 3.6: detached processes persist until stop, kill, or host shutdown explicitly ends them.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn detached_processes_need_explicit_termination() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
let mut pids = Vec::new();
|
||||
for _ in 0..3 {
|
||||
let pid = call(
|
||||
&h,
|
||||
"core.call",
|
||||
json!({"method":"probe.serve","params":{},"detach":true}),
|
||||
)
|
||||
.await;
|
||||
assert!(h.processes()[pid.as_str().unwrap()].parent.is_none());
|
||||
pids.push(pid);
|
||||
}
|
||||
call(&h, "probe.echo", json!({})).await;
|
||||
for pid in &pids {
|
||||
assert!(h.processes().contains_key(pid.as_str().unwrap()));
|
||||
}
|
||||
call(&h, "core.stop", json!({"pid":pids[0]})).await;
|
||||
call(&h, "core.kill", json!({"pid":pids[1]})).await;
|
||||
assert!(h.processes().contains_key(pids[2].as_str().unwrap()));
|
||||
h.stop().await;
|
||||
for pid in pids {
|
||||
assert!(!h.processes().contains_key(pid.as_str().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() {
|
||||
let w = TestWorld::new();
|
||||
std::fs::write(
|
||||
w.0.join("config/boot.json"),
|
||||
br#"[{"method":"core.exit","params":{}}]"#,
|
||||
)
|
||||
.unwrap();
|
||||
let output = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(30),
|
||||
tokio::process::Command::new(env!("CARGO_BIN_EXE_sausage"))
|
||||
.arg("--headless")
|
||||
.arg("run")
|
||||
.arg(&w.0)
|
||||
.stdin(std::process::Stdio::null())
|
||||
.kill_on_drop(true)
|
||||
.output(),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
output.status.code(),
|
||||
Some(75),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user