From e73ef5c8f86eec6d38418720e148ac0f8abbdd02 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 5 Sep 2026 17:32:00 +0200 Subject: [PATCH] Prove the seed lane, and make an unadvertised operation impossible to ship MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eighteen checks. The ones that matter most: an operation the panel emits but `operation_schema` does not name now fails a test rather than dying silently at validation — that test fails on the parent commit because of `world.advance_time`, which is why the schema gap was cut first. A v1 create payload is refused at validation and a v2 payload missing its scale target is a payload error; neither defaults, and neither creates a world. The lane: one session commits one Draft patch whose subjects have controllers, grants, positions, and goals, and a port built on anyone else's evidence is `Unauthorized` — the owner is the author, and that is the reducer's rule and not a convention. The deficit falls 6, 4, 2, 0 across a sweep and the fourth step spends no inference; a commit that does not lower its row returns `NoProgress` and stops rather than spinning. An Active world refuses the runner before inference and refuses the port afterwards with `AnswerRequired`. A citation the Vault did not return is dropped by `filter_evidence` and the canonical fact naming it is refused; the same fact citing a retrieved reference commits. A rejection reopens one derived command id with the kernel's complete mismatch set and a fresh runner resumes it. A v9 row is refused at open beside the pinned v8 and v7 refusals, and cross-lane writes conflict. End to end: a two-root world is created, seeded to zero deficit, approved, activated, elaborated against the boundaries the seed manufactured, ticked, and replayed from its journal to the same state digest. The Vault reader is tested with no kernel at all: references are vault-relative paths, all three wikilink forms resolve to one note, an ambiguous stem takes the shortest path, and `..`, an absolute scope, and a symlink out are refused at open. The ignored live smoke now seeds from a real Vault before it ticks. Its log is still the deliverable, and it gains the per-round deficit and the subjects that qualified. Co-Authored-By: Claude Opus 5 --- crates/ghostlight-dungeon/src/app_session.rs | 9 + crates/ghostlight-dungeon/src/runtime.rs | 452 ++++++- .../src/world/controllers.rs | 1152 ++++++++++++++++- 3 files changed, 1573 insertions(+), 40 deletions(-) diff --git a/crates/ghostlight-dungeon/src/app_session.rs b/crates/ghostlight-dungeon/src/app_session.rs index 7301712..71f5bc6 100644 --- a/crates/ghostlight-dungeon/src/app_session.rs +++ b/crates/ghostlight-dungeon/src/app_session.rs @@ -64,6 +64,15 @@ impl VerifiedPrincipalEvidence { pub(crate) fn account_subject_hash(&self) -> &str { &self.account_subject_hash } + + /// Test-only. Production keeps exactly one minter — a live cookie resolved + /// by `account_for_cookie` — and this constructor is compiled out of it. + #[cfg(test)] + pub(crate) fn fixture(account_subject_hash: impl Into) -> Self { + Self { + account_subject_hash: account_subject_hash.into(), + } + } } pub(crate) struct AppSessionOwner { diff --git a/crates/ghostlight-dungeon/src/runtime.rs b/crates/ghostlight-dungeon/src/runtime.rs index e7b84b9..1a46a43 100644 --- a/crates/ghostlight-dungeon/src/runtime.rs +++ b/crates/ghostlight-dungeon/src/runtime.rs @@ -3338,15 +3338,18 @@ mod tests { ); } - /// The local live smoke: the production tick driver, elaboration sweep, - /// and clock against a real CodexConnector, on a genesis world, for a - /// configured number of ticks, writing what the operator would see to a - /// log. Everything else in this module proves the machine under fixture - /// ports; this is the one place the road is tested. It asserts only that - /// the loop ran; the log is the deliverable. + /// Spec test 18. The local live smoke: a world created with a real scale + /// intent, seeded from a real Vault until its deficit is zero or the + /// session budget is spent, then approved, activated, and ticked by the + /// production tick driver, elaboration sweep, and clock against a real + /// CodexConnector. Everything else in this module proves the machine under + /// fixture ports; this is the one place the road is tested. It asserts only + /// that the loop ran; the log is the deliverable — patches committed, + /// deficit per round, subjects qualified, and the prose a tick produces + /// from a world that now has people in it. #[tokio::test] - #[ignore = "requires a running CodexConnector; see GHOSTLIGHT_SMOKE_* environment"] - async fn live_smoke_ticks_a_genesis_world_against_the_connector() { + #[ignore = "requires a running CodexConnector and a vault; see GHOSTLIGHT_SMOKE_* environment"] + async fn live_smoke_seeds_then_ticks_a_world_against_the_connector() { let env = |name: &str| std::env::var(name).unwrap_or_else(|_| panic!("{name} is required")); let live = LiveController { endpoint: env("GHOSTLIGHT_CONTROLLER_CONNECTOR").parse().unwrap(), @@ -3369,12 +3372,115 @@ mod tests { log.flush().unwrap(); }; + let seed_sessions: usize = env("GHOSTLIGHT_SMOKE_SEED_SESSIONS").parse().unwrap(); + let seed_target: u32 = env("GHOSTLIGHT_SMOKE_SEED_TARGET").parse().unwrap(); + let vault_scope = std::env::var("GHOSTLIGHT_SMOKE_VAULT_SCOPE").unwrap_or_default(); + let brief = std::env::var("GHOSTLIGHT_SMOKE_SEED_BRIEF").ok(); + // Read here rather than only inside `seed_once`, so a misconfigured run + // fails at the top instead of after the first paid session. + let _ = env(SEED_VAULT_ROOT_ENVIRONMENT); + let fixture = fixture_with(Some(live)).await; let state = &fixture.state; - active_two_cell_world(state, &fixture.cookie).await; - let snapshot = state.world.snapshot().await.unwrap(); + two_cell_world( + state, + &fixture.cookie, + BTreeMap::from([(SubjectKind::Person, seed_target)]), + vec![CreateJurisdictionIntent { + handle: "seed_root".into(), + label: env("GHOSTLIGHT_SMOKE_SEED_ROOT_LABEL"), + permille: 1000, + }], + false, + ) + .await; + + let principal = state + .sessions + .lock() + .await + .account_for_cookie(&fixture.cookie, Utc::now()) + .unwrap() + .expect("the fixture cookie names a live session"); + let draft = state.world.snapshot().await.unwrap(); line(format!( - "genesis world={:?} revision={} phase={:?} subjects={} opportunities={} now={:?} boundaries={} deficit_rows={}", + "draft world={:?} revision={} deficit_rows={} shortfall={:?}", + draft.world_id, + draft.revision, + draft.scale_deficit.len(), + crate::world::select_row(&draft) + )); + for round in 1..=seed_sessions { + let before = state.world.snapshot().await.unwrap(); + if crate::world::select_row(&before).is_none() { + line(format!("seed round {round} skipped: no shortfall left")); + break; + } + let started = std::time::Instant::now(); + let outcome = seed_once( + state, + &principal, + &before, + SeedPayload { + vault_scope: vault_scope.clone(), + brief: brief.clone(), + }, + ) + .await; + let after = state.world.snapshot().await.unwrap(); + line(format!( + "seed round {round} took={:?} outcome={:?} revision {}->{} patches={} qualified={} rows={:?}", + started.elapsed(), + outcome.as_ref().map(|value| value.name()), + before.revision, + after.revision, + after.revision.saturating_sub(1), + after + .subjects + .iter() + .filter(|subject| subject.qualified) + .count(), + after + .scale_deficit + .iter() + .map(|row| (row.kind, row.target, row.qualified, row.deficit)) + .collect::>() + )); + if !matches!(outcome, Ok(SeedOutcome::Committed)) { + break; + } + } + for subject in &state.world.snapshot().await.unwrap().subjects { + line(format!( + " seeded {} kind={:?} mode={:?} grants={} qualified={}", + subject.label, + subject.kind, + subject.controller_mode, + subject.affordances.len(), + subject.qualified + )); + } + + for body in [CommandBody::ApproveDraft, CommandBody::ActivateWorld] { + let snapshot = state.world.snapshot().await.unwrap(); + state + .world + .submit_principal( + PrincipalCommandIntent { + id: CommandId::new(), + world_id: snapshot.world_id, + expected_revision: snapshot.revision, + body, + }, + &principal, + ) + .await + .unwrap(); + } + let snapshot = state.world.snapshot().await.unwrap(); + assert_eq!(snapshot.phase, WorldPhase::Active); + line(format!( + "activated world={:?} revision={} phase={:?} subjects={} opportunities={} now={:?} boundaries={} deficit_rows={}", snapshot.world_id, snapshot.revision, snapshot.phase, @@ -3445,4 +3551,328 @@ mod tests { "the clock alone must move the revision" ); } + + // ---- The seed command ------------------------------------------------ + + /// Spec test 1. `world_create.v1` is not kept alive beside v2: an + /// invocation announcing it dies at validation before any handler runs, and + /// a payload that announces v2 but omits the scale target is a payload + /// error rather than a defaulted empty intent. Neither creates a world. + #[tokio::test] + async fn a_v1_create_payload_is_refused() { + let fixture = fixture().await; + let stale = post( + &fixture.state, + &fixture.cookie, + invocation( + "world.create", + "ghostlight.world_create.v1", + 0, + json!({"title":"Stale World","subject_label":"Operator"}), + &uuid::Uuid::new_v4().to_string(), + ), + ) + .await; + assert_eq!(stale["state"], "denied"); + assert!(current_world(&fixture.state).await.unwrap().is_none()); + + let partial = post( + &fixture.state, + &fixture.cookie, + invocation( + "world.create", + "ghostlight.world_create.v2", + 0, + json!({"title":"Half World","subject_label":"Operator"}), + &uuid::Uuid::new_v4().to_string(), + ), + ) + .await; + assert_eq!(partial["state"], "denied"); + assert!( + current_world(&fixture.state).await.unwrap().is_none(), + "a v2 payload with no scale target created a world anyway" + ); + } + + /// Every `control.button` in a surface, at whatever depth. + fn surface_buttons(node: &Value, into: &mut Vec) { + if node["kind"] == "control.button" + && let Some(command) = node["props"]["command"].as_str() + { + into.push(command.to_owned()); + } + for child in node["children"].as_array().into_iter().flatten() { + surface_buttons(child, into); + } + } + + /// Spec test 11. An operation can be emitted by the panel, handled by + /// `execute_world`, and still be dead because `operation_schema` does not + /// name it — which is exactly what `world.advance_time` was. Every button + /// the panel emits and every descriptor it advertises must resolve, and the + /// descriptor's schema must be the one validation will demand. + #[tokio::test] + async fn every_operation_the_panel_emits_has_a_schema() { + let fixture = fixture().await; + let owner = fixture + .state + .sessions + .lock() + .await + .account_for_cookie(&fixture.cookie, Utc::now()) + .unwrap() + .unwrap() + .account_subject_hash() + .to_owned(); + let stranger = "someone-else"; + let cover = eve::CoverPanel { + cells: 240, + constituent_cap: 24, + urgency_slots: 36, + last: None, + }; + + let mut surfaces = vec![eve::authenticated_surface(&owner, None, &[], &cover).unwrap()]; + two_cell_world( + &fixture.state, + &fixture.cookie, + BTreeMap::from([(SubjectKind::Person, 4)]), + vec![CreateJurisdictionIntent { + handle: "sere".into(), + label: "The Low Sere".into(), + permille: 1000, + }], + false, + ) + .await; + let draft = fixture.state.world.snapshot().await.unwrap(); + assert_eq!(draft.phase, WorldPhase::Draft); + for account in [owner.as_str(), stranger] { + surfaces.push(eve::authenticated_surface(account, Some(&draft), &[], &cover).unwrap()); + } + for body in [CommandBody::ApproveDraft, CommandBody::ActivateWorld] { + let snapshot = fixture.state.world.snapshot().await.unwrap(); + let principal = fixture + .state + .sessions + .lock() + .await + .account_for_cookie(&fixture.cookie, Utc::now()) + .unwrap() + .unwrap(); + fixture + .state + .world + .submit_principal( + PrincipalCommandIntent { + id: CommandId::new(), + world_id: snapshot.world_id, + expected_revision: snapshot.revision, + body, + }, + &principal, + ) + .await + .unwrap(); + } + let active = fixture.state.world.snapshot().await.unwrap(); + assert_eq!(active.phase, WorldPhase::Active); + for account in [owner.as_str(), stranger] { + surfaces.push(eve::authenticated_surface(account, Some(&active), &[], &cover).unwrap()); + } + surfaces.push(eve::anonymous_surface()); + + let mut seen = 0usize; + for surface in &surfaces { + let mut buttons = Vec::new(); + surface_buttons(&surface["surface"]["root"], &mut buttons); + assert!(!buttons.is_empty()); + for command in &buttons { + assert!( + eve::operation_schema(command).is_some(), + "the panel emits {command}, which Ghostlight does not advertise" + ); + seen += 1; + } + for descriptor in surface["commands"].as_array().unwrap() { + let command = descriptor["command"].as_str().unwrap(); + assert_eq!( + eve::operation_schema(command), + descriptor["payloadSchema"].as_str(), + "the descriptor for {command} names a schema validation will refuse" + ); + } + } + assert!(seen > 5, "the walk found almost nothing to check"); + } + + /// Spec test 12. Seeding is the owner's lane and Draft's lane, and both + /// refusals land before the request reaches a paid endpoint or a vault. + #[tokio::test] + async fn world_seed_is_owner_only_and_draft_only_before_it_spends_anything() { + let fixture = fixture().await; + let stranger = fixture + .state + .sessions + .lock() + .await + .create_session(heimdall::VerifiedSessionAdmission::fixture( + "stranger-account", + "heimdall-stranger", + 1, + Utc::now() + chrono::Duration::hours(1), + Utc::now() + chrono::Duration::days(1), + "stranger-refresh", + )) + .unwrap(); + two_cell_world( + &fixture.state, + &fixture.cookie, + BTreeMap::from([(SubjectKind::Person, 4)]), + vec![CreateJurisdictionIntent { + handle: "sere".into(), + label: "The Low Sere".into(), + permille: 1000, + }], + false, + ) + .await; + let draft = fixture.state.world.snapshot().await.unwrap(); + + // The vault root is deliberately unset: neither refusal may reach it. + unsafe { std::env::remove_var(SEED_VAULT_ROOT_ENVIRONMENT) }; + let denied = post( + &fixture.state, + &stranger, + invocation( + "world.seed", + "ghostlight.world_seed.v1", + draft.revision + 1, + json!({}), + &uuid::Uuid::new_v4().to_string(), + ), + ) + .await; + assert_eq!(denied["state"], "denied"); + assert!( + denied["message"].as_str().unwrap().contains("owner"), + "{denied}" + ); + + for body in [CommandBody::ApproveDraft, CommandBody::ActivateWorld] { + let snapshot = fixture.state.world.snapshot().await.unwrap(); + let principal = fixture + .state + .sessions + .lock() + .await + .account_for_cookie(&fixture.cookie, Utc::now()) + .unwrap() + .unwrap(); + fixture + .state + .world + .submit_principal( + PrincipalCommandIntent { + id: CommandId::new(), + world_id: snapshot.world_id, + expected_revision: snapshot.revision, + body, + }, + &principal, + ) + .await + .unwrap(); + } + let active = fixture.state.world.snapshot().await.unwrap(); + let refused = post( + &fixture.state, + &fixture.cookie, + invocation( + "world.seed", + "ghostlight.world_seed.v1", + active.revision + 1, + json!({}), + &uuid::Uuid::new_v4().to_string(), + ), + ) + .await; + assert_eq!(refused["state"], "accepted"); + assert_eq!(refused["receipt"]["outcome"], "not_draft"); + assert_eq!( + fixture.state.world.snapshot().await.unwrap().revision, + active.revision, + "a refused seed still moved the world" + ); + } + + /// Spec test 13. The card's rows are the deficit rows, and the shortfall it + /// names is the row the runner will actually select. + #[tokio::test] + async fn the_seed_card_projects_the_deficit_it_will_answer() { + let fixture = fixture().await; + let owner = fixture + .state + .sessions + .lock() + .await + .account_for_cookie(&fixture.cookie, Utc::now()) + .unwrap() + .unwrap() + .account_subject_hash() + .to_owned(); + two_cell_world( + &fixture.state, + &fixture.cookie, + BTreeMap::from([(SubjectKind::Person, 9)]), + vec![CreateJurisdictionIntent { + handle: "sere".into(), + label: "The Low Sere".into(), + permille: 1000, + }], + false, + ) + .await; + let draft = fixture.state.world.snapshot().await.unwrap(); + let surface = eve::authenticated_surface( + &owner, + Some(&draft), + &[], + &eve::CoverPanel { + cells: 240, + constituent_cap: 24, + urgency_slots: 36, + last: None, + }, + ) + .unwrap(); + let encoded = serde_json::to_string(&surface).unwrap(); + let card = surface["surface"]["root"]["children"] + .as_array() + .unwrap() + .iter() + .find(|child| child["id"] == "world.seed.card") + .expect("the seed card"); + assert_eq!( + card["children"].as_array().unwrap().len(), + draft.scale_deficit.len() + ); + let selected = crate::world::select_row(&draft).expect("a shortfall to answer"); + assert_eq!(selected.target, 9); + assert!( + card["props"]["nextShortfall"] + .as_str() + .unwrap() + .contains(&format!("short {}", selected.deficit)), + "{card}" + ); + assert!( + card["props"]["detail"] + .as_str() + .unwrap() + .contains(&format!("{}", draft.revision.saturating_sub(1))), + ); + assert!(encoded.contains("world.seed"), "the button is missing"); + } } diff --git a/crates/ghostlight-dungeon/src/world/controllers.rs b/crates/ghostlight-dungeon/src/world/controllers.rs index fa989bc..18ba734 100644 --- a/crates/ghostlight-dungeon/src/world/controllers.rs +++ b/crates/ghostlight-dungeon/src/world/controllers.rs @@ -4943,8 +4943,13 @@ fn purpose_name(purpose: InferencePurpose) -> &'static str { #[cfg(test)] mod tests { use super::*; - use crate::world::patch::{kernel_speak_entry, kernel_speak_grant}; - use crate::world::{CoverBudget, WorldScaleIntentRef, derive_cover}; + use crate::world::elaboration::{EvidenceError, EvidenceQuery, EvidenceReceipt}; + use crate::world::patch::{RECORD_GAP_PATCH_TOOL, kernel_speak_entry, kernel_speak_grant}; + use crate::world::{ + CommitmentKind, CoverBudget, CreateJurisdictionIntent, CreateWorldIntent, + EntityDeclaration, EntityId, EvidenceRef, JurisdictionKey, Ref, SeedOutcome, SeedPort, + TickMinutes, WorldScaleIntentRef, derive_cover, + }; /// The granted catalog every controller fixture works against: the /// kernel-built Speak entry under a fixture id. @@ -6034,7 +6039,7 @@ mod tests { title: "Controller Fixture".into(), patch: WorldPatch { declarations: vec![ - Declaration::Entity(crate::world::EntityDeclaration { + Declaration::Entity(EntityDeclaration { handle: DraftHandle::new("commons"), label: "The Commons".into(), kind: EntityKind::Place, @@ -6047,9 +6052,7 @@ mod tests { controller, affordances: kernel_speak_grant(), // Speech needs a room to fill. - position: Some(crate::world::Ref::Draft(DraftHandle::new( - "commons", - ))), + position: Some(Ref::Draft(DraftHandle::new("commons"))), }), ], operations: Vec::new(), @@ -7026,8 +7029,8 @@ mod tests { tempfile::TempDir, WorldMailbox, tokio::task::JoinHandle<()>, - crate::world::EntityId, - crate::world::EntityId, + EntityId, + EntityId, ) { let directory = tempfile::tempdir().unwrap(); let (mailbox, task) = WorldMailbox::open(directory.path().join("world.cc")).unwrap(); @@ -7041,25 +7044,23 @@ mod tests { title: "Elaboration Fixture".into(), patch: WorldPatch { declarations: vec![ - Declaration::Entity(crate::world::EntityDeclaration { + Declaration::Entity(EntityDeclaration { handle: DraftHandle::new("commons"), label: "The Commons".into(), kind: EntityKind::Place, container: None, }), - Declaration::Entity(crate::world::EntityDeclaration { + Declaration::Entity(EntityDeclaration { handle: DraftHandle::new("road"), label: "The Unwalked Road".into(), kind: EntityKind::Place, - container: Some(crate::world::Ref::Draft(DraftHandle::new( - "commons", - ))), + container: Some(Ref::Draft(DraftHandle::new("commons"))), }), Declaration::Route(crate::world::RouteDeclaration { handle: DraftHandle::new("lane"), label: "The Long Lane".into(), - from: crate::world::Ref::Draft(DraftHandle::new("commons")), - to: crate::world::Ref::Draft(DraftHandle::new("road")), + from: Ref::Draft(DraftHandle::new("commons")), + to: Ref::Draft(DraftHandle::new("road")), access: crate::world::AccessKind::Public, cost: Cost(1), }), @@ -7069,9 +7070,7 @@ mod tests { kind: SubjectKind::Person, controller: NewController::NarrativePersona, affordances: kernel_speak_grant(), - position: Some(crate::world::Ref::Draft(DraftHandle::new( - "commons", - ))), + position: Some(Ref::Draft(DraftHandle::new("commons"))), }), ], operations: Vec::new(), @@ -7122,7 +7121,7 @@ mod tests { #[tokio::test] async fn soul_an_elaboration_session_resumes_and_repairs_from_its_mismatch_set() { let (_directory, mailbox, task, commons, road) = elaboration_mailbox().await; - let jurisdiction = crate::world::JurisdictionKey::PlaceSubtree(commons); + let jurisdiction = JurisdictionKey::PlaceSubtree(commons); let road_id = serde_json::to_value(road).unwrap(); let store = Arc::new(RecordingWorkStore { persisted: Arc::new(AtomicBool::new(false)), @@ -7274,10 +7273,7 @@ mod tests { ); // The road's own subtree holds the road's boundary; a leaf with nothing // under it and no deficit row is clean. - let outcome = runner - .step(crate::world::JurisdictionKey::Uncovered) - .await - .unwrap(); + let outcome = runner.step(JurisdictionKey::Uncovered).await.unwrap(); assert_eq!( outcome, crate::world::elaboration::ElaborationOutcome::Clean @@ -7303,7 +7299,7 @@ mod tests { let (mailbox, task) = WorldMailbox::open(directory.path().join("world.cc")).unwrap(); let owner = PrincipalId::new("owner"); let authenticated = AuthenticatedCaller::fixture(CallerId::Principal(owner.clone())); - let mut declarations = vec![Declaration::Entity(crate::world::EntityDeclaration { + let mut declarations = vec![Declaration::Entity(EntityDeclaration { handle: DraftHandle::new("commons"), label: "The Commons".into(), kind: EntityKind::Place, @@ -7316,7 +7312,7 @@ mod tests { kind: SubjectKind::Person, controller, affordances: kernel_speak_grant(), - position: Some(crate::world::Ref::Draft(DraftHandle::new("commons"))), + position: Some(Ref::Draft(DraftHandle::new("commons"))), })); } let creation = mailbox @@ -7849,7 +7845,7 @@ mod tests { let authenticated = AuthenticatedCaller::fixture(CallerId::Principal(owner.clone())); let mut declarations = Vec::new(); for (index, controller) in controllers.into_iter().enumerate() { - declarations.push(Declaration::Entity(crate::world::EntityDeclaration { + declarations.push(Declaration::Entity(EntityDeclaration { handle: DraftHandle::new(&format!("room{index}")), label: format!("Room {index}"), kind: EntityKind::Place, @@ -7861,9 +7857,7 @@ mod tests { kind: SubjectKind::Person, controller, affordances: kernel_speak_grant(), - position: Some(crate::world::Ref::Draft(DraftHandle::new(&format!( - "room{index}" - )))), + position: Some(Ref::Draft(DraftHandle::new(&format!("room{index}")))), })); } let creation = mailbox @@ -8289,4 +8283,1104 @@ mod tests { } ); } + + // ---- The seed lane --------------------------------------------------- + + struct SeedFixture { + _directory: tempfile::TempDir, + mailbox: WorldMailbox, + task: tokio::task::JoinHandle<()>, + owner: PrincipalId, + principal: crate::app_session::VerifiedPrincipalEvidence, + sere: EntityId, + speak: AffordanceId, + } + + /// A Draft world with an authored scale intent: persons wanted in the Low + /// Sere, none alive yet, plus the owner's own subject standing in the + /// commons, which no declared root covers. + async fn seed_mailbox(target: u32) -> SeedFixture { + let directory = tempfile::tempdir().unwrap(); + let (mailbox, task) = WorldMailbox::open(directory.path().join("world.cc")).unwrap(); + let owner = PrincipalId::new("seed-owner"); + let authenticated = AuthenticatedCaller::fixture(CallerId::Principal(owner.clone())); + mailbox + .create_fixture( + CreateWorld { + id: CommandId::new(), + owner: owner.clone(), + title: "Seed Fixture".into(), + patch: WorldPatch { + declarations: vec![ + Declaration::Entity(EntityDeclaration { + handle: DraftHandle::new("commons"), + label: "The Commons".into(), + kind: EntityKind::Place, + container: None, + }), + Declaration::Entity(EntityDeclaration { + handle: DraftHandle::new("sere"), + label: "The Low Sere".into(), + kind: EntityKind::Place, + container: None, + }), + Declaration::Subject(SubjectDeclaration { + handle: DraftHandle::new("first-person"), + label: "The Owner".into(), + kind: SubjectKind::Person, + controller: NewController::Human { + principal: owner.clone(), + }, + affordances: kernel_speak_grant(), + position: Some(Ref::Draft(DraftHandle::new("commons"))), + }), + ], + operations: Vec::new(), + evidence: Vec::new(), + }, + scale_intent: WorldScaleIntentRef { + targets: BTreeMap::from([(SubjectKind::Person, target)]), + jurisdictions: BTreeMap::from([(DraftHandle::new("sere"), 1000)]), + }, + }, + &authenticated, + ) + .await + .unwrap(); + let snapshot = mailbox.snapshot().await.unwrap(); + assert_eq!(snapshot.phase, WorldPhase::Draft); + let sere = snapshot + .places + .iter() + .find(|place| place.label == "The Low Sere") + .expect("the declared root") + .id; + let speak = snapshot + .affordances + .iter() + .find(|entry| entry.entry.kind.0 == "speak") + .expect("the kernel Speak entry") + .id; + SeedFixture { + _directory: directory, + mailbox, + task, + owner, + principal: crate::app_session::VerifiedPrincipalEvidence::fixture("seed-owner"), + sere, + speak, + } + } + + fn seed_port(fixture: &SeedFixture) -> SeedPort { + SeedPort::new(fixture.mailbox.clone(), fixture.principal.clone()) + } + + /// One session that declares `handles.len()` persons who qualify: a + /// controller, the granted Speak entry, a position inside the root, and a + /// personal goal each. + fn author_persons( + receipt: &str, + root: EntityId, + speak: AffordanceId, + handles: &[&str], + ) -> Result { + author_across(receipt, speak, &[(root, handles)]) + } + + /// The same, over more than one jurisdiction root in one patch. + fn author_across( + receipt: &str, + speak: AffordanceId, + groups: &[(EntityId, &[&str])], + ) -> Result { + let entry = serde_json::to_value(speak).unwrap(); + let mut calls: Vec<(&str, Value)> = Vec::new(); + let first = groups + .first() + .and_then(|(_, handles)| handles.first().copied()); + for (root, handles) in groups { + author_group(&mut calls, *root, &entry, handles, first); + } + calls.push(("submit", json!({}))); + tool_round(calls, receipt) + } + + fn author_group( + calls: &mut Vec<(&'static str, Value)>, + root: EntityId, + entry: &Value, + handles: &[&str], + owed: Option<&str>, + ) { + let place = serde_json::to_value(root).unwrap(); + for (index, handle) in handles.iter().enumerate() { + calls.push(( + "declare_subject", + json!({ + "handle": handle, + "label": format!("Sere {handle}"), + "kind": "person", + "controller": {"type": "narrative_persona"}, + "affordances": [{"ref": "existing", "value": entry}], + "position": {"ref": "existing", "value": place}, + }), + )); + calls.push(( + "create_commitment", + json!({ + "subject": {"ref": "draft", "value": handle}, + "counterparty": null, + "kind": "goal", + "due": 600, + "period": null, + "checks": [], + }), + )); + // Everyone after the first owes the first something. That is what + // leaves the Active elaborator a `MissingStructure` boundary to + // answer; a goal cannot, because a goal carrying a counterparty is + // refused with `GoalWithCounterparty`. + if let Some(owed) = owed.filter(|owed| owed != handle) { + let _ = index; + calls.push(( + "create_commitment", + json!({ + "subject": {"ref": "draft", "value": handle}, + "counterparty": {"ref": "draft", "value": owed}, + "kind": "obligation", + "due": 900, + "period": null, + "checks": [], + }), + )); + } + } + } + + fn fresh_store() -> Arc { + Arc::new(RecordingWorkStore { + persisted: Arc::new(AtomicBool::new(false)), + work: Mutex::new(BTreeMap::new()), + }) + } + + fn seed_script(rounds: Vec>) -> Arc { + Arc::new(ElaborationScript { + outputs: Mutex::new(rounds), + seen: Mutex::new(Vec::new()), + }) + } + + /// A fixture Vault that hands back exactly what it was built with, so a + /// citation test can name a reference inside and outside the retrieved set + /// without a filesystem. + struct FixtureVault { + receipts: Vec, + } + + #[async_trait] + impl EvidenceSource for FixtureVault { + async fn retrieve( + &self, + _query: &EvidenceQuery, + ) -> Result, EvidenceError> { + Ok(self.receipts.clone()) + } + } + + fn seed_runner( + fixture: &SeedFixture, + script: Arc, + store: Arc, + evidence: Arc, + ) -> SeedRunner { + SeedRunner::new( + seed_port(fixture), + script, + evidence, + store, + models().elaborator, + None, + ) + } + + fn root_deficit(snapshot: &WorldSnapshot, root: EntityId) -> u32 { + snapshot + .scale_deficit + .iter() + .find(|row| row.jurisdiction == JurisdictionKey::PlaceSubtree(root)) + .map_or(0, |row| row.deficit) + } + + /// Spec test 4. One session commits one Draft patch as the owner's own act, + /// and every subject it declared qualifies. + #[tokio::test] + async fn a_seed_session_commits_a_draft_patch_as_the_owner() { + let fixture = seed_mailbox(6).await; + let before = fixture.mailbox.snapshot().await.unwrap(); + let runner = seed_runner( + &fixture, + seed_script(vec![author_persons( + "seed-round-zero", + fixture.sere, + fixture.speak, + &["digger", "warden"], + )]), + fresh_store(), + Arc::new(NullEvidenceSource), + ); + assert_eq!(runner.step().await.unwrap(), SeedOutcome::Committed); + + let after = fixture.mailbox.snapshot().await.unwrap(); + assert_eq!(after.revision, before.revision + 1); + assert_eq!(after.phase, WorldPhase::Draft, "a seed never activates"); + for label in ["Sere digger", "Sere warden"] { + let subject = after + .subjects + .iter() + .find(|subject| subject.label == label) + .expect("the declared subject"); + assert!(subject.controller_id.is_some()); + assert!(!subject.affordances.is_empty()); + assert_eq!(subject.position, Some(fixture.sere)); + assert!( + subject + .commitments + .iter() + .any(|held| held.kind == CommitmentKind::Goal) + ); + assert!(subject.qualified, "{label} does not reduce the deficit"); + } + assert_eq!(root_deficit(&after, fixture.sere), 4); + + // The commit is the owner's own act, and that is not a convention: the + // only unconfined Draft author is `Principal(owner)`, so a port built + // on anyone else's evidence is refused by the reducer. + let stranger = SeedPort::new( + fixture.mailbox.clone(), + crate::app_session::VerifiedPrincipalEvidence::fixture("not-the-owner"), + ); + let refused = stranger + .submit_seed( + CommandId::new(), + after.world_id, + after.revision, + WorldPatch { + declarations: vec![Declaration::Entity(EntityDeclaration { + handle: DraftHandle::new("shed"), + label: "A Shed".into(), + kind: EntityKind::Place, + container: None, + })], + operations: Vec::new(), + evidence: Vec::new(), + }, + ) + .await; + assert!( + matches!( + refused, + Err(MailboxError::Kernel(KernelError::Unauthorized)) + ), + "{refused:?}" + ); + drop(stranger); + + drop(runner); + drop(fixture.mailbox); + fixture.task.await.unwrap(); + } + + /// Spec test 5. Termination is derived: the deficit falls as patches land + /// and the sweep stops at zero without spending a further inference call. + #[tokio::test] + async fn the_deficit_falls_as_patches_land_and_the_sweep_stops_at_zero() { + let fixture = seed_mailbox(6).await; + assert_eq!( + root_deficit(&fixture.mailbox.snapshot().await.unwrap(), fixture.sere), + 6 + ); + let scripted = seed_script(vec![ + author_persons("r0", fixture.sere, fixture.speak, &["a1", "a2"]), + author_persons("r1", fixture.sere, fixture.speak, &["b1", "b2"]), + author_persons("r2", fixture.sere, fixture.speak, &["c1", "c2"]), + ]); + let runner = seed_runner( + &fixture, + scripted.clone(), + fresh_store(), + Arc::new(NullEvidenceSource), + ); + assert_eq!(runner.sweep(8).await.unwrap(), SeedOutcome::Clean); + assert_eq!( + root_deficit(&fixture.mailbox.snapshot().await.unwrap(), fixture.sere), + 0 + ); + assert_eq!( + scripted.seen.lock().unwrap().len(), + 3, + "the fourth step is clean and must spend nothing" + ); + + drop(runner); + drop(fixture.mailbox); + fixture.task.await.unwrap(); + } + + /// Spec test 6. A commit that does not strictly lower its row is a fixed + /// point, not a retry. + #[tokio::test] + async fn a_committed_patch_that_does_not_move_the_deficit_stops_the_row() { + let fixture = seed_mailbox(6).await; + let scripted = seed_script(vec![tool_round( + vec![ + ( + "declare_place", + json!({"handle": "shed", "label": "A Shed", "container": null}), + ), + ("submit", json!({})), + ], + "r0", + )]); + let runner = seed_runner( + &fixture, + scripted.clone(), + fresh_store(), + Arc::new(NullEvidenceSource), + ); + assert_eq!(runner.sweep(4).await.unwrap(), SeedOutcome::NoProgress); + assert_eq!( + scripted.seen.lock().unwrap().len(), + 1, + "a fixed point spun against the endpoint" + ); + + drop(runner); + drop(fixture.mailbox); + fixture.task.await.unwrap(); + } + + /// Spec test 7. Seeding is Draft's lane, refused twice: the runner's own + /// phase gate spends nothing, and the kernel refuses the port directly + /// because an Active patch that declares must answer and this lane cannot + /// express an answer. + #[tokio::test] + async fn a_seed_patch_cannot_be_admitted_in_active_by_the_seed_lane() { + let fixture = seed_mailbox(6).await; + let mut snapshot = fixture.mailbox.snapshot().await.unwrap(); + let authenticated = + AuthenticatedCaller::fixture(CallerId::Principal(fixture.owner.clone())); + for body in [CommandBody::ApproveDraft, CommandBody::ActivateWorld] { + fixture + .mailbox + .submit_fixture( + CommandEnvelope { + id: CommandId::new(), + world_id: snapshot.world_id, + expected_revision: snapshot.revision, + caller: CallerId::Principal(fixture.owner.clone()), + body, + }, + &authenticated, + ) + .await + .unwrap(); + snapshot = fixture.mailbox.snapshot().await.unwrap(); + } + assert_eq!(snapshot.phase, WorldPhase::Active); + + let store = fresh_store(); + let scripted = seed_script(vec![author_persons( + "r0", + fixture.sere, + fixture.speak, + &["late"], + )]); + let runner = seed_runner( + &fixture, + scripted.clone(), + store.clone(), + Arc::new(NullEvidenceSource), + ); + assert_eq!(runner.step().await.unwrap(), SeedOutcome::NotDraft); + assert!(scripted.seen.lock().unwrap().is_empty()); + assert!(store.work.lock().unwrap().is_empty()); + + let refused = seed_port(&fixture) + .submit_seed( + CommandId::new(), + snapshot.world_id, + snapshot.revision, + WorldPatch { + declarations: vec![Declaration::Entity(EntityDeclaration { + handle: DraftHandle::new("shed"), + label: "A Shed".into(), + kind: EntityKind::Place, + container: None, + })], + operations: Vec::new(), + evidence: Vec::new(), + }, + ) + .await; + assert!( + matches!( + refused, + Err(MailboxError::Kernel(KernelError::AnswerRequired)) + ), + "{refused:?}" + ); + + drop(runner); + drop(fixture.mailbox); + fixture.task.await.unwrap(); + } + + /// Spec test 8. `SeedPort` has two methods. Approve, activate, the clock, + /// the operator log, and the agency graph are not among them, so reaching + /// for one fails to compile rather than failing a test; what runs here is + /// the other half — the body is hardcoded, so the lane cannot express any + /// command but an unanswered `AdmitPatch`, and a commit through it moves + /// neither the phase, nor the approvals, nor the clock. + #[tokio::test] + async fn the_seed_lane_cannot_approve_or_activate() { + let fixture = seed_mailbox(6).await; + let port = seed_port(&fixture); + let snapshot = port.snapshot().await.unwrap(); + assert_eq!(snapshot.phase, WorldPhase::Draft); + assert!(snapshot.draft_approvals.is_empty()); + + port.submit_seed( + CommandId::new(), + snapshot.world_id, + snapshot.revision, + WorldPatch { + declarations: vec![Declaration::Entity(EntityDeclaration { + handle: DraftHandle::new("shed"), + label: "A Shed".into(), + kind: EntityKind::Place, + container: None, + })], + operations: Vec::new(), + evidence: Vec::new(), + }, + ) + .await + .unwrap(); + let after = port.snapshot().await.unwrap(); + assert_eq!(after.revision, snapshot.revision + 1); + assert_eq!(after.phase, WorldPhase::Draft); + assert!(after.draft_approvals.is_empty()); + assert_eq!(after.now, snapshot.now, "the lane moved the clock"); + + drop(port); + drop(fixture.mailbox); + fixture.task.await.unwrap(); + } + + /// Spec test 9. Provenance's owner is the runner. A citation outside the + /// round's retrieved set is dropped before the kernel sees it, so the + /// canonical fact that named it is refused; the same fact citing a + /// reference the Vault did return commits. + #[tokio::test] + async fn a_citation_outside_the_retrieved_set_is_dropped_and_an_uncited_fact_is_refused() { + let retrieved = EvidenceReceipt { + reference: EvidenceRef::new("Public/Places/Low Sere.md"), + excerpt: "The sere runs dry nine months a year.".into(), + source: "Low Sere".into(), + }; + let fact = |reference: &str| { + tool_round( + vec![ + ( + "declare_fact", + json!({ + "handle": "dry", + "label": "The Dry Season", + "statement": "The sere runs dry nine months a year.", + "standing": {"standing": "canonical", "evidence": reference}, + }), + ), + ("submit", json!({})), + ], + "r0", + ) + }; + + let forged = seed_mailbox(6).await; + let before = forged.mailbox.snapshot().await.unwrap().revision; + let refused = seed_runner( + &forged, + seed_script(vec![fact("Public/Places/Nowhere.md")]), + fresh_store(), + Arc::new(FixtureVault { + receipts: vec![retrieved.clone()], + }), + ); + assert_eq!(refused.step().await.unwrap(), SeedOutcome::Rejected); + assert_eq!( + forged.mailbox.snapshot().await.unwrap().revision, + before, + "a forged citation minted canon" + ); + drop(refused); + drop(forged.mailbox); + forged.task.await.unwrap(); + + let cited = seed_mailbox(6).await; + let before = cited.mailbox.snapshot().await.unwrap().revision; + let admitted = seed_runner( + &cited, + seed_script(vec![fact("Public/Places/Low Sere.md")]), + fresh_store(), + Arc::new(FixtureVault { + receipts: vec![retrieved], + }), + ); + // It commits; it declares nobody alive, so the row does not move and + // the outcome is the honest fixed point rather than a rejection. + assert_eq!(admitted.step().await.unwrap(), SeedOutcome::NoProgress); + assert_eq!( + cited.mailbox.snapshot().await.unwrap().revision, + before + 1, + "the cited fact was refused" + ); + drop(admitted); + drop(cited.mailbox); + cited.task.await.unwrap(); + } + + /// Spec test 10. A rejection reopens the same derived id with the kernel's + /// complete mismatch set, and a fresh runner over the same store resumes it + /// rather than opening a second session. + #[tokio::test] + async fn a_seed_session_resumes_its_checkpoint_under_the_same_derived_id() { + let fixture = seed_mailbox(6).await; + let store = fresh_store(); + let entry = serde_json::to_value(fixture.speak).unwrap(); + let scripted = seed_script(vec![ + // A subject standing in a handle nothing declares. + tool_round( + vec![ + ( + "declare_subject", + json!({ + "handle": "digger", + "label": "Sere digger", + "kind": "person", + "controller": {"type": "narrative_persona"}, + "affordances": [{"ref": "existing", "value": entry}], + "position": {"ref": "draft", "value": "nowhere"}, + }), + ), + ("submit", json!({})), + ], + "r0", + ), + author_persons("r1", fixture.sere, fixture.speak, &["digger"]), + ]); + let first = seed_runner( + &fixture, + scripted.clone(), + store.clone(), + Arc::new(NullEvidenceSource), + ); + assert_eq!(first.step().await.unwrap(), SeedOutcome::Rejected); + drop(first); + + let (command_id, prompt) = { + let stored = store.work.lock().unwrap(); + assert_eq!(stored.len(), 1, "one session, one row"); + let ControllerWork::Seed(SeedCheckpoint::SeedInFlight { + command_id, + last_mismatches, + agent_prompt, + completed, + .. + }) = stored.values().next().unwrap().clone() + else { + panic!("the rejection did not reopen the session for repair"); + }; + assert!(!last_mismatches.is_empty(), "the repair set is empty"); + assert!(completed.is_empty(), "a rejected round kept its evidence"); + (command_id, agent_prompt) + }; + assert!( + prompt.contains("Your previous patch was refused"), + "{prompt}" + ); + assert!(prompt.contains("You are seeding a world before it opens")); + assert!(prompt.contains("holds a goal commitment")); + + let second = seed_runner( + &fixture, + scripted, + store.clone(), + Arc::new(NullEvidenceSource), + ); + assert_eq!(second.step().await.unwrap(), SeedOutcome::Committed); + { + let stored = store.work.lock().unwrap(); + assert_eq!(stored.len(), 1, "the resume opened a second row"); + assert_eq!(stored.values().next().unwrap().command_id(), command_id); + } + + drop(second); + drop(fixture.mailbox); + fixture.task.await.unwrap(); + } + + /// Spec test 16. A v9 row is refused at open beside the pinned v8 and v7 + /// refusals; a seed checkpoint written over another lane's row under one + /// command id is a mode conflict; cross-lane progression is false; and a + /// first write that is not the lane's initial stage is refused. + #[tokio::test] + async fn a_controller_work_row_written_before_this_pass_is_refused() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("controller-work.cc"); + let command_id = CommandId::new(); + let opportunity = fixture_opportunity(ControllerMode::OperationalAgent); + let work = operational_in_flight( + command_id, + &opportunity, + "Hold the bridge.", + "operational-model", + vec![], + ); + { + let mut store = OwnedRedbMessagePackBackingStore::new(&path).unwrap(); + store + .push(&CultCacheEnvelope { + key: store_key(command_id).unwrap(), + r#type: "controller_work.v9".into(), + payload: rmp_serde::to_vec_named(&work).unwrap(), + stored_at: Utc::now().to_rfc3339(), + schema_id: Some("ghostlight.controller_work.v9".into()), + }) + .unwrap(); + } + let Err(error) = CultCacheControllerWorkStore::open(&path) else { + panic!("a v9 row was accepted by the v10 store"); + }; + assert!(matches!(error, ControllerWorkStoreError::Fault { .. })); + + // A real seed row, taken from a session that committed. + let fixture = seed_mailbox(6).await; + let store = fresh_store(); + let runner = seed_runner( + &fixture, + seed_script(vec![author_persons( + "r0", + fixture.sere, + fixture.speak, + &["digger"], + )]), + store.clone(), + Arc::new(NullEvidenceSource), + ); + assert_eq!(runner.step().await.unwrap(), SeedOutcome::Committed); + drop(runner); + let seed_row = store.work.lock().unwrap().values().next().unwrap().clone(); + let seed_command = seed_row.command_id(); + + // Another lane's row under the same command id. The store is seeded + // directly, because what is under test is the conflict on write and not + // the other lane's own entry path. + let other = operational_in_flight( + seed_command, + &opportunity, + "Hold the bridge.", + "operational-model", + vec![], + ); + let conflicted = fresh_store(); + conflicted + .work + .lock() + .unwrap() + .insert(seed_command, other.clone()); + assert!(matches!( + conflicted.persist(&seed_row).await, + Err(ControllerWorkStoreError::CommandModeConflict) + )); + assert!(!valid_controller_work_progression(&other, &seed_row)); + assert!(!valid_controller_work_progression(&seed_row, &other)); + + // The row the committing session left is `ReadyToSubmit`, which is not + // the lane's initial stage, so a store that has never seen the session + // refuses it. + assert!(matches!( + seed_row, + ControllerWork::Seed(SeedCheckpoint::ReadyToSubmit { .. }) + )); + assert!(fresh_store().persist(&seed_row).await.is_err()); + + drop(fixture.mailbox); + fixture.task.await.unwrap(); + } + + /// Spec test 17. The whole road: a world is created with a two-root scale + /// intent, seeded to zero deficit, approved, activated, elaborated, ticked, + /// and then replayed from its own journal to the same state digest. + #[tokio::test] + async fn a_world_is_created_seeded_approved_activated_and_replays() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("world.cc"); + let owner = PrincipalId::new("seed-owner"); + let principal = crate::app_session::VerifiedPrincipalEvidence::fixture("seed-owner"); + let authenticated = AuthenticatedCaller::fixture(CallerId::Principal(owner.clone())); + let (mailbox, task) = WorldMailbox::open(&path).unwrap(); + mailbox + .create( + CreateWorldIntent { + id: CommandId::new(), + title: "The Whole Road".into(), + human_subject_label: "The Owner".into(), + narrative_persona_label: None, + operational_agent_label: None, + targets: BTreeMap::from([(SubjectKind::Person, 4)]), + jurisdictions: vec![ + CreateJurisdictionIntent { + handle: "sere".into(), + label: "The Low Sere".into(), + permille: 500, + }, + CreateJurisdictionIntent { + handle: "gate".into(), + label: "The Rain Gate".into(), + permille: 500, + }, + ], + }, + &principal, + ) + .await + .unwrap(); + let genesis = mailbox.snapshot().await.unwrap(); + assert_eq!(genesis.scale_deficit.len(), 2, "one row per declared root"); + assert!( + genesis + .scale_deficit + .iter() + .all(|row| row.target == 2 && row.deficit == 2) + ); + let root = |label: &str| { + genesis + .places + .iter() + .find(|place| place.label == label) + .expect("the declared root") + .id + }; + let (sere, gate) = (root("The Low Sere"), root("The Rain Gate")); + let speak = genesis + .affordances + .iter() + .find(|entry| entry.entry.kind.0 == "speak") + .unwrap() + .id; + + let runner = SeedRunner::new( + SeedPort::new(mailbox.clone(), principal.clone()), + // One patch fills both roots. Which row the runner selects first is + // the snapshot's order and not this test's business; a session that + // answers both cannot pick wrong. + seed_script(vec![author_across( + "r0", + speak, + &[(sere, &["s1", "s2"]), (gate, &["g1", "g2"])], + )]), + Arc::new(NullEvidenceSource), + fresh_store(), + models().elaborator, + None, + ); + assert_eq!(runner.sweep(3).await.unwrap(), SeedOutcome::Clean); + let seeded = mailbox.snapshot().await.unwrap(); + assert!( + seeded.scale_deficit.iter().all(|row| row.deficit == 0), + "{:?}", + seeded.scale_deficit + ); + for label in ["Sere s1", "Sere s2", "Sere g1", "Sere g2"] { + assert!( + seeded + .subjects + .iter() + .any(|subject| subject.label == label && subject.qualified) + ); + } + drop(runner); + + let mut snapshot = seeded; + for body in [CommandBody::ApproveDraft, CommandBody::ActivateWorld] { + mailbox + .submit_fixture( + CommandEnvelope { + id: CommandId::new(), + world_id: snapshot.world_id, + expected_revision: snapshot.revision, + caller: CallerId::Principal(owner.clone()), + body, + }, + &authenticated, + ) + .await + .unwrap(); + snapshot = mailbox.snapshot().await.unwrap(); + } + assert_eq!(snapshot.phase, WorldPhase::Active); + // The seeded goals have no counterparty who can command or litigate + // them, which is exactly the boundary the elaborator exists to answer. + assert!( + !snapshot.boundaries.is_empty(), + "the seed manufactured nothing for the elaborator" + ); + + let elaborator = ElaborationRunner::new( + ElaborationPort::new(mailbox.clone()), + // One text-only round per jurisdiction: the sweep reaches every + // root the intent named plus the uncovered residual, and each ends + // in `NoPatch` without a tool call. What is under test here is that + // the elaborator has something to be given, not what it does with + // it. + seed_script( + (0..4) + .map(|round| { + output( + vec![InferenceEvent::Text("nothing to add".into())], + &format!("e{round}"), + ) + }) + .collect(), + ), + Arc::new(NullEvidenceSource), + fresh_store(), + models().elaborator, + ); + elaborator.sweep().await.unwrap(); + drop(elaborator); + + mailbox + .submit_clock(CommandId::new(), TickMinutes::new(60).unwrap()) + .await + .unwrap(); + let final_snapshot = mailbox.snapshot().await.unwrap(); + assert!(final_snapshot.now.0 >= 60); + drop(mailbox); + task.await.unwrap(); + + // Replay: the journal alone reproduces the state, digest for digest. + let (reopened, replay_task) = WorldMailbox::open(&path).unwrap(); + let replayed = reopened.snapshot().await.unwrap(); + assert_eq!(replayed.state_digest, final_snapshot.state_digest); + assert_eq!(replayed.revision, final_snapshot.revision); + assert_eq!(replayed.subjects.len(), final_snapshot.subjects.len()); + assert_eq!(replayed.scale_deficit, final_snapshot.scale_deficit); + drop(reopened); + replay_task.await.unwrap(); + } + + /// Spec test 2. A create naming two roots yields a world whose places + /// include both, whose deficit carries one row per `(kind, root)`, and + /// whose weights divide the target. The two ways to author a bad intent are + /// refused by the resolver, in its complete mismatch set, rather than + /// pre-checked at ingress. + #[tokio::test] + async fn the_intents_roots_are_declared_and_covered() { + let directory = tempfile::tempdir().unwrap(); + let owner = PrincipalId::new("seed-owner"); + let authenticated = AuthenticatedCaller::fixture(CallerId::Principal(owner.clone())); + let genesis = |intent: WorldScaleIntentRef, roots: &[&str]| CreateWorld { + id: CommandId::new(), + owner: owner.clone(), + title: "Two Roots".into(), + patch: WorldPatch { + declarations: std::iter::once(Declaration::Entity(EntityDeclaration { + handle: DraftHandle::new("commons"), + label: "The Commons".into(), + kind: EntityKind::Place, + container: None, + })) + .chain(roots.iter().map(|handle| { + Declaration::Entity(EntityDeclaration { + handle: DraftHandle::new(*handle), + label: format!("The {handle}"), + kind: EntityKind::Place, + container: None, + }) + })) + .chain(std::iter::once(Declaration::Subject(SubjectDeclaration { + handle: DraftHandle::new("first-person"), + label: "The Owner".into(), + kind: SubjectKind::Person, + controller: NewController::Human { + principal: owner.clone(), + }, + affordances: kernel_speak_grant(), + position: Some(Ref::Draft(DraftHandle::new("commons"))), + }))) + .collect(), + operations: Vec::new(), + evidence: Vec::new(), + }, + scale_intent: intent, + }; + + let (mailbox, task) = WorldMailbox::open(directory.path().join("two-roots.cc")).unwrap(); + mailbox + .create_fixture( + genesis( + WorldScaleIntentRef { + targets: BTreeMap::from([(SubjectKind::Person, 10)]), + jurisdictions: BTreeMap::from([ + (DraftHandle::new("sere"), 700), + (DraftHandle::new("gate"), 300), + ]), + }, + &["sere", "gate"], + ), + &authenticated, + ) + .await + .unwrap(); + let snapshot = mailbox.snapshot().await.unwrap(); + for label in ["The sere", "The gate"] { + assert!(snapshot.places.iter().any(|place| place.label == label)); + } + assert_eq!(snapshot.scale_deficit.len(), 2); + let mut targets: Vec = snapshot + .scale_deficit + .iter() + .map(|row| { + assert_eq!(row.kind, SubjectKind::Person); + assert_eq!(row.deficit, row.target, "nothing is alive at genesis"); + row.target + }) + .collect(); + targets.sort_unstable(); + assert_eq!(targets, vec![3, 7], "the weights did not divide the target"); + drop(mailbox); + task.await.unwrap(); + + // A root the genesis patch does not declare. + let (mailbox, task) = WorldMailbox::open(directory.path().join("unknown.cc")).unwrap(); + let refused = mailbox + .create_fixture( + genesis( + WorldScaleIntentRef { + targets: BTreeMap::from([(SubjectKind::Person, 10)]), + jurisdictions: BTreeMap::from([(DraftHandle::new("nowhere"), 1000)]), + }, + &["sere"], + ), + &authenticated, + ) + .await; + let Err(MailboxError::Kernel(KernelError::PatchRejected(mismatches))) = refused else { + panic!("an undeclared jurisdiction root was admitted"); + }; + assert!( + mismatches.iter().any(|mismatch| matches!( + mismatch, + crate::world::Mismatch::UnknownJurisdictionRoot { .. } + )), + "{mismatches:?}" + ); + drop(mailbox); + task.await.unwrap(); + + // Weights distribute the target and never raise it. + let (mailbox, task) = WorldMailbox::open(directory.path().join("heavy.cc")).unwrap(); + let refused = mailbox + .create_fixture( + genesis( + WorldScaleIntentRef { + targets: BTreeMap::from([(SubjectKind::Person, 10)]), + jurisdictions: BTreeMap::from([ + (DraftHandle::new("sere"), 700), + (DraftHandle::new("gate"), 700), + ]), + }, + &["sere", "gate"], + ), + &authenticated, + ) + .await; + let Err(MailboxError::Kernel(KernelError::PatchRejected(mismatches))) = refused else { + panic!("permille weights over the whole were admitted"); + }; + assert!( + mismatches.iter().any(|mismatch| matches!( + mismatch, + crate::world::Mismatch::ScaleWeightsExceedWhole + )), + "{mismatches:?}" + ); + drop(mailbox); + task.await.unwrap(); + } + + /// Spec test 3. The world's first person belongs to no jurisdiction's + /// population target: it stands in the commons, which no declared root + /// covers, so it counts in `Uncovered` and reduces nothing. + #[tokio::test] + async fn the_owner_subject_lands_uncovered() { + let fixture = seed_mailbox(6).await; + let before = fixture.mailbox.snapshot().await.unwrap(); + let owner_subject = before + .subjects + .iter() + .find(|subject| subject.label == "The Owner") + .expect("the first person"); + assert!( + !owner_subject.qualified, + "the first person holds no goal and cannot count yet" + ); + assert_eq!(before.scale_deficit.len(), 1, "one root, one row"); + + // Give it a goal through the same lane a seed uses, and it appears in + // the residual rather than in the root's row. + let runner = seed_runner( + &fixture, + seed_script(vec![tool_round( + vec![ + ( + "create_commitment", + json!({ + "subject": {"ref": "existing", "value": serde_json::to_value(owner_subject.id).unwrap()}, + "counterparty": null, + "kind": "goal", + "due": 600, + "period": null, + "checks": [], + }), + ), + ("submit", json!({})), + ], + "r0", + )]), + fresh_store(), + Arc::new(NullEvidenceSource), + ); + assert_eq!(runner.step().await.unwrap(), SeedOutcome::NoProgress); + let after = fixture.mailbox.snapshot().await.unwrap(); + let residual = after + .scale_deficit + .iter() + .find(|row| row.jurisdiction == JurisdictionKey::Uncovered) + .expect("the uncovered residual"); + assert_eq!(residual.qualified, 1); + assert_eq!(residual.target, 0, "the first person raised a target"); + assert_eq!( + root_deficit(&after, fixture.sere), + 6, + "the first person reduced a root's shortfall" + ); + + drop(runner); + drop(fixture.mailbox); + fixture.task.await.unwrap(); + } }