This commit is contained in:
@@ -47,6 +47,83 @@ async fn receive_shutdown_messages(
|
||||
.expect("shutdown output or lifecycle reply did not arrive");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn global_config_is_readable_owner_written_and_atomically_replaced() {
|
||||
let world = create_world("global-config");
|
||||
let (host, mut mailbox) = start(&world).await;
|
||||
let wasm = toolbox_wasm().clone();
|
||||
for actor in ["shared", "reader"] {
|
||||
let source = passing_source(&world, actor);
|
||||
spawn_type(&host, &mut mailbox, actor, &wasm, &source).await;
|
||||
}
|
||||
let initial = ask_type(&host, &mut mailbox, "reader", json!({"type": "read", "path": "/config"}), "content").await;
|
||||
assert_eq!(initial.body["text"], "{}\n");
|
||||
for path in ["/config", "/global/config", "/global/config.tmp", "/actor/shared/data/../../../../global/config"] {
|
||||
ask_type(&host, &mut mailbox, "reader", json!({"type": "write", "path": path, "text": "not allowed"}), "error").await;
|
||||
}
|
||||
ask_type(&host, &mut mailbox, "reader", json!({"type": "rename", "path": "/config", "to": "/global/stolen"}), "error").await;
|
||||
// The root view stays read-only even for the owner; publication uses its
|
||||
// writable directory alias, with a sibling temporary file and rename.
|
||||
ask_type(&host, &mut mailbox, "shared", json!({"type": "write", "path": "/config", "text": "not allowed"}), "error").await;
|
||||
let next = "{\"llm\":{\"thinking\":\"medium\"}}\n";
|
||||
ask_type(&host, &mut mailbox, "shared", json!({"type": "write", "path": "/global/config.tmp", "text": next}), "ok").await;
|
||||
let before = ask_type(&host, &mut mailbox, "reader", json!({"type": "read", "path": "/config"}), "content").await;
|
||||
assert_eq!(before.body["text"], "{}\n");
|
||||
// A native/world reader holding the old inode sees the complete old file.
|
||||
let mut old = std::fs::File::open(world.root.join("config")).unwrap();
|
||||
ask_type(&host, &mut mailbox, "shared", json!({"type": "rename", "path": "/global/config.tmp", "to": "/global/config"}), "ok").await;
|
||||
let after = ask_type(&host, &mut mailbox, "reader", json!({"type": "read", "path": "/config"}), "content").await;
|
||||
assert_eq!(after.body["text"], next);
|
||||
assert_eq!(std::fs::read_to_string(world.root.join("config")).unwrap(), next);
|
||||
assert_eq!(std::fs::read_to_string(world.resolve("reader", "/config").unwrap()).unwrap(), next);
|
||||
let mut previous = String::new();
|
||||
std::io::Read::read_to_string(&mut old, &mut previous).unwrap();
|
||||
assert_eq!(previous, "{}\n");
|
||||
assert!(!world.global_dir().join("config.tmp").exists());
|
||||
// The mount must not give shared writable access to any other world data.
|
||||
ask_type(&host, &mut mailbox, "shared", json!({"type": "write", "path": "/global/../actor/reader/data/escape", "text": "no"}), "error").await;
|
||||
host.stop().await;
|
||||
let (host, mut mailbox) = start(&world).await;
|
||||
let persisted = ask_type(&host, &mut mailbox, "reader", json!({"type": "read", "path": "/config"}), "content").await;
|
||||
assert_eq!(persisted.body["text"], next);
|
||||
host.stop().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn global_config_candidates_cannot_change_the_live_file() {
|
||||
let world = create_world("candidate-global-config");
|
||||
let (host, mut mailbox) = start(&world).await;
|
||||
let source = source_with_test(&world, "shared", json!({"steps": [
|
||||
{"send": {"type": "write", "path": "/global/next", "text": "{\"test\":true}"}, "expect": {"type": "ok"}},
|
||||
{"send": {"type": "rename", "path": "/global/next", "to": "/global/config"}, "expect": {"type": "ok"}},
|
||||
{"send": {"type": "read", "path": "/config"}, "expect": {"type": "content", "text": "{\"test\":true}"}}
|
||||
]}));
|
||||
spawn_type(&host, &mut mailbox, "shared", toolbox_wasm(), &source).await;
|
||||
assert_eq!(std::fs::read_to_string(world.config_path()).unwrap(), "{}\n");
|
||||
host.stop().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn global_config_is_added_to_existing_worlds_without_copying_actor_settings() {
|
||||
let world = create_world("old-world-config");
|
||||
std::fs::remove_file(world.root.join("config")).unwrap();
|
||||
std::fs::remove_dir_all(world.global_dir()).unwrap();
|
||||
world.ensure_actor_dirs("shared").unwrap();
|
||||
// Synthetic marker only; no actual credentials are read in this test.
|
||||
std::fs::write(world.data_dir("shared").join("config.json"), "legacy-private-marker").unwrap();
|
||||
let (host, _) = start(&world).await;
|
||||
assert_eq!(std::fs::read_to_string(world.root.join("config")).unwrap(), "{}\n");
|
||||
assert_eq!(std::fs::read_to_string(world.data_dir("shared").join("config.json")).unwrap(), "legacy-private-marker");
|
||||
host.stop().await;
|
||||
// Preserve an operator-provided global config when adding the mounts.
|
||||
std::fs::remove_file(world.root.join("config")).unwrap();
|
||||
std::fs::remove_dir_all(world.global_dir()).unwrap();
|
||||
std::fs::write(world.root.join("config"), "{\"operator\":true}").unwrap();
|
||||
let (host, _) = start(&world).await;
|
||||
assert_eq!(std::fs::read_to_string(world.root.join("config")).unwrap(), "{\"operator\":true}");
|
||||
host.stop().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn routes_replies_publishes_and_reports_undeliverable() {
|
||||
let world = create_world("routing");
|
||||
|
||||
Reference in New Issue
Block a user