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:
+182
-2
@@ -313,6 +313,24 @@ async fn review_retirement_checks_live_version() {
|
||||
marker(world.0.join("state/notes/retired"), &previous).await;
|
||||
host.stop().await;
|
||||
}
|
||||
/// T23: an unwritable compile cache does not prevent a world from running.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn unwritable_compile_cache_is_nonfatal() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let world = TestWorld::new();
|
||||
world.plugin("notes", document("add"), document("add"));
|
||||
let parent = world.0.join("unwritable");
|
||||
std::fs::create_dir(&parent).unwrap();
|
||||
std::fs::set_permissions(&parent, std::fs::Permissions::from_mode(0o500)).unwrap();
|
||||
let result = Host::start_with_cache(&world.0, &parent.join("cache")).await;
|
||||
std::fs::set_permissions(&parent, std::fs::Permissions::from_mode(0o700)).unwrap();
|
||||
let host = result.unwrap();
|
||||
assert_eq!(
|
||||
call(&host, "notes.version", json!({})).await["version"],
|
||||
"1"
|
||||
);
|
||||
host.stop().await;
|
||||
}
|
||||
/// L13: source subtrees require no watches, even when the host cannot read them.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn review_watcher_is_shallow() {
|
||||
@@ -322,7 +340,7 @@ async fn review_watcher_is_shallow() {
|
||||
let source = world.0.join("plugins/notes/source");
|
||||
std::fs::create_dir_all(&source).unwrap();
|
||||
std::fs::set_permissions(&source, std::fs::Permissions::from_mode(0o0)).unwrap();
|
||||
let result = Host::start(&world.0).await;
|
||||
let result = Host::start_with_cache(&world.0, &world.0.join("compile-cache")).await;
|
||||
std::fs::set_permissions(&source, std::fs::Permissions::from_mode(0o700)).unwrap();
|
||||
let host = result.unwrap();
|
||||
activate(&host, "notes").await.unwrap();
|
||||
@@ -337,7 +355,7 @@ async fn review_watcher_failure_is_nonfatal() {
|
||||
let inaccessible = world.0.join("plugins/inaccessible");
|
||||
std::fs::create_dir_all(&inaccessible).unwrap();
|
||||
std::fs::set_permissions(&inaccessible, std::fs::Permissions::from_mode(0o0)).unwrap();
|
||||
let result = Host::start(&world.0).await;
|
||||
let result = Host::start_with_cache(&world.0, &world.0.join("compile-cache")).await;
|
||||
std::fs::set_permissions(&inaccessible, std::fs::Permissions::from_mode(0o700)).unwrap();
|
||||
let host = result.unwrap();
|
||||
assert!(
|
||||
@@ -785,3 +803,165 @@ async fn does_not_restart_detached_call() {
|
||||
);
|
||||
host.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 7: activation records the plugin's upgrade grace period in the journal.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn upgrades_enter_the_journal() {
|
||||
let world = TestWorld::new();
|
||||
let mut old = document("hold");
|
||||
old.as_object_mut().unwrap().remove("x-sausage-grace");
|
||||
world.plugin("notes", old, document("hold"));
|
||||
let host = world.start().await;
|
||||
call(&host, "notes.hold", json!({})).await;
|
||||
activate(&host, "notes").await.unwrap();
|
||||
let entries = tokio::time::timeout(Duration::from_secs(10), async {
|
||||
loop {
|
||||
let entries = call(
|
||||
&host,
|
||||
"core.log",
|
||||
json!({"plugin":"notes","grep":"upgrade grace"}),
|
||||
)
|
||||
.await;
|
||||
if entries
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|e| e["line"] == "upgrade grace 60 seconds")
|
||||
{
|
||||
break entries;
|
||||
}
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("the upgrade journal entry must arrive within 10 seconds");
|
||||
assert!(
|
||||
entries
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.all(|e| e["stream"] == "core")
|
||||
);
|
||||
host.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 10.2 and 10.3: breaking endpoint methods reject activation with counterexamples where possible.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn endpoint_contracts_and_activation_counterexamples() {
|
||||
let method = json!({"name":"stats","params":[{"name":"value","required":true,"schema":{"type":"integer","minimum":0}}],"result":{"schema":{"type":"string"}}});
|
||||
let expected = json!({"methods":[],"x-sausage-endpoints":{"control":{"methods":[method]}}});
|
||||
for case in ["missing", "params", "result", "unsupported"] {
|
||||
let world = TestWorld::new();
|
||||
let mut next = expected.clone();
|
||||
match case {
|
||||
"missing" => next["x-sausage-endpoints"]["control"]["methods"] = json!([]),
|
||||
"params" => {
|
||||
next["x-sausage-endpoints"]["control"]["methods"][0]["params"][0]["schema"]["minimum"] =
|
||||
json!(1)
|
||||
}
|
||||
"result" => {
|
||||
next["x-sausage-endpoints"]["control"]["methods"][0]["result"]["schema"] =
|
||||
json!({"type":"integer"})
|
||||
}
|
||||
_ => {
|
||||
next["x-sausage-endpoints"]["control"]["methods"][0]["result"]["schema"]["pattern"] =
|
||||
json!("[a-z]+")
|
||||
}
|
||||
}
|
||||
world.plugin("notes", expected.clone(), next);
|
||||
world.plugin(
|
||||
"agent",
|
||||
consumer("notes", expected.clone()),
|
||||
document("consume"),
|
||||
);
|
||||
let host = world.start().await;
|
||||
let error = activate(&host, "notes").await.unwrap_err();
|
||||
assert_eq!(error["code"], -32003, "{case}: {error}");
|
||||
assert!(!error["data"].as_array().unwrap().is_empty());
|
||||
if ["params", "result"].contains(&case) {
|
||||
assert!(
|
||||
error["data"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|p| !p["counterexample"].is_null()),
|
||||
"{error}"
|
||||
);
|
||||
}
|
||||
if case == "unsupported" {
|
||||
assert!(error["data"].to_string().contains("unsupported"), "{error}");
|
||||
}
|
||||
assert_eq!(
|
||||
std::fs::read(world.0.join("plugins/notes/plugin.wasm")).unwrap(),
|
||||
std::fs::read(fixture(1)).unwrap()
|
||||
);
|
||||
host.stop().await;
|
||||
}
|
||||
}
|
||||
|
||||
/// PROTOCOL 10.2: providers can add methods, optional params, and result fields without breaking consumers.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn additive_contract_changes_activate() {
|
||||
let old = json!({"methods":[{"name":"read","params":[],"result":{"schema":{"type":"object","properties":{"text":{"type":"string"}},"required":["text"]}}}]});
|
||||
let mut next = old.clone();
|
||||
next["methods"][0]["params"] =
|
||||
json!([{"name":"limit","required":false,"schema":{"type":"integer"}}]);
|
||||
next["methods"][0]["result"]["schema"]["properties"]["extra"] = json!({"type":"boolean"});
|
||||
next["methods"]
|
||||
.as_array_mut()
|
||||
.unwrap()
|
||||
.push(json!({"name":"new","params":[],"result":{"schema":{"type":"null"}}}));
|
||||
let world = TestWorld::new();
|
||||
world.plugin("notes", old.clone(), next);
|
||||
world.plugin("agent", consumer("notes", old), document("consume"));
|
||||
let host = world.start().await;
|
||||
activate(&host, "notes").await.unwrap();
|
||||
host.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 3.7: an old consumer can still send its old import during grace, even after the provider removes it.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn old_consumer_can_call_removed_provider_method() {
|
||||
let world = TestWorld::new();
|
||||
world.plugin("notes", document("legacy"), document("new"));
|
||||
let mut old = consumer("notes", document("legacy"));
|
||||
old["x-sausage-grace"] = json!(60);
|
||||
world.plugin("agent", old, document("consume"));
|
||||
let host = world.start().await;
|
||||
let pid = call(&host, "agent.deferred", json!({})).await;
|
||||
activate(&host, "agent").await.unwrap();
|
||||
activate(&host, "notes").await.unwrap();
|
||||
assert_eq!(host.processes()[pid.as_str().unwrap()].state, "running");
|
||||
std::fs::write(world.0.join("state/agent/release-call"), "release").unwrap();
|
||||
let response: Value = tokio::time::timeout(Duration::from_secs(10), async {
|
||||
loop {
|
||||
if let Ok(bytes) = std::fs::read(world.0.join("state/agent/call-result"))
|
||||
&& let Ok(value) = serde_json::from_slice(&bytes)
|
||||
{
|
||||
break value;
|
||||
}
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response["error"]["code"], -32601);
|
||||
assert_eq!(response["error"]["data"]["version"], "2");
|
||||
host.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 10.2: activation and revert check interfaces but leave persistent state unchanged.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn state_is_outside_interface_checks() {
|
||||
let world = TestWorld::new();
|
||||
world.plugin("notes", document("add"), document("add"));
|
||||
let state = world.0.join("state/notes/user-state");
|
||||
let contents = b"a state format neither version understands\0";
|
||||
std::fs::write(&state, contents).unwrap();
|
||||
let host = world.start().await;
|
||||
activate(&host, "notes").await.unwrap();
|
||||
assert_eq!(std::fs::read(&state).unwrap(), contents);
|
||||
call(&host, "core.revert", json!({"plugin":"notes"})).await;
|
||||
assert_eq!(std::fs::read(state).unwrap(), contents);
|
||||
host.stop().await;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user