Trim the harness to what it needs to keep going

Host:
- test first: a new version is tested where it lies; only an installed
  version goes to versions/, and instances run from there
- data/ and public/ are shared by all versions; no snapshots; rollback
  restores code only
- no re-delivery, status.json, list, remove/archive, smoke test, data/
  hiding, or stdin timeout; rollback needs a version
- supervisor.rs split out; one place for installing a version
- init only writes the agent folder; run installs it like any actor
- console: @actor, text to the agent, /help, /quit

Agent:
- every text goes out as a broadcast; 5-line prompt; 5 tools that describe
  themselves; turn.json only marks an active turn

Docs: PROTOCOL.md, README.md, DECISIONS.md, AGENTS.md rewritten.
Examples: only notes and notes-v2.
This commit is contained in:
2026-10-15 13:35:45 +01:00
parent cee862275c
commit 11144b947e
35 changed files with 909 additions and 2038 deletions
+75 -210
View File
@@ -1,6 +1,6 @@
mod common;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use std::time::Duration;
use common::*;
use sausage::fs::TMP_PREFIX;
@@ -48,13 +48,6 @@ async fn receive_shutdown_messages(
.expect("shutdown output or lifecycle reply did not arrive");
}
fn epoch_second() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
}
#[tokio::test]
async fn routes_replies_publishes_and_reports_undeliverable() {
let world = create_world("routing");
@@ -112,6 +105,29 @@ async fn routes_replies_publishes_and_reports_undeliverable() {
host.stop().await;
}
#[tokio::test]
async fn broker_does_not_answer_messages_with_reply_to() {
let world = create_world("broker-reply-to");
let (host, mut mailbox) = start(&world).await;
let scanned = request(&host, &mut mailbox, json!({"type": "scan"})).await;
assert_eq!(scanned.body["type"], "scanned");
host.broker.send(
"console",
Some("broker"),
None,
Some("upstream-message"),
json!({"type": "scan"}),
);
assert!(
tokio::time::timeout(Duration::from_secs(1), mailbox.recv())
.await
.is_err(),
"broker replied to a message that already had reply_to"
);
host.stop().await;
}
#[tokio::test]
async fn filesystem_permissions_and_public_sharing() {
let (_, host, mut mailbox) = filesystem_world("filesystem-permissions").await;
@@ -148,6 +164,47 @@ async fn filesystem_permissions_and_public_sharing() {
host.stop().await;
}
#[tokio::test]
async fn actors_can_read_but_not_write_other_actor_data() {
let (_, host, mut mailbox) = filesystem_world("actor-data-sharing").await;
ask_type(
&host,
&mut mailbox,
"writer",
json!({"type": "write", "path": "/data/x", "text": "shared"}),
"ok",
)
.await;
let shared = ask_type(
&host,
&mut mailbox,
"reader",
json!({"type": "read", "path": "/actor/writer/data/x"}),
"content",
)
.await;
assert_eq!(shared.body["text"], "shared");
ask_type(
&host,
&mut mailbox,
"reader",
json!({"type": "write", "path": "/actor/writer/data/x", "text": "changed"}),
"error",
)
.await;
let unchanged = ask_type(
&host,
&mut mailbox,
"writer",
json!({"type": "read", "path": "/data/x"}),
"content",
)
.await;
assert_eq!(unchanged.body["text"], "shared");
host.stop().await;
}
#[tokio::test]
async fn filesystem_hides_new_and_truncated_writes_until_close() {
let (_, host, mut mailbox) = filesystem_world("filesystem-commit").await;
@@ -274,42 +331,6 @@ async fn filesystem_append_is_visible_before_close() {
host.stop().await;
}
#[tokio::test]
async fn filesystem_crash_discards_an_open_truncated_write() {
let (world, host, mut mailbox) = filesystem_world("filesystem-crash").await;
ask(
&host,
&mut mailbox,
"writer",
json!({"type": "write", "path": "/public/crash.txt", "text": "old"}),
)
.await;
ask(
&host,
&mut mailbox,
"writer",
json!({"type": "open", "path": "/public/crash.txt", "text": "uncommitted"}),
)
.await;
let crash_id = host.broker.send(
"console",
Some("writer"),
None,
None,
json!({"type": "crash"}),
);
wait_for_parked(&world, "writer", &crash_id).await;
let after_crash = ask(
&host,
&mut mailbox,
"reader",
json!({"type": "read", "path": "/actor/writer/public/crash.txt"}),
)
.await;
assert_eq!(after_crash.body["text"], "old");
host.stop().await;
}
#[tokio::test]
async fn filesystem_rejects_paths_that_escape_data() {
let (_, host, mut mailbox) = filesystem_world("filesystem-path-escape").await;
@@ -351,6 +372,8 @@ async fn upgrades_test_candidates_keep_old_versions_and_queue_messages_in_order(
assert_eq!(failed.body["type"], "error");
let old_version = ask(&host, &mut mailbox, "queue", json!({"type": "version"})).await;
assert_eq!(old_version.body["version"], "1");
// A version that fails its tests never enters versions/.
assert!(!world.actor_dir("queue").join("versions/2").exists());
let upgrade_id = host.broker.send(
"console",
@@ -408,72 +431,14 @@ async fn upgrades_test_candidates_keep_old_versions_and_queue_messages_in_order(
assert_eq!(envelope.from, "queue");
assert_eq!(envelope.body["type"], "ordered");
assert_eq!(envelope.body["n"], json!(index + 1));
assert_eq!(envelope.body["version"], "3");
assert_eq!(envelope.body["version"], "2");
}
host.stop().await;
}
#[tokio::test]
async fn redelivers_a_crashed_message_after_the_next_upgrade() {
let world = create_world("redelivery");
let wasm = toolbox_wasm().clone();
let (host, mut mailbox) = start(&world).await;
let source = passing_source(&world, "source");
assert_eq!(
spawn_actor(&host, &mut mailbox, "parked", &wasm, &source)
.await
.body["type"],
"spawned"
);
let crash_id = host.broker.send(
"console",
Some("parked"),
None,
None,
json!({"type": "crash"}),
);
wait_for_parked(&world, "parked", &crash_id).await;
assert!(matches!(
mailbox.try_recv(),
Err(tokio::sync::mpsc::error::TryRecvError::Empty)
));
let upgrade_id = host.broker.send(
"console",
Some("broker"),
None,
None,
json!({"type": "upgrade", "actor": "parked", "wasm": wasm.display().to_string(), "source": source.display().to_string()}),
);
let mut upgraded = None;
let mut replayed = None;
tokio::time::timeout(Duration::from_secs(15), async {
while upgraded.is_none() || replayed.is_none() {
let envelope = mailbox.recv().await.expect("console mailbox closed");
if envelope.reply_to.as_deref() == Some(upgrade_id.as_str()) {
upgraded = Some(envelope);
} else if envelope.reply_to.as_deref() == Some(crash_id.as_str()) {
replayed = Some(envelope);
} else {
panic!("unexpected message during parked delivery: {envelope:?}");
}
}
})
.await
.expect("upgrade did not redeliver the parked message");
assert_eq!(upgraded.unwrap().body["type"], "upgraded");
assert_eq!(
replayed.unwrap().body,
json!({"type": "replayed", "version": "2"})
);
host.stop().await;
}
#[tokio::test]
async fn rolls_back_a_crash_loop_and_restores_its_data_snapshot() {
async fn rolls_back_a_crash_loop_to_the_previous_version_without_restoring_data() {
let world = create_world("crash-loop");
let wasm = toolbox_wasm().clone();
let (host, mut mailbox) = start(&world).await;
@@ -505,7 +470,7 @@ async fn rolls_back_a_crash_loop_and_restores_its_data_snapshot() {
);
let mut upgraded: Option<Value> = None;
let mut rolled_back: Option<Value> = None;
tokio::time::timeout(Duration::from_secs(20), async {
tokio::time::timeout(Duration::from_secs(35), async {
while upgraded.is_none() || rolled_back.is_none() {
let envelope = mailbox.recv().await.expect("console mailbox closed");
let kind = envelope.body["type"].as_str().map(str::to_string);
@@ -530,20 +495,20 @@ async fn rolls_back_a_crash_loop_and_restores_its_data_snapshot() {
let version = ask(&host, &mut mailbox, "crashloop", json!({"type": "version"})).await;
assert_eq!(version.body["version"], "1");
let restored = ask(
let current = ask(
&host,
&mut mailbox,
"crashloop",
json!({"type": "read", "path": "/data/state"}),
)
.await;
assert_eq!(restored.body["text"], "stable");
assert_eq!(current.body["text"], "bad-v2");
host.stop().await;
}
#[tokio::test]
async fn explicit_rollback_restores_the_previous_version_and_snapshot() {
async fn explicit_rollback_keeps_current_data() {
let world = create_world("explicit-rollback");
let wasm = toolbox_wasm().clone();
let (host, mut mailbox) = start(&world).await;
@@ -581,17 +546,16 @@ async fn explicit_rollback_restores_the_previous_version_and_snapshot() {
assert_eq!(rolled_back.body["type"], "rolled_back");
assert_eq!(rolled_back.body["from"], 2);
assert_eq!(rolled_back.body["to"], 1);
assert_eq!(rolled_back.body["restored_state"], true);
let version = ask(&host, &mut mailbox, "recover", json!({"type": "version"})).await;
assert_eq!(version.body["version"], "1");
let restored = ask(
let current = ask(
&host,
&mut mailbox,
"recover",
json!({"type": "read", "path": "/data/state"}),
)
.await;
assert_eq!(restored.body["text"], "version-one");
assert_eq!(current.body["text"], "version-two");
host.stop().await;
}
@@ -721,37 +685,6 @@ async fn concurrent_spawns_of_one_id_leave_one_live_instance() {
host.stop().await;
}
#[tokio::test]
async fn rollback_preserves_target_and_git_files_in_data_and_public() {
let world = create_world("snapshot-special-directories");
let wasm = toolbox_wasm().clone();
let (host, mut mailbox) = start(&world).await;
let source = passing_source(&world, "source");
spawn_type(&host, &mut mailbox, "snapshot", &wasm, &source).await;
let files = [
("/data/target", "/data/target/x", "data-target"),
("/data/.git", "/data/.git/x", "data-git"),
("/public/target", "/public/target/x", "public-target"),
("/public/.git", "/public/.git/x", "public-git"),
];
for (dir, path, text) in files {
let mkdir = json!({"type": "mkdir", "path": dir});
ask_type(&host, &mut mailbox, "snapshot", mkdir, "ok").await;
let write = json!({"type": "write", "path": path, "text": text});
ask_type(&host, &mut mailbox, "snapshot", write, "ok").await;
}
let upgrade = json!({"type": "upgrade", "actor": "snapshot", "wasm": wasm.display().to_string(), "source": source.display().to_string()});
ask_type(&host, &mut mailbox, "broker", upgrade, "upgraded").await;
let rollback = json!({"type": "rollback", "actor": "snapshot", "version": 1});
ask_type(&host, &mut mailbox, "broker", rollback, "rolled_back").await;
for (_, path, text) in files {
let read = json!({"type": "read", "path": path});
let reply = ask_type(&host, &mut mailbox, "snapshot", read, "content").await;
assert_eq!(reply.body["text"], text);
}
host.stop().await;
}
#[tokio::test]
async fn stop_finishes_when_actor_stdin_is_full() {
let world = create_world("full-stdin-stop");
@@ -819,74 +752,6 @@ async fn shutdown_reply_and_publish_survive_stop_and_upgrade() {
host.stop().await;
}
#[tokio::test]
async fn rapid_remove_recreate_remove_keeps_actor_manageable() {
let world = create_world("rapid-remove");
let wasm = toolbox_wasm().clone();
let (host, mut mailbox) = start(&world).await;
let source = passing_source(&world, "source");
spawn_type(&host, &mut mailbox, "rapid", &wasm, &source).await;
let stop = json!({"type": "stop", "actor": "rapid"});
ask_type(&host, &mut mailbox, "broker", stop, "stopped").await;
while SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.subsec_millis()
>= 100
{
tokio::time::sleep(Duration::from_millis(10)).await;
}
let archive_second = epoch_second();
let remove = json!({"type": "remove", "actor": "rapid"});
let first = ask_type(&host, &mut mailbox, "broker", remove.clone(), "removed").await;
assert_eq!(
epoch_second(),
archive_second,
"first remove crossed an epoch-second boundary"
);
// Restore the stopped actor from its archive to avoid a candidate smoke test.
sausage::world::copy_tree(
first.body["archive"]
.as_str()
.map(std::path::Path::new)
.unwrap(),
&world.actor_dir("rapid"),
)
.unwrap();
let scan = json!({"type": "scan"});
ask_type(&host, &mut mailbox, "broker", scan.clone(), "scanned").await;
let second = ask_type(&host, &mut mailbox, "broker", remove.clone(), "removed").await;
assert_eq!(
epoch_second(),
archive_second,
"remove and recreate crossed an epoch-second boundary"
);
sausage::world::copy_tree(
second.body["archive"]
.as_str()
.map(std::path::Path::new)
.unwrap(),
&world.actor_dir("rapid"),
)
.unwrap();
ask_type(&host, &mut mailbox, "broker", scan, "scanned").await;
let start = json!({"type": "start", "actor": "rapid"});
ask_type(&host, &mut mailbox, "broker", start, "started").await;
let echo = ask_type(
&host,
&mut mailbox,
"rapid",
json!({"type": "echo", "value": "managed"}),
"echo",
)
.await;
assert_eq!(echo.body["value"], "managed");
host.stop().await;
}
#[tokio::test]
async fn close_commits_open_file_after_parent_directory_rename() {
let (_, host, mut mailbox) = filesystem_world("rename-open-file").await;