core: review fixes, init builds the agent, console without a terminal

Activation path (review T6R): a broken plugin or a stray file in
/plugins blocks no activation; candidates replace their own entries;
discovery has a 10 s deadline and its process is stopped after the
answer; an unchanged hash keeps plugin.prev.wasm; activations run in
their own task; the watcher is not recursive; no duplicate services;
rpc.retired waits for live processes of its version; reads and hashes
on blocking threads.

Guest I/O and per-call paths: a stdin write error is EOF, stdout and
stderr are drained before the status; stderr is lossy UTF-8 and goes
to the journal from its own reader; compiled components are freed;
shutdown starts no new processes; lock queues drop stopping processes;
compile before the admission gate; CommandPre per component;
core.unlock in order; null errors are successes; the journal thread
survives SQLite errors.

sausage init builds the agent with cargo.build; sausage without a
command creates ./world. The console reads plain lines without a
terminal. PROTOCOL.md and DECISIONS.md describe the decided rules. The
flake tests the whole workspace and checks the packaged cargo.build.
This commit is contained in:
2026-10-23 02:25:03 +01:00
parent 53b90a8ecf
commit d7b78ed0fd
27 changed files with 1800 additions and 352 deletions
+52 -13
View File
@@ -44,7 +44,10 @@ impl TestWorld {
}
std::fs::create_dir_all(world.state_dir("probe")).unwrap();
std::fs::write(world.state_dir("probe").join("discover.json"),json!({"methods":[{"name":"echo","description":"Echo supplied text.","params":[{"name":"text","description":"The input text.","schema":{"type":"string"}}],"result":{"schema":{"type":"string"}}}]}).to_string()).unwrap();
std::fs::copy(fixture(), root.join("candidate.wasm")).unwrap();
let mut candidate = std::fs::read(fixture()).unwrap();
// A custom section creates a new version without changing probe behavior.
candidate.extend_from_slice(&[0, 2, 1, b'c']);
std::fs::write(root.join("candidate.wasm"), candidate).unwrap();
Self(root)
}
async fn start(&self) -> Host {
@@ -152,7 +155,7 @@ async fn ps() {
assert!(text.contains("probe serve console"));
host.stop().await;
}
/// Log supports plugin filters, process filters, and a row count.
/// Log formats local times and short pids, while core.log preserves JSON and filters.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn log() {
let world = TestWorld::new();
@@ -160,22 +163,58 @@ 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();
let filtered: Value =
serde_json::from_str(&command(&host, "log data-logger -n 1").await).unwrap();
assert_eq!(filtered[0]["plugin"], "data-logger");
let pid = host.call("probe.serve", json!({})).await.unwrap();
for line in [
"log".to_owned(),
"log probe -n 1".to_owned(),
format!("log -n 1 {}", pid.as_str().unwrap()),
for (line, filters) in [
("log".to_owned(), json!({})),
(
"log data-logger -n 1".to_owned(),
json!({"plugin":"data-logger","limit":1}),
),
(
"log probe -n 1".to_owned(),
json!({"plugin":"probe","limit":1}),
),
(
format!("log -n 1 {}", pid.as_str().unwrap()),
json!({"pid":pid,"limit":1}),
),
] {
let entries: Value = serde_json::from_str(&command(&host, &line).await).unwrap();
assert!(!entries.as_array().unwrap().is_empty());
let raw = command(&host, &format!("core.log {filters}")).await;
let entries: Value = serde_json::from_str(&raw).unwrap();
let entries = entries.as_array().unwrap();
assert!(!entries.is_empty());
let printed = command(&host, &line).await;
let lines: Vec<_> = printed.lines().collect();
assert_eq!(lines.len(), entries.len(), "{printed}");
if line != "log" {
assert_eq!(entries.as_array().unwrap().len(), 1);
assert_eq!(entries[0]["plugin"], "probe");
assert_eq!(entries.len(), 1);
}
for (line, entry) in lines.into_iter().zip(entries) {
let time = chrono::DateTime::parse_from_rfc3339(entry["time"].as_str().unwrap())
.unwrap()
.with_timezone(&chrono::Local)
.format("%H:%M:%S%.3f")
.to_string();
let plugin = entry["plugin"].as_str().unwrap();
let pid = entry["pid"].as_str().unwrap();
let prefix = format!("{plugin}-");
let short = pid
.strip_prefix(&prefix)
.map(|uuid| format!("{prefix}{}", &uuid[..8]))
.unwrap_or_else(|| pid.to_owned());
assert_eq!(
line,
format!(
"{} {} {} {}",
time,
short,
entry["stream"].as_str().unwrap(),
entry["line"].as_str().unwrap()
)
);
}
}
assert_eq!(command(&host, "log absent-plugin").await, "");
assert!(execute_line(&host, "log -n").await.is_err());
host.stop().await;
}