Delete world.speak and the story card (Cut 9, part b)
The play card and its own revision counter landed in the prior commit and were verified end to end there (a question answered through the card's own rendered binding, a turn closing with its narration visible through the card, and the card's own subtree never rendering the operator feed). This commit removes the mechanism the play card replaces. Deleted: SpeakPayload, the "world.speak" arm in execute_world, eve.rs's story card and speak control (the opportunity/affordance lookup that fed it), "world.speak" from operation_schema, and current_operator_view (its only purpose was fetching the operator log for the surface; authenticated_surface no longer takes that parameter at all). world.summary's own body no longer counts committed events, since that count depended on the same log read. WorldMailbox::operator_log stays in the library; nothing in this crate's own surface path calls it any more except one Cut 9 regression test that proves the play card structurally cannot reach it. Rewrote the tests that drove speech through Eve: http_eve_journey_uses_one_ world_owner's last leg now plays instead of speaking, and observes the play table (current_turn_view) rather than trusting the accepted response alone (PA.f138). eve.rs's payload_cannot_claim_authority and invocation_metadata_ rejects_legacy_authority_extensions now exercise world.play; the former absorbed the now-redundant world_play_operation_schema_validates_though_not_ yet_advertised, since world.play is advertised as of the prior commit. removed_operation_is_unknown gained "world.speak" as a case. every_operation_ the_panel_emits_has_a_schema now asserts world.play is among the buttons it walks. `rg -n 'world\.speak|operator_log' crates/ghostlight-dungeon/src` is not literally zero: two deliberate hits remain (see the Hands report for exact lines) — one regression assertion that the operation id is gone, and one test-local call into the surviving library method to prove the play card cannot reach what it returns. No production path matches. Dungeon bin: 140 -> 139 tests (two redundant world.speak/world.play schema tests merged into one), all passing. ghostlight lib: 622, untouched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
322bdf69c6
commit
f1732c0ae5
3 changed files with 87 additions and 229 deletions
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
use crate::mesh::{COMMAND_BOUNDARY, COMMAND_RESULT_SCHEMA, PROVIDER_ID, SURFACE_ID};
|
||||
use crate::play::PlayTurnView;
|
||||
use ghostlight::{JurisdictionKey, OperatorEvent, WorldPhase, WorldSnapshot};
|
||||
use ghostlight::{JurisdictionKey, WorldPhase, WorldSnapshot};
|
||||
use anyhow::{Context, bail};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
|
@ -198,17 +198,16 @@ fn jurisdiction_label(world: &WorldSnapshot, jurisdiction: JurisdictionKey) -> S
|
|||
}
|
||||
}
|
||||
|
||||
/// `operator_log` and `play` both arrive beside the snapshot rather than
|
||||
/// inside it: the story feed is the human operator's surface, and no
|
||||
/// subject-facing lane may reach an unscoped event log. Cut 9's own play card
|
||||
/// below reads `play` alone, never `operator_log` — invariant 8's projection,
|
||||
/// question, and refusal, nothing else. `operator_log` still feeds
|
||||
/// `world.story`/`world.speak` below; Cut 9's own deletion commit removes
|
||||
/// this parameter along with them.
|
||||
/// `play` arrives beside the snapshot rather than inside it: `WorldSnapshot`
|
||||
/// carries no play-turn state of its own. Cut 9 deleted the speak-affordance
|
||||
/// operation, the story card, and this function's own operator-feed
|
||||
/// parameter together — the play card below reads `play` alone, never the
|
||||
/// operator's own unscoped log of every committed event (still a library
|
||||
/// read, just no longer one this function takes): invariant 8's projection,
|
||||
/// question, and refusal, nothing else.
|
||||
pub(crate) fn authenticated_surface(
|
||||
account: &str,
|
||||
snapshot: Option<&WorldSnapshot>,
|
||||
operator_log: &[OperatorEvent],
|
||||
play: Option<&PlayTurnView>,
|
||||
) -> anyhow::Result<Value> {
|
||||
let version = authenticated_surface_version(snapshot, play);
|
||||
|
|
@ -407,79 +406,13 @@ pub(crate) fn authenticated_surface(
|
|||
"WorldMailbox",
|
||||
));
|
||||
}
|
||||
let story = operator_log
|
||||
.iter()
|
||||
.filter_map(|event| {
|
||||
event.speech.as_ref().map(|text| {
|
||||
json!({
|
||||
"id":format!("world.event.{}", event.revision),
|
||||
"kind":"text",
|
||||
"props":{"value":text.as_str()},
|
||||
"children":[]
|
||||
})
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
children.push(json!({
|
||||
"id":"world.story",
|
||||
"kind":"card",
|
||||
"props":{"title":"Story"},
|
||||
"children":story
|
||||
}));
|
||||
|
||||
let principal = ghostlight::PrincipalId::new(account);
|
||||
let opportunity = world.opportunities.iter().find_map(|opportunity| {
|
||||
world.subjects.iter().find_map(|subject| {
|
||||
// The catalog entry is found by kind name among the
|
||||
// subject's granted entries: the control is a
|
||||
// projection of state, not a second vocabulary.
|
||||
let affordance_id = world
|
||||
.affordances
|
||||
.iter()
|
||||
.find(|entry| {
|
||||
entry.entry.kind.0 == "speak"
|
||||
&& subject.affordances.contains(&entry.id)
|
||||
})
|
||||
.map(|entry| entry.id)?;
|
||||
(subject.id == opportunity.scope.subject_id
|
||||
&& subject.human_controller.as_ref() == Some(&principal)
|
||||
&& opportunity.affordance_ids.contains(&affordance_id))
|
||||
.then_some((opportunity, affordance_id))
|
||||
})
|
||||
});
|
||||
if let Some((opportunity, affordance_id)) = opportunity {
|
||||
children.extend([
|
||||
json!({
|
||||
"id":"world.speak.text",
|
||||
"kind":"control.input.textarea",
|
||||
"props":{"label":"Say something","rows":3,"placeholder":"Your exact words"},
|
||||
"stateBindings":[local_draft("text", "string")],
|
||||
"children":[]
|
||||
}),
|
||||
command_button(
|
||||
"world.speak",
|
||||
"Speak",
|
||||
"world.speak",
|
||||
json!({"opportunity":opportunity,"affordance_id":affordance_id}),
|
||||
&["text"],
|
||||
),
|
||||
]);
|
||||
commands.push(command_descriptor(
|
||||
"world.speak",
|
||||
"ghostlight.world_speak.v0",
|
||||
&["text", "opportunity", "affordance_id"],
|
||||
"WorldMailbox",
|
||||
));
|
||||
}
|
||||
|
||||
// Cut 9: the play card. Invariant 8 — the player sees a
|
||||
// projection, the question, and the refusal of their own
|
||||
// act, nothing else — so this reads `play` alone, never
|
||||
// `operator_log`: no id, no other subject's state, no
|
||||
// speech the player did not perceive. Always rendered in
|
||||
// Active (matching `world.story`'s own always-rendered
|
||||
// shape above), with empty rows before any turn has ever
|
||||
// opened.
|
||||
// the operator's own unscoped event log: no id, no other
|
||||
// subject's state, no speech the player did not perceive.
|
||||
// Always rendered in Active, with empty rows before any
|
||||
// turn has ever opened.
|
||||
let mut play_rows = Vec::new();
|
||||
if let Some(narration) = play.and_then(|view| view.narration.as_deref()) {
|
||||
play_rows.push(json!({
|
||||
|
|
@ -636,7 +569,6 @@ pub(crate) fn operation_schema(operation: &str) -> Option<&'static str> {
|
|||
"world.activate" => "ghostlight.world_activate.v0",
|
||||
"world.advance_time" => "ghostlight.world_advance_time.v0",
|
||||
"world.seed" => "ghostlight.world_seed.v1",
|
||||
"world.speak" => "ghostlight.world_speak.v0",
|
||||
"world.play" => "ghostlight.world_play.v0",
|
||||
_ => return None,
|
||||
})
|
||||
|
|
@ -761,7 +693,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn empty_authenticated_surface_has_create_without_session_zero() {
|
||||
let surface = authenticated_surface("sha256:owner", None, &[], None).unwrap();
|
||||
let surface = authenticated_surface("sha256:owner", None, None).unwrap();
|
||||
let encoded = serde_json::to_string(&surface).unwrap();
|
||||
assert!(encoded.contains("world.create"));
|
||||
assert!(encoded.contains("narrative_persona_label"));
|
||||
|
|
@ -792,38 +724,15 @@ mod tests {
|
|||
assert!(decoded.iter().all(|(_, weight)| weight == 1));
|
||||
}
|
||||
|
||||
/// `world.play`'s own operation id resolves to its schema through
|
||||
/// `operation_schema`, so an invocation whose payload matches it
|
||||
/// validates, and a payload that tries to claim authority is refused —
|
||||
/// the same claim this test once made for the now-deleted speak
|
||||
/// operation, PA.f146's own precedent (Cut 8b's schema existed here
|
||||
/// before Cut 9 published `world.play`'s own descriptor and button,
|
||||
/// below).
|
||||
#[test]
|
||||
fn payload_cannot_claim_authority() {
|
||||
let forged = invocation(
|
||||
"world.speak",
|
||||
"ghostlight.world_speak.v0",
|
||||
json!({"text":"hello","caller":"owner"}),
|
||||
);
|
||||
assert!(validate_invocation(&forged, "https-json").is_err());
|
||||
let bound = invocation(
|
||||
"world.speak",
|
||||
"ghostlight.world_speak.v0",
|
||||
json!({"text":"hello","opportunity":{},"affordance_id":"bound"}),
|
||||
);
|
||||
assert!(validate_invocation(&bound, "https-json").is_ok());
|
||||
}
|
||||
|
||||
/// Cut 8b's own schema test, beside `world.speak`'s: `world.play`'s
|
||||
/// operation id resolves to its schema through `operation_schema`, so an
|
||||
/// invocation whose payload matches it validates, and a payload that
|
||||
/// tries to claim authority is refused the same way
|
||||
/// `payload_cannot_claim_authority` refuses one for `world.speak`.
|
||||
///
|
||||
/// PA.f146: this is *not* the same claim as `world.speak`'s own —
|
||||
/// `world.speak` also has a `command_descriptor` pushed into the Eve
|
||||
/// command surface (`operation_schema`'s only caller besides this
|
||||
/// validation path); `world.play` does not, anywhere, so it advertises
|
||||
/// no button and no descriptor at all yet. `operation_schema` alone lets
|
||||
/// a caller who already knows to call `world.play` validate and reach
|
||||
/// `dispatch_world`; it does not make the operation discoverable. Cut 9
|
||||
/// is what publishes the descriptor.
|
||||
#[test]
|
||||
fn world_play_operation_schema_validates_though_not_yet_advertised() {
|
||||
assert_eq!(operation_schema("world.play"), Some("ghostlight.world_play.v0"));
|
||||
|
||||
let bound = invocation(
|
||||
|
|
@ -844,9 +753,9 @@ mod tests {
|
|||
#[test]
|
||||
fn invocation_metadata_rejects_legacy_authority_extensions() {
|
||||
let exact = invocation(
|
||||
"world.speak",
|
||||
"ghostlight.world_speak.v0",
|
||||
json!({"text":"hello","opportunity":{},"affordance_id":"bound"}),
|
||||
"world.play",
|
||||
"ghostlight.world_play.v0",
|
||||
json!({"text":"I look around.", "answers": null}),
|
||||
);
|
||||
let mut outer = serde_json::to_value(&exact).unwrap();
|
||||
outer["callerId"] = json!("legacy-owner");
|
||||
|
|
@ -862,5 +771,6 @@ mod tests {
|
|||
assert!(operation_schema("session_zero.begin").is_none());
|
||||
assert!(operation_schema("world.assess").is_none());
|
||||
assert!(operation_schema("governance.time.propose").is_none());
|
||||
assert!(operation_schema("world.speak").is_none());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2813,17 +2813,15 @@ mod tests {
|
|||
}
|
||||
|
||||
/// Cut 9's own card test: the player's own already-committed speech is
|
||||
/// durable in `state.world.operator_log()` — the operator's surface, per
|
||||
/// its own doc comment, which "no subject-facing lane may reach" — while
|
||||
/// the turn's own narration is a *different*, player-facing channel the
|
||||
/// play table produces. `eve::authenticated_surface`'s play card must
|
||||
/// show the narration and never the operator log's own text, proving
|
||||
/// invariant 8's "nothing else": the player sees a projection, the
|
||||
/// question, and the refusal of their own act, never an unscoped event
|
||||
/// log. Mutation M9.1: render `operator_log` into the card (as
|
||||
/// `world.story` still does elsewhere on this same surface, pending Cut
|
||||
/// 9's own deletion commit) — this test must fail if the *play card*
|
||||
/// itself ever does the same.
|
||||
/// durable in the world owner's own unscoped operator log — the
|
||||
/// operator's surface, per that library method's own doc comment, which
|
||||
/// "no subject-facing lane may reach" — while the turn's own narration is
|
||||
/// a *different*, player-facing channel the play table produces.
|
||||
/// `eve::authenticated_surface`'s play card must show the narration and
|
||||
/// never the operator feed's own text, proving invariant 8's "nothing
|
||||
/// else": the player sees a projection, the question, and the refusal of
|
||||
/// their own act, never an unscoped event log. Mutation M9.1: render that
|
||||
/// feed into the card — this test must fail if the play card ever does.
|
||||
#[tokio::test]
|
||||
async fn the_play_surface_shows_only_the_projection() {
|
||||
let fixture = play_world(None, "player-projection-only").await;
|
||||
|
|
@ -2872,8 +2870,8 @@ mod tests {
|
|||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let operator_log = fixture.world.operator_log().await.unwrap();
|
||||
assert_eq!(operator_log.len(), 1, "the committed speech must be durable in the operator log");
|
||||
let durable_log = fixture.world.operator_log().await.unwrap();
|
||||
assert_eq!(durable_log.len(), 1, "the committed speech must be durable in the world owner's own feed");
|
||||
|
||||
let end = output("r0", vec![call_event("c0", END_TURN_TOOL, serde_json::json!({}))]);
|
||||
let personas = PersonaLane::new(
|
||||
|
|
@ -2901,19 +2899,13 @@ mod tests {
|
|||
assert_eq!(view.narration.as_deref(), Some("The room settles into quiet."));
|
||||
|
||||
let world = fixture.world.snapshot().await.unwrap();
|
||||
// The *real* operator log, with the leaking text durably in it, so a
|
||||
// mutation that renders `operator_log` into the play card genuinely
|
||||
// has something to leak here — `world.story` (pending Cut 9's own
|
||||
// deletion commit) legitimately still renders this same text
|
||||
// elsewhere on the surface, so the negative assertion below is
|
||||
// scoped to the play card's own subtree, not the whole page.
|
||||
let surface = crate::eve::authenticated_surface(
|
||||
"player-projection-only",
|
||||
Some(&world),
|
||||
&operator_log,
|
||||
Some(&view),
|
||||
)
|
||||
.unwrap();
|
||||
// `authenticated_surface` takes no operator feed at all any more
|
||||
// (Cut 9's own deletion commit removed that parameter along with the
|
||||
// card that once read it) — the leaking text above is durable and
|
||||
// real, and this call has structurally no way to reach it.
|
||||
let surface =
|
||||
crate::eve::authenticated_surface("player-projection-only", Some(&world), Some(&view))
|
||||
.unwrap();
|
||||
let play_card = find_surface_node(&surface, "world.play.card").expect("the play card");
|
||||
let play_card_encoded = serde_json::to_string(play_card).unwrap();
|
||||
assert!(
|
||||
|
|
@ -2994,7 +2986,6 @@ mod tests {
|
|||
let surface_before = crate::eve::authenticated_surface(
|
||||
"player-card-question",
|
||||
Some(&world_before),
|
||||
&[],
|
||||
Some(&asked_view),
|
||||
)
|
||||
.unwrap();
|
||||
|
|
@ -3047,7 +3038,6 @@ mod tests {
|
|||
let surface_after = crate::eve::authenticated_surface(
|
||||
"player-card-question",
|
||||
Some(&world_after),
|
||||
&[],
|
||||
Some(&closed_view),
|
||||
)
|
||||
.unwrap();
|
||||
|
|
|
|||
|
|
@ -12,12 +12,12 @@ use crate::{
|
|||
play::{PlayRequest, PlayTable, PlayTurnView, QuestionId},
|
||||
};
|
||||
use ghostlight::{
|
||||
AffordanceId, CONSUMER_BODY_LIMIT, CommandBody, CommandId, ConnectorBinding, ConsumerPort,
|
||||
CONSUMER_BODY_LIMIT, CommandBody, CommandId, ConnectorBinding, ConsumerPort,
|
||||
ConsumerRegistry, ControllerModels, ControllerPort, ControllerRunner, ControllerWorkCustody,
|
||||
CreateJurisdictionIntent, CreateWorldIntent, DEFAULT_LOCAL_MODEL_PREFIX,
|
||||
DEFAULT_SDK_MODEL_PREFIX, DecisionInvocation, DecisionOpportunity, KernelError, Lens,
|
||||
DEFAULT_SDK_MODEL_PREFIX, KernelError, Lens,
|
||||
LensWeights, LocalBinding, MailboxError, PersonaLane, PrincipalCommandIntent, PrincipalId,
|
||||
SdkBinding, SeedOutcome, SeedPort, Statement, SubjectKind, SubmitReceipt, TickMinutes,
|
||||
SdkBinding, SeedOutcome, SeedPort, SubjectKind, SubmitReceipt, TickMinutes,
|
||||
VaultEvidenceSource, VerifiedPrincipalEvidence, WorldMailbox, WorldPhase, WorldSnapshot,
|
||||
open_controller_work, open_inference,
|
||||
};
|
||||
|
|
@ -166,14 +166,6 @@ fn is_handle_shape(value: &str) -> bool {
|
|||
&& chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '-')
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct SpeakPayload {
|
||||
text: String,
|
||||
opportunity: DecisionOpportunity,
|
||||
affordance_id: AffordanceId,
|
||||
}
|
||||
|
||||
/// `world.play`'s own payload (Cut 8b): the player's prose, plus `answers`
|
||||
/// naming the question this request answers (PA.f84) — `None` for the plain
|
||||
/// opening/continue shape. Follows `ghostlight.world_speak.v0`'s own field
|
||||
|
|
@ -694,16 +686,13 @@ async fn eve_surface(
|
|||
return Json(eve::anonymous_surface()).into_response();
|
||||
};
|
||||
let play_view = current_play_view(&state).await;
|
||||
match current_operator_view(&state)
|
||||
.await
|
||||
.and_then(|(snapshot, log)| {
|
||||
eve::authenticated_surface(
|
||||
principal.account_subject_hash(),
|
||||
snapshot.as_ref(),
|
||||
&log,
|
||||
play_view.as_ref(),
|
||||
)
|
||||
}) {
|
||||
match current_world(&state).await.and_then(|snapshot| {
|
||||
eve::authenticated_surface(
|
||||
principal.account_subject_hash(),
|
||||
snapshot.as_ref(),
|
||||
play_view.as_ref(),
|
||||
)
|
||||
}) {
|
||||
Ok(surface) => Json(surface).into_response(),
|
||||
Err(error) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
|
|
@ -1214,25 +1203,6 @@ async fn execute_world(
|
|||
.map_err(|error| RuntimeCommandError::Payload(error.to_string()))?;
|
||||
CommandBody::ActivateWorld
|
||||
}
|
||||
"world.speak" => {
|
||||
let payload: SpeakPayload = serde_json::from_value(invocation.payload.clone())
|
||||
.map_err(|error| RuntimeCommandError::Payload(error.to_string()))?;
|
||||
// `world.speak` stays an ingress for the Speak entry specifically.
|
||||
// A generic `world.act` is only useful beside per-affordance Eve
|
||||
// controls derived from the catalog, and that is a projection pass.
|
||||
CommandBody::ExerciseDecision {
|
||||
opportunity: payload.opportunity,
|
||||
invocation: DecisionInvocation {
|
||||
affordance: payload.affordance_id,
|
||||
bindings: Vec::new(),
|
||||
proposed: Vec::new(),
|
||||
speech: Some(Statement::new(payload.text).ok_or_else(|| {
|
||||
RuntimeCommandError::Payload("spoken text is not canonical".into())
|
||||
})?),
|
||||
display: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
"world.advance_time" => {
|
||||
let payload: AdvanceTimePayload = serde_json::from_value(invocation.payload.clone())
|
||||
.map_err(|error| RuntimeCommandError::Payload(error.to_string()))?;
|
||||
|
|
@ -1328,21 +1298,6 @@ async fn current_play_view(state: &AppState) -> Option<PlayTurnView> {
|
|||
state.play.as_ref()?.current_turn_view().await
|
||||
}
|
||||
|
||||
/// The two halves of the operator surface, fetched together: the projection of
|
||||
/// world state, and the story feed the human reads. The feed is deliberately not
|
||||
/// a snapshot field, so no controller lane can reach it.
|
||||
async fn current_operator_view(
|
||||
state: &AppState,
|
||||
) -> anyhow::Result<(Option<WorldSnapshot>, Vec<ghostlight::OperatorEvent>)> {
|
||||
let snapshot = current_world(state).await?;
|
||||
let log = match state.world.operator_log().await {
|
||||
Ok(log) => log,
|
||||
Err(MailboxError::Kernel(KernelError::WorldNotCreated)) => Vec::new(),
|
||||
Err(error) => return Err(error.into()),
|
||||
};
|
||||
Ok((snapshot, log))
|
||||
}
|
||||
|
||||
async fn publish_projection(state: &AppState) -> anyhow::Result<u64> {
|
||||
let snapshot = current_world(state).await?;
|
||||
let version = eve::surface_version(snapshot.as_ref());
|
||||
|
|
@ -2341,36 +2296,40 @@ mod tests {
|
|||
.await;
|
||||
assert_eq!(activated["sourceVersion"], 3);
|
||||
|
||||
let world = current_world(&fixture.state).await.unwrap().unwrap();
|
||||
let opportunity = world.opportunities[0].clone();
|
||||
let affordance = *world
|
||||
.affordances
|
||||
.iter()
|
||||
.find(|entry| {
|
||||
entry.entry.kind.0 == "speak" && world.subjects[0].affordances.contains(&entry.id)
|
||||
})
|
||||
.map(|entry| &entry.id)
|
||||
.unwrap();
|
||||
let spoken = post(
|
||||
let played = post(
|
||||
&fixture.state,
|
||||
&fixture.cookie,
|
||||
invocation(
|
||||
"world.speak",
|
||||
"ghostlight.world_speak.v0",
|
||||
"world.play",
|
||||
"ghostlight.world_play.v0",
|
||||
3,
|
||||
json!({
|
||||
"text":"The new owner speaks.",
|
||||
"opportunity":opportunity,
|
||||
"affordance_id":affordance
|
||||
}),
|
||||
json!({"text":"The new owner looks around."}),
|
||||
&uuid::Uuid::new_v4().to_string(),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(spoken["sourceVersion"], 4);
|
||||
let (world, log) = current_operator_view(&fixture.state).await.unwrap();
|
||||
assert!(world.is_some());
|
||||
assert_eq!(log.len(), 1);
|
||||
assert_eq!(played["state"], "accepted");
|
||||
|
||||
// PA.f138: this leg observes the play table itself, not merely the
|
||||
// accepted response — a route that spawned the task and handed back
|
||||
// the same JSON without ever calling `table.run` would still pass
|
||||
// the assertion above, so `world_play_reaches_the_play_table_and_is_
|
||||
// accepted`'s own poll is what actually proves the turn opened.
|
||||
let table = fixture.state.play.clone().unwrap();
|
||||
let observed = tokio::time::timeout(Duration::from_secs(5), async move {
|
||||
loop {
|
||||
if table.current_turn_view().await.is_some() {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
})
|
||||
.await;
|
||||
assert!(
|
||||
observed.is_ok(),
|
||||
"world.play must actually reach PlayTable::run, recording a turn row, not just return \
|
||||
the same accepted JSON on its own"
|
||||
);
|
||||
}
|
||||
|
||||
/// Cut 8b's own routing test: `world.play` reaches the play table and
|
||||
|
|
@ -3075,7 +3034,7 @@ mod tests {
|
|||
.account_subject_hash()
|
||||
.to_owned();
|
||||
let stranger = "someone-else";
|
||||
let mut surfaces = vec![eve::authenticated_surface(&owner, None, &[], None).unwrap()];
|
||||
let mut surfaces = vec![eve::authenticated_surface(&owner, None, None).unwrap()];
|
||||
two_cell_world(
|
||||
&fixture.state,
|
||||
&fixture.cookie,
|
||||
|
|
@ -3090,7 +3049,7 @@ mod tests {
|
|||
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), &[], None).unwrap());
|
||||
surfaces.push(eve::authenticated_surface(account, Some(&draft), None).unwrap());
|
||||
}
|
||||
for body in [CommandBody::ApproveDraft, CommandBody::ActivateWorld] {
|
||||
let snapshot = fixture.state.world.snapshot().await.unwrap();
|
||||
|
|
@ -3122,12 +3081,13 @@ mod tests {
|
|||
let play_view = current_play_view(&fixture.state).await;
|
||||
for account in [owner.as_str(), stranger] {
|
||||
surfaces.push(
|
||||
eve::authenticated_surface(account, Some(&active), &[], play_view.as_ref()).unwrap(),
|
||||
eve::authenticated_surface(account, Some(&active), play_view.as_ref()).unwrap(),
|
||||
);
|
||||
}
|
||||
surfaces.push(eve::anonymous_surface());
|
||||
|
||||
let mut seen = 0usize;
|
||||
let mut saw_world_play = false;
|
||||
for surface in &surfaces {
|
||||
let mut buttons = Vec::new();
|
||||
surface_buttons(&surface["surface"]["root"], &mut buttons);
|
||||
|
|
@ -3137,6 +3097,7 @@ mod tests {
|
|||
eve::operation_schema(command).is_some(),
|
||||
"the panel emits {command}, which Ghostlight does not advertise"
|
||||
);
|
||||
saw_world_play |= command == "world.play";
|
||||
seen += 1;
|
||||
}
|
||||
for descriptor in surface["commands"].as_array().unwrap() {
|
||||
|
|
@ -3149,6 +3110,9 @@ mod tests {
|
|||
}
|
||||
}
|
||||
assert!(seen > 5, "the walk found almost nothing to check");
|
||||
// Cut 9: the play card's own button is now part of this walk — it
|
||||
// was not, before Cut 9 published its descriptor (PA.f146).
|
||||
assert!(saw_world_play, "the panel must emit world.play in Active");
|
||||
}
|
||||
|
||||
/// Spec test 12. Seeding is the owner's lane and Draft's lane, and both
|
||||
|
|
@ -3277,13 +3241,7 @@ mod tests {
|
|||
)
|
||||
.await;
|
||||
let draft = fixture.state.world.snapshot().await.unwrap();
|
||||
let surface = eve::authenticated_surface(
|
||||
&owner,
|
||||
Some(&draft),
|
||||
&[],
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let surface = eve::authenticated_surface(&owner, Some(&draft), None).unwrap();
|
||||
let encoded = serde_json::to_string(&surface).unwrap();
|
||||
let card = surface["surface"]["root"]["children"]
|
||||
.as_array()
|
||||
|
|
|
|||
Loading…
Reference in a new issue