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:
@@ -146,8 +146,6 @@ fn main() {
|
||||
}
|
||||
Err(error) => io_error(error),
|
||||
},
|
||||
Some("crash") if version == "1" => panic!("asked to crash"),
|
||||
Some("crash") => json!({"type": "replayed", "version": version}),
|
||||
_ => json!({"type": "error", "error": "unknown message"}),
|
||||
};
|
||||
reply(&mut out, &message, response);
|
||||
|
||||
+23
-115
@@ -9,7 +9,7 @@ use std::process::Command;
|
||||
use std::sync::OnceLock;
|
||||
use std::time::Duration;
|
||||
|
||||
use common::{create_world, receive_replies, receive_reply, request, source_with_test, start};
|
||||
use common::{create_world, receive_message, receive_reply, request, source_with_test, start};
|
||||
use sausage::{Envelope, Host, World};
|
||||
use serde_json::{Value, json};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
@@ -319,6 +319,13 @@ async fn start_agent(
|
||||
spawned.body["type"], "spawned",
|
||||
"agent spawn failed: {spawned:?}"
|
||||
);
|
||||
let subscribed = request(
|
||||
&host,
|
||||
&mut mailbox,
|
||||
json!({"type": "subscribe", "actor": "agent"}),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(subscribed.body["type"], "subscribed");
|
||||
(world, host, mailbox)
|
||||
}
|
||||
|
||||
@@ -332,11 +339,12 @@ fn send_text(host: &Host, text: &str) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
async fn assert_answer(mailbox: &mut mpsc::Receiver<Envelope>, id: &str, text: &str) {
|
||||
let reply = receive_reply(mailbox, id, Duration::from_secs(20)).await;
|
||||
assert_eq!(reply.to, "console");
|
||||
assert_eq!(reply.reply_to.as_deref(), Some(id));
|
||||
assert_eq!(reply.body, json!({"type": "text", "text": text}));
|
||||
async fn assert_published_text(mailbox: &mut mpsc::Receiver<Envelope>, text: &str) {
|
||||
let message = receive_message(mailbox, Duration::from_secs(20)).await;
|
||||
assert_eq!(message.from, "agent");
|
||||
assert_eq!(message.to, "console");
|
||||
assert_eq!(message.reply_to, None);
|
||||
assert_eq!(message.body, json!({"type": "text", "text": text}));
|
||||
}
|
||||
|
||||
fn history(world: &World) -> Vec<Value> {
|
||||
@@ -439,9 +447,9 @@ async fn text_answers_use_both_inference_apis() {
|
||||
.await;
|
||||
let (_, host, mut mailbox) =
|
||||
start_agent(api, &fake, &format!("agent-text-{}", api.name())).await;
|
||||
let id = send_text(&host, "say hello");
|
||||
send_text(&host, "say hello");
|
||||
api.check_request(&fake.next_request().await);
|
||||
assert_answer(&mut mailbox, &id, "hello from the fake model").await;
|
||||
assert_published_text(&mut mailbox, "hello from the fake model").await;
|
||||
host.stop().await;
|
||||
}
|
||||
}
|
||||
@@ -455,7 +463,7 @@ async fn tool_calls_pair_with_results_in_both_inference_apis() {
|
||||
.await;
|
||||
let (world, host, mut mailbox) =
|
||||
start_agent(api, &fake, &format!("agent-tools-{}", api.name())).await;
|
||||
let id = send_text(&host, "list actors");
|
||||
send_text(&host, "list actors");
|
||||
api.check_request(&fake.next_request().await);
|
||||
fake.answer(api.text_response("the actor list is ready"))
|
||||
.await;
|
||||
@@ -470,8 +478,9 @@ async fn tool_calls_pair_with_results_in_both_inference_apis() {
|
||||
.iter()
|
||||
.any(|entry| entry["name"] == "agent")
|
||||
);
|
||||
assert_answer(&mut mailbox, &id, "the actor list is ready").await;
|
||||
assert_published_text(&mut mailbox, "the actor list is ready").await;
|
||||
|
||||
host.stop().await;
|
||||
let history = history(&world);
|
||||
assert_eq!(
|
||||
history
|
||||
@@ -482,7 +491,6 @@ async fn tool_calls_pair_with_results_in_both_inference_apis() {
|
||||
);
|
||||
assert_eq!(history[1]["id"], "list-1");
|
||||
assert_eq!(history[2]["id"], "list-1");
|
||||
host.stop().await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -513,8 +521,8 @@ async fn restart_resumes_a_turn_with_completed_tool_history() {
|
||||
serde_json::from_slice::<Value>(
|
||||
&std::fs::read(world.data_dir("agent").join("turn.json")).unwrap()
|
||||
)
|
||||
.unwrap()["active"]["id"],
|
||||
id
|
||||
.unwrap(),
|
||||
json!(true)
|
||||
);
|
||||
let _ = release.send(());
|
||||
fake.answer(api.text_response("resumed answer")).await;
|
||||
@@ -528,22 +536,9 @@ async fn restart_resumes_a_turn_with_completed_tool_history() {
|
||||
let resumed = fake.next_request().await;
|
||||
api.check_request(&resumed);
|
||||
assert_tool_pair(&resumed, api, "list-1", "list_dir", &args);
|
||||
let replies = receive_replies(
|
||||
&mut mailbox,
|
||||
&[start_id.clone(), id.clone()],
|
||||
Duration::from_secs(20),
|
||||
)
|
||||
.await;
|
||||
let started = replies
|
||||
.iter()
|
||||
.find(|reply| reply.reply_to.as_deref() == Some(start_id.as_str()))
|
||||
.unwrap();
|
||||
let started = receive_reply(&mut mailbox, &start_id, Duration::from_secs(20)).await;
|
||||
assert_eq!(started.body["type"], "started");
|
||||
let answer = replies
|
||||
.iter()
|
||||
.find(|reply| reply.reply_to.as_deref() == Some(id.as_str()))
|
||||
.unwrap();
|
||||
assert_eq!(answer.body["text"], "resumed answer");
|
||||
assert_published_text(&mut mailbox, "resumed answer").await;
|
||||
|
||||
let history = history(&world);
|
||||
assert_history_tools_have_results(&history);
|
||||
@@ -558,90 +553,3 @@ async fn restart_resumes_a_turn_with_completed_tool_history() {
|
||||
host.stop().await;
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn queued_text_resumes_after_the_active_turn_across_restart() {
|
||||
let wait_args = json!({"seconds": 10});
|
||||
for api in [Api::Anthropic] {
|
||||
let mut fake = FakeServer::start().await;
|
||||
fake.answer(api.tool_response("wait-1", "wait_for_messages", &wait_args))
|
||||
.await;
|
||||
let (world, host, mut mailbox) =
|
||||
start_agent(api, &fake, &format!("agent-queue-{}", api.name())).await;
|
||||
let first_id = send_text(&host, "first turn");
|
||||
api.check_request(&fake.next_request().await);
|
||||
let release = fake.hold(api.text_response("discarded response")).await;
|
||||
let second_id = send_text(&host, "second turn");
|
||||
let second_request = fake.next_request().await;
|
||||
api.check_request(&second_request);
|
||||
let output = assert_tool_pair(
|
||||
&second_request,
|
||||
api,
|
||||
"wait-1",
|
||||
"wait_for_messages",
|
||||
&wait_args,
|
||||
);
|
||||
let queued: Value = serde_json::from_str(&output).unwrap();
|
||||
assert!(
|
||||
queued["messages"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|message| message["message"]["id"] == second_id)
|
||||
);
|
||||
let turn: Value = serde_json::from_slice(
|
||||
&std::fs::read(world.data_dir("agent").join("turn.json")).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(turn["queue"][0]["id"], second_id);
|
||||
|
||||
let stopped = request(
|
||||
&host,
|
||||
&mut mailbox,
|
||||
json!({"type": "stop", "actor": "agent"}),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(stopped.body["type"], "stopped");
|
||||
let _ = release.send(());
|
||||
fake.answer(api.text_response("first answer")).await;
|
||||
fake.answer(api.text_response("second answer")).await;
|
||||
let start_id = host.broker.send(
|
||||
"console",
|
||||
Some("broker"),
|
||||
None,
|
||||
None,
|
||||
json!({"type": "start", "actor": "agent"}),
|
||||
);
|
||||
let resumed_first = fake.next_request().await;
|
||||
api.check_request(&resumed_first);
|
||||
assert!(resumed_first.body.to_string().contains("first turn"));
|
||||
let next_turn = fake.next_request().await;
|
||||
api.check_request(&next_turn);
|
||||
assert!(next_turn.body.to_string().contains("second turn"));
|
||||
|
||||
let replies = receive_replies(
|
||||
&mut mailbox,
|
||||
&[start_id.clone(), first_id.clone(), second_id.clone()],
|
||||
Duration::from_secs(20),
|
||||
)
|
||||
.await;
|
||||
let started = replies
|
||||
.iter()
|
||||
.find(|reply| reply.reply_to.as_deref() == Some(start_id.as_str()))
|
||||
.unwrap();
|
||||
assert_eq!(started.body["type"], "started");
|
||||
let first_position = replies
|
||||
.iter()
|
||||
.position(|reply| reply.reply_to.as_deref() == Some(first_id.as_str()))
|
||||
.unwrap();
|
||||
let second_position = replies
|
||||
.iter()
|
||||
.position(|reply| reply.reply_to.as_deref() == Some(second_id.as_str()))
|
||||
.unwrap();
|
||||
assert!(first_position < second_position);
|
||||
assert_eq!(replies[first_position].body["text"], "first answer");
|
||||
assert_eq!(replies[second_position].body["text"], "second answer");
|
||||
assert_history_tools_have_results(&history(&world));
|
||||
host.stop().await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,26 +246,3 @@ pub async fn upgrade_actor(
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn wait_for_parked(world: &World, actor: &str, message_id: &str) {
|
||||
tokio::time::timeout(Duration::from_secs(5), async {
|
||||
loop {
|
||||
let status_path = world.actor_dir(actor).join("status.json");
|
||||
let parked_id = std::fs::read_to_string(status_path)
|
||||
.ok()
|
||||
.and_then(|text| serde_json::from_str::<Value>(&text).ok())
|
||||
.and_then(|status| {
|
||||
status
|
||||
.pointer("/parked_message/id")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string)
|
||||
});
|
||||
if parked_id.as_deref() == Some(message_id) {
|
||||
return;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("actor did not park the crashed message");
|
||||
}
|
||||
|
||||
+75
-210
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user