use super::*;
use bevy_app::{App, Update};
use bevy_ecs::component::Component;
use bevy_ecs::event::Event;
use bevy_ecs::observer::On;
use bevy_ecs::resource::Resource;
use bevy_ecs::system::{In, ResMut, RunSystemOnce as _};
use brink_format::{CallAtom, CapabilityParam, DefinitionTag, DirectEffects, DispatchEntry};
use brink_runtime::ContextAccess as _;
use serde::{Deserialize, Serialize};
use crate::advance_batch;
use crate::asset::{BrinkStoryAsset, LineTablesAsset};
use crate::bindings::BrinkBindingsAppExt as _;
use crate::capability::{
BrinkCapabilityAppExt, CapabilityChanges, CapabilityEffects, CapabilityManifest,
CapabilityManifestExternal, CapabilityRegistry, ContainerAccess,
};
use crate::event::{BrinkLineDelivered, BrinkStoryEnded, BrinkTurnDone};
use crate::globals::BrinkGlobals;
use crate::handle::{BrinkHandleAppExt as _, HandleKind};
use crate::test_support::{add_story_assets, compile_test_story, make_test_app};
use crate::{BrinkBatchReport, BrinkFlowRequest};
fn call_atom(name: brink_format::NameId) -> CallAtom {
CallAtom {
name,
capability: CapabilityParam::Any,
handle_param: None,
}
}
#[derive(Resource, Default)]
struct TextLog(String);
#[derive(Component)]
struct GameStateCap;
#[derive(Component)]
struct Gate {
open: bool,
}
fn is_gate_open(
In((flow, _args)): In<crate::BrinkQueryInput>,
gates: bevy_ecs::system::Query<&Gate>,
) -> Value {
Value::Bool(gates.get(flow).is_ok_and(|gate| gate.open))
}
const GATED_GATE_STORY: &str = "EXTERNAL is_gate_open(id)\n\
Woke up!\n-> END\n\
=== function should_wake() ===\n~ return is_gate_open(0)\n";
const GATED_STORY: &str = "VAR gate = 0\n\
Woke up!\n-> DONE\n\
Second turn.\n-> END\n\
=== function should_wake() ===\n~ return gate\n";
const LOOPING_STORY: &str = "VAR gate = 1\n\
-> beat\n\
=== beat ===\n\
Beat.\n-> DONE\n-> beat\n\
=== function should_wake() ===\n~ return gate\n";
const LATCH_STORY: &str = "VAR gate = 0\n\
-> beat\n\
=== beat ===\n\
Beat.\n-> DONE\n-> beat\n\
=== function should_wake() ===\n~ return gate\n";
const GATED_STORY_WITH_EXTERNALS: &str = "VAR gate = 0\n\
EXTERNAL touch_state(id)\n\
EXTERNAL read_state(id)\n\
Woke up!\n-> DONE\n\
Second turn.\n-> END\n\
=== function should_wake() ===\n~ return gate\n\
=== function uses_externals() ===\n\
~ temp a = touch_state(0)\n\
~ temp b = read_state(0)\n\
~ return 0\n";
struct SleepProbeKind;
#[derive(Clone, Serialize, Deserialize)]
struct SleepProbeSaveKey;
impl HandleKind for SleepProbeKind {
const KIND: &'static str = "SleepProbe";
type Resource = ();
type SaveKey = SleepProbeSaveKey;
fn save_key(&self, _world: &EcsWorld, _res: &Self::Resource) -> Option<Self::SaveKey> {
Some(SleepProbeSaveKey)
}
fn resolve(&self, _world: &mut EcsWorld, _key: &Self::SaveKey) -> Option<Self::Resource> {
Some(())
}
}
fn build_app() -> App {
let mut app = make_test_app();
app.add_systems(Update, advance_batch::<()>);
app.insert_resource(TextLog::default());
app.add_observer(|t: On<BrinkLineDelivered<()>>, mut log: ResMut<TextLog>| {
log.0.push_str(&t.event().text);
});
app.add_observer(|t: On<BrinkTurnDone<()>>, mut log: ResMut<TextLog>| {
log.0.push_str(&t.event().text);
});
app.add_observer(|t: On<BrinkStoryEnded<()>>, mut log: ResMut<TextLog>| {
log.0.push_str(&t.event().text);
});
app
}
fn build_app_parallel() -> App {
let mut app = make_test_app();
app.add_systems(Update, crate::advance_batch_parallel::<()>);
app.insert_resource(TextLog::default());
app.add_observer(|t: On<BrinkLineDelivered<()>>, mut log: ResMut<TextLog>| {
log.0.push_str(&t.event().text);
});
app.add_observer(|t: On<BrinkTurnDone<()>>, mut log: ResMut<TextLog>| {
log.0.push_str(&t.event().text);
});
app.add_observer(|t: On<BrinkStoryEnded<()>>, mut log: ResMut<TextLog>| {
log.0.push_str(&t.event().text);
});
app
}
fn set_gate(app: &mut App, program_global_idx: u32, value: i32) {
app.world_mut()
.resource_mut::<BrinkGlobals<()>>()
.inner
.set_global(program_global_idx, Value::Int(value));
}
fn pump(app: &mut App, frames: usize) {
for _ in 0..frames {
app.update();
}
}
#[test]
fn dormant_flow_stays_parked_until_condition_true() {
let mut app = build_app();
let (program, tables, ctx) = compile_test_story(GATED_STORY);
let gate_idx = program.global_index("gate").expect("gate global exists");
let story = add_story_assets(&mut app, program, tables, ctx);
app.world_mut().spawn((
BrinkFlowRequest::<()>::builder().story(story).build(),
FlowSleep::<()>::persistent("should_wake").dormant(),
));
pump(&mut app, 6);
assert!(
app.world().resource::<TextLog>().0.is_empty(),
"a dormant flow whose condition is false must never run: got {:?}",
app.world().resource::<TextLog>().0
);
let sleep = single_sleep(&mut app);
assert_eq!(sleep, SleepState::Parked, "still parked while gate == 0");
set_gate(&mut app, gate_idx, 1);
pump(&mut app, 6);
assert!(
app.world().resource::<TextLog>().0.contains("Woke up!"),
"flow should wake and run its first turn once gate != 0: got {:?}",
app.world().resource::<TextLog>().0
);
}
#[test]
fn advance_batch_parallel_dormant_flow_stays_parked_until_condition_true() {
let mut app = build_app_parallel();
let (program, tables, ctx) = compile_test_story(GATED_STORY);
let gate_idx = program.global_index("gate").expect("gate global exists");
let story = add_story_assets(&mut app, program, tables, ctx);
app.world_mut().spawn((
BrinkFlowRequest::<()>::builder().story(story).build(),
FlowSleep::<()>::persistent("should_wake").dormant(),
));
pump(&mut app, 6);
assert!(
app.world().resource::<TextLog>().0.is_empty(),
"a dormant flow whose condition is false must never run under the parallel \
driver either — Collect must skip it, not just the serial driver's Collect: \
got {:?}",
app.world().resource::<TextLog>().0
);
let sleep = single_sleep(&mut app);
assert_eq!(sleep, SleepState::Parked, "still parked while gate == 0");
set_gate(&mut app, gate_idx, 1);
pump(&mut app, 6);
assert!(
app.world().resource::<TextLog>().0.contains("Woke up!"),
"flow should wake and run its first turn once gate != 0: got {:?}",
app.world().resource::<TextLog>().0
);
}
#[test]
fn persistent_policy_reparks_when_condition_goes_false() {
let mut app = build_app();
let (program, tables, ctx) = compile_test_story(LOOPING_STORY);
let gate_idx = program.global_index("gate").expect("gate global exists");
let story = add_story_assets(&mut app, program, tables, ctx);
app.world_mut().spawn((
BrinkFlowRequest::<()>::builder().story(story).build(),
FlowSleep::<()>::persistent("should_wake").with_detect(DetectSummary::from_bits(
[("Poll".to_string(), false)].into_iter().collect(),
)),
));
pump(&mut app, 8);
let beats_running = app.world().resource::<TextLog>().0.matches("Beat.").count();
assert!(
beats_running >= 2,
"a persistent always-true policy should re-arm and run repeatedly; got {beats_running} beats"
);
set_gate(&mut app, gate_idx, 0);
pump(&mut app, 4); let after_clear = app.world().resource::<TextLog>().0.matches("Beat.").count();
pump(&mut app, 6);
let final_count = app.world().resource::<TextLog>().0.matches("Beat.").count();
assert_eq!(
final_count, after_clear,
"once the condition is false the flow must stop running (parked, zero cost)"
);
assert_eq!(single_sleep(&mut app), SleepState::Parked);
}
#[test]
fn latch_wakes_on_each_transition_and_cycles_across_multiple_flips() {
let mut app = build_app();
let (program, tables, ctx) = compile_test_story(LATCH_STORY);
let gate_idx = program.global_index("gate").expect("gate global exists");
let story = add_story_assets(&mut app, program, tables, ctx);
app.world_mut().spawn((
BrinkFlowRequest::<()>::builder().story(story).build(),
FlowSleep::<()>::latch("should_wake")
.dormant()
.with_detect(DetectSummary::from_bits(
[("Poll".to_string(), false)].into_iter().collect(),
)),
));
pump(&mut app, 4);
assert!(
app.world().resource::<TextLog>().0.is_empty(),
"must stay parked while the condition is false: got {:?}",
app.world().resource::<TextLog>().0
);
set_gate(&mut app, gate_idx, 1);
pump(&mut app, 8);
let after_first_rise = app.world().resource::<TextLog>().0.matches("Beat.").count();
assert_eq!(
after_first_rise, 1,
"a Latch policy must fire exactly once per edge, not re-step while \
the condition stays true"
);
pump(&mut app, 6); assert_eq!(
app.world().resource::<TextLog>().0.matches("Beat.").count(),
1,
"must not re-fire again while the condition is still true (waiting for the fall)"
);
set_gate(&mut app, gate_idx, 0);
pump(&mut app, 8);
assert_eq!(
app.world().resource::<TextLog>().0.matches("Beat.").count(),
2,
"the falling edge must also fire, and the component must still be \
attached (Latch never retires, unlike Once)"
);
assert!(
{
let mut q = app.world_mut().query::<&FlowSleep<()>>();
q.iter(app.world()).next().is_some()
},
"a Latch policy must never be removed"
);
for expected_count in 3..=6 {
let on = expected_count % 2 == 1;
set_gate(&mut app, gate_idx, i32::from(on));
pump(&mut app, 8);
assert_eq!(
app.world().resource::<TextLog>().0.matches("Beat.").count(),
expected_count,
"flip #{expected_count} (gate={on}) must fire exactly one more wake"
);
}
}
#[test]
fn wake_once_fires_once_then_retires() {
let mut app = build_app();
let (program, tables, ctx) = compile_test_story(GATED_STORY);
let gate_idx = program.global_index("gate").expect("gate global exists");
let story = add_story_assets(&mut app, program, tables, ctx);
let entity = app
.world_mut()
.spawn((
BrinkFlowRequest::<()>::builder().story(story).build(),
FlowSleep::<()>::once("should_wake").dormant(),
))
.id();
pump(&mut app, 4); set_gate(&mut app, gate_idx, 1);
pump(&mut app, 8);
assert!(
app.world().entity(entity).get::<FlowSleep<()>>().is_none(),
"a wake_once policy must be removed after firing"
);
assert!(
app.world().resource::<TextLog>().0.contains("Woke up!"),
"the one-shot turn should have run"
);
}
#[test]
fn cancel_resolves_condition_to_false_forever() {
let mut app = build_app();
let (program, tables, ctx) = compile_test_story(GATED_STORY);
let gate_idx = program.global_index("gate").expect("gate global exists");
let story = add_story_assets(&mut app, program, tables, ctx);
let entity = app
.world_mut()
.spawn((
BrinkFlowRequest::<()>::builder().story(story).build(),
FlowSleep::<()>::persistent("should_wake").dormant(),
))
.id();
pump(&mut app, 3);
app.world_mut()
.get_mut::<FlowSleep<()>>(entity)
.expect("policy present")
.cancel();
set_gate(&mut app, gate_idx, 1);
pump(&mut app, 8);
assert!(
app.world().resource::<TextLog>().0.is_empty(),
"a cancelled policy must never wake the flow, even once gate != 0: got {:?}",
app.world().resource::<TextLog>().0
);
assert_eq!(
app.world()
.entity(entity)
.get::<FlowSleep<()>>()
.expect("still attached")
.state(),
SleepState::Cancelled
);
}
#[test]
fn ended_flow_drops_its_policy() {
let mut app = build_app();
let (program, tables, ctx) = compile_test_story(
"VAR gate = 1\nDone here.\n-> END\n=== function should_wake() ===\n~ return gate\n",
);
let story = add_story_assets(&mut app, program, tables, ctx);
let entity = app
.world_mut()
.spawn((
BrinkFlowRequest::<()>::builder().story(story).build(),
FlowSleep::<()>::persistent("should_wake").dormant(),
))
.id();
pump(&mut app, 8);
assert!(
app.world().entity(entity).get::<FlowSleep<()>>().is_none(),
"an -> END flow's policy is inert and must be removed"
);
assert!(app.world().resource::<TextLog>().0.contains("Done here."));
}
#[test]
fn detect_summary_from_and_merged_container_polls_on_conflict() {
let access = ContainerAccess {
detect: [("Transform".to_string(), false)].into_iter().collect(),
..ContainerAccess::default()
};
let summary = DetectSummary::from_container_access(&access);
assert!(
!summary.all_detect_capable,
"a must-poll (false) merged bit must classify the policy as polling"
);
let sleep = FlowSleep::<()>::persistent("cond").with_detect(summary);
assert!(
!sleep.dependencies_all_detect_capable(),
"the policy inherits the must-poll verdict — it will re-evaluate every pass"
);
}
#[test]
fn detect_summary_all_true_or_empty_is_detect_capable() {
let all_true = DetectSummary::from_bits(
[
("Transform".to_string(), true),
("Health".to_string(), true),
]
.into_iter()
.collect(),
);
assert!(all_true.all_detect_capable);
let empty = DetectSummary::from_bits(std::collections::BTreeMap::new());
assert!(
empty.all_detect_capable,
"no external-capability dependency → change-detectable via the ink World"
);
}
#[test]
fn detect_summary_default_matches_vacuous_true_from_bits() {
assert_eq!(
DetectSummary::default(),
DetectSummary::from_bits(std::collections::BTreeMap::new()),
"DetectSummary::default() must agree with from_bits(empty): both vacuously \
all-detect-capable"
);
assert!(
DetectSummary::default().all_detect_capable,
"a policy built without .with_detect must default to the cheap \
(all-detect-capable) path, not must-poll"
);
let sleep = FlowSleep::<()>::persistent("cond");
assert!(
sleep.dependencies_all_detect_capable(),
"a freshly built FlowSleep with no .with_detect must be all-detect-capable"
);
}
#[test]
fn mark_wake_dirty_must_polls_an_unregistered_component_backed_policy() {
let mut app = App::new();
app.init_resource::<CapabilityRegistry<()>>();
app.init_resource::<CapabilityChanges<()>>();
let entity = app
.world_mut()
.spawn(
FlowSleep::<()>::persistent("should_wake")
.with_detect(DetectSummary::from_bits(
[("Transform".to_string(), true)].into_iter().collect(),
))
.dormant(),
)
.id();
assert!(
app.world()
.entity(entity)
.get::<FlowSleep<()>>()
.expect("just spawned")
.dependencies_all_detect_capable(),
"fixture sanity: the AND-merge verdict is all-true"
);
app.world_mut()
.get_mut::<FlowSleep<()>>(entity)
.expect("policy present")
.evaluated_once = true;
app.world_mut()
.run_system_once(mark_wake_dirty::<()>)
.expect("mark_wake_dirty runs");
assert!(
app.world()
.entity(entity)
.get::<FlowSleep<()>>()
.expect("still attached")
.needs_eval,
"a detect-capable policy naming an UNREGISTERED capability must be \
must-polled — mark_wake_dirty has no change-tracker to observe it and \
must not risk a missed wake"
);
}
#[test]
fn detect_capable_component_policy_is_not_reevaluated_while_unchanged() {
let mut app = make_test_app();
app.register_capability::<(), Gate>("Gate");
let gate = app.world_mut().spawn(Gate { open: false }).id();
let entity = app
.world_mut()
.spawn(
FlowSleep::<()>::persistent("should_wake")
.with_detect(DetectSummary::from_bits(
[("Gate".to_string(), true)].into_iter().collect(),
))
.dormant(),
)
.id();
app.world_mut()
.get_mut::<FlowSleep<()>>(entity)
.expect("policy present")
.evaluated_once = true;
pump(&mut app, 3);
app.world_mut()
.get_mut::<FlowSleep<()>>(entity)
.expect("policy present")
.needs_eval = false;
app.update();
assert!(
!app.world()
.entity(entity)
.get::<FlowSleep<()>>()
.expect("still attached")
.needs_eval,
"a detect-capable component-backed policy must NOT be flagged on a \
frame where its watched component did not change — the §12.5 cheap path"
);
app.world_mut().get_mut::<Gate>(gate).expect("gate").open = true;
app.update();
assert!(
app.world()
.entity(entity)
.get::<FlowSleep<()>>()
.expect("still attached")
.needs_eval,
"flipping the watched component must flag the policy for re-evaluation \
the same frame — the missed-wake class this issue closes"
);
}
#[test]
fn detect_capable_component_condition_wakes_on_component_change() {
let mut app = build_app();
app.register_capability::<(), Gate>("Gate");
app.bind_brink_query::<(), _, _>("is_gate_open", is_gate_open);
let (program, tables, ctx) = compile_test_story(GATED_GATE_STORY);
let story = add_story_assets(&mut app, program, tables, ctx);
let entity =
app.world_mut()
.spawn((
Gate { open: false },
BrinkFlowRequest::<()>::builder().story(story).build(),
FlowSleep::<()>::once("should_wake").dormant().with_detect(
DetectSummary::from_bits([("Gate".to_string(), true)].into_iter().collect()),
),
))
.id();
pump(&mut app, 6);
assert!(
app.world().resource::<TextLog>().0.is_empty(),
"a dormant component-backed flow must stay parked while its Gate is \
closed: got {:?}",
app.world().resource::<TextLog>().0
);
app.world_mut().get_mut::<Gate>(entity).expect("gate").open = true;
pump(&mut app, 8);
assert!(
app.world().resource::<TextLog>().0.contains("Woke up!"),
"the flow must wake and run once its watched Gate opens: got {:?}",
app.world().resource::<TextLog>().0
);
}
#[test]
fn condition_truthiness_matches_ink_coercion() {
assert!(is_condition_true(&Value::Bool(true)));
assert!(!is_condition_true(&Value::Bool(false)));
assert!(is_condition_true(&Value::Int(1)));
assert!(is_condition_true(&Value::Int(-1)));
assert!(!is_condition_true(&Value::Int(0)));
assert!(is_condition_true(&Value::Float(0.5)));
assert!(!is_condition_true(&Value::Float(0.0)));
assert!(!is_condition_true(&Value::Null));
}
#[test]
fn wake_fan_out_scenario_ratios() {
let mut app = build_app();
let (program, tables, ctx) = compile_test_story(GATED_STORY);
let story = add_story_assets(&mut app, program, tables, ctx);
let parked = 6usize;
let active = 2usize;
for _ in 0..parked {
app.world_mut().spawn((
BrinkFlowRequest::<()>::builder()
.story(story.clone())
.build(),
FlowSleep::<()>::persistent("should_wake").dormant(),
));
}
for _ in 0..active {
app.world_mut().spawn(
BrinkFlowRequest::<()>::builder()
.story(story.clone())
.build(),
);
}
app.update(); app.update();
let report = app.world().resource::<BrinkBatchReport<()>>();
assert_eq!(
report.stepped, active,
"exactly the active flows step; parked flows are skipped by Collect"
);
assert_eq!(report.awaiting, 0);
assert_eq!(report.errored, 0);
assert!(
report.flows.len() <= active,
"parked flows contribute no per-flow batch record: {} records for {active} active",
report.flows.len()
);
let mut storm_app = build_app();
let (sp, st, sc) = compile_test_story(LOOPING_STORY);
let storm_story = add_story_assets(&mut storm_app, sp, st, sc);
let storm_n = 5usize;
for _ in 0..storm_n {
storm_app.world_mut().spawn((
BrinkFlowRequest::<()>::builder()
.story(storm_story.clone())
.build(),
FlowSleep::<()>::persistent("should_wake").with_detect(DetectSummary::from_bits(
[("Poll".to_string(), false)].into_iter().collect(),
)),
));
}
let mut max_stepped = 0usize;
for _ in 0..12 {
storm_app.update();
max_stepped = max_stepped.max(storm_app.world().resource::<BrinkBatchReport<()>>().stepped);
}
assert_eq!(
max_stepped, storm_n,
"wake storm: all {storm_n} flows wake and step together in a batch turn"
);
}
fn add_story_assets_with_effect_rows(
app: &mut App,
program: Program,
tables: Vec<Vec<brink_format::LineEntry>>,
initial_context: brink_runtime::World,
effect_rows: Vec<EffectRowEntry>,
) -> bevy_asset::Handle<BrinkStoryAsset> {
let world = app.world_mut();
let program_handle = world
.resource_mut::<Assets<ProgramAsset>>()
.add(ProgramAsset {
program,
initial_context,
effect_rows,
});
let tables_handle = world
.resource_mut::<Assets<LineTablesAsset>>()
.add(LineTablesAsset { tables });
world
.resource_mut::<Assets<BrinkStoryAsset>>()
.add(BrinkStoryAsset {
program: program_handle,
line_tables: tables_handle,
})
}
fn no_write_row(def: DefinitionId) -> EffectRowEntry {
EffectRowEntry {
def,
is_entry: true,
direct: DirectEffects::default(),
dispatches: vec![],
}
}
fn writing_row(def: DefinitionId, write: DefinitionId) -> EffectRowEntry {
EffectRowEntry {
def,
is_entry: true,
direct: DirectEffects {
writes: vec![write],
..DirectEffects::default()
},
dispatches: vec![],
}
}
fn opaque_row(def: DefinitionId) -> EffectRowEntry {
EffectRowEntry {
def,
is_entry: true,
direct: DirectEffects {
opaque: true,
..DirectEffects::default()
},
dispatches: vec![],
}
}
#[test]
fn check_named_condition_purity_accepts_a_pure_row() {
let (program, _tables, _ctx) = compile_test_story(GATED_STORY);
let def = program
.definition_id_for_path("should_wake")
.expect("should_wake resolves");
let rows = vec![no_write_row(def)];
assert!(
check_named_condition_purity(
&program,
&rows,
&CapabilityManifest::default(),
None::<&BrinkBindings<()>>,
"should_wake"
)
.is_ok()
);
}
#[test]
fn check_named_condition_purity_rejects_a_writing_row() {
let (program, _tables, _ctx) = compile_test_story(GATED_STORY);
let def = program
.definition_id_for_path("should_wake")
.expect("should_wake resolves");
let write_id = DefinitionId::new(DefinitionTag::GlobalVar, 999);
let rows = vec![writing_row(def, write_id)];
let err = check_named_condition_purity(
&program,
&rows,
&CapabilityManifest::default(),
None::<&BrinkBindings<()>>,
"should_wake",
)
.unwrap_err();
assert!(
matches!(&err, WakeConditionPurityError::Writes { condition, .. } if condition == "should_wake"),
"got {err:?}"
);
}
#[test]
fn check_named_condition_purity_rejects_a_dispatch_fallback_write() {
let (program, _tables, _ctx) = compile_test_story(GATED_STORY);
let def = program
.definition_id_for_path("should_wake")
.expect("should_wake resolves");
let write_id = DefinitionId::new(DefinitionTag::GlobalVar, 999);
let dispatch_cell = DefinitionId::new(DefinitionTag::GlobalVar, 1000);
let rows = vec![EffectRowEntry {
def,
is_entry: true,
direct: DirectEffects::default(),
dispatches: vec![DispatchEntry {
cell: dispatch_cell,
narrowable: false,
fallback: DirectEffects {
writes: vec![write_id],
..DirectEffects::default()
},
}],
}];
let err = check_named_condition_purity(
&program,
&rows,
&CapabilityManifest::default(),
None::<&BrinkBindings<()>>,
"should_wake",
)
.unwrap_err();
assert!(
matches!(err, WakeConditionPurityError::Writes { .. }),
"got {err:?}"
);
}
#[test]
fn check_named_condition_purity_rejects_an_opaque_row() {
let (program, _tables, _ctx) = compile_test_story(GATED_STORY);
let def = program
.definition_id_for_path("should_wake")
.expect("should_wake resolves");
let rows = vec![opaque_row(def)];
let err = check_named_condition_purity(
&program,
&rows,
&CapabilityManifest::default(),
None::<&BrinkBindings<()>>,
"should_wake",
)
.unwrap_err();
assert!(
matches!(err, WakeConditionPurityError::Opaque { .. }),
"got {err:?}"
);
}
#[test]
fn check_named_condition_purity_unknown_condition_is_named() {
let (program, _tables, _ctx) = compile_test_story(GATED_STORY);
let def = program
.definition_id_for_path("should_wake")
.expect("should_wake resolves");
let rows = vec![no_write_row(def)];
let err = check_named_condition_purity(
&program,
&rows,
&CapabilityManifest::default(),
None::<&BrinkBindings<()>>,
"no_such_fn",
)
.unwrap_err();
assert!(
matches!(&err, WakeConditionPurityError::UnknownCondition { condition } if condition == "no_such_fn"),
"got {err:?}"
);
}
#[test]
fn check_named_condition_purity_bypasses_when_effect_rows_table_is_empty() {
let (program, _tables, _ctx) = compile_test_story(GATED_STORY);
assert!(
check_named_condition_purity(
&program,
&[],
&CapabilityManifest::default(),
None::<&BrinkBindings<()>>,
"should_wake"
)
.is_ok()
);
assert!(
check_named_condition_purity(
&program,
&[],
&CapabilityManifest::default(),
None::<&BrinkBindings<()>>,
"no_such_fn"
)
.is_ok()
);
}
fn calling_row(def: DefinitionId, call: CallAtom) -> EffectRowEntry {
EffectRowEntry {
def,
is_entry: true,
direct: DirectEffects {
calls: vec![call],
..DirectEffects::default()
},
dispatches: vec![],
}
}
fn manifest_with(name: &str, effects: CapabilityEffects) -> CapabilityManifest {
CapabilityManifest {
externals: vec![CapabilityManifestExternal {
name: name.to_string(),
effects,
}],
}
}
#[test]
fn check_named_condition_purity_accepts_a_reads_only_external_call() {
let (program, _tables, _ctx) = compile_test_story(GATED_STORY_WITH_EXTERNALS);
let def = program
.definition_id_for_path("should_wake")
.expect("should_wake resolves");
let read_state = program
.name_id("read_state")
.expect("interned as a call kind");
let rows = vec![calling_row(def, call_atom(read_state))];
let manifest = manifest_with(
"read_state",
CapabilityEffects {
reads: vec!["GameState".to_string()],
writes: vec![],
detect: std::collections::BTreeMap::new(),
},
);
assert!(
check_named_condition_purity(
&program,
&rows,
&manifest,
None::<&BrinkBindings<()>>,
"should_wake"
)
.is_ok()
);
}
#[test]
fn check_named_condition_purity_rejects_an_external_declared_write() {
let (program, _tables, _ctx) = compile_test_story(GATED_STORY_WITH_EXTERNALS);
let def = program
.definition_id_for_path("should_wake")
.expect("should_wake resolves");
let touch_state = program
.name_id("touch_state")
.expect("interned as a call kind");
let rows = vec![calling_row(def, call_atom(touch_state))];
let manifest = manifest_with(
"touch_state",
CapabilityEffects {
reads: vec![],
writes: vec!["GameState".to_string()],
detect: std::collections::BTreeMap::new(),
},
);
let err = check_named_condition_purity(
&program,
&rows,
&manifest,
None::<&BrinkBindings<()>>,
"should_wake",
)
.unwrap_err();
assert!(
matches!(
&err,
WakeConditionPurityError::ExternalWrites { condition, external, writes }
if condition == "should_wake"
&& external == "touch_state"
&& writes == &vec!["GameState".to_string()]
),
"got {err:?}"
);
}
#[test]
fn check_named_condition_purity_accepts_an_unregistered_external() {
let (program, _tables, _ctx) = compile_test_story(GATED_STORY_WITH_EXTERNALS);
let def = program
.definition_id_for_path("should_wake")
.expect("should_wake resolves");
let touch_state = program
.name_id("touch_state")
.expect("interned as a call kind");
let rows = vec![calling_row(def, call_atom(touch_state))];
let manifest = CapabilityManifest::default();
assert!(
check_named_condition_purity(
&program,
&rows,
&manifest,
None::<&BrinkBindings<()>>,
"should_wake"
)
.is_ok()
);
}
#[test]
fn check_named_condition_purity_rejects_an_external_write_via_dispatch_fallback() {
let (program, _tables, _ctx) = compile_test_story(GATED_STORY_WITH_EXTERNALS);
let def = program
.definition_id_for_path("should_wake")
.expect("should_wake resolves");
let touch_state = program
.name_id("touch_state")
.expect("interned as a call kind");
let dispatch_cell = DefinitionId::new(DefinitionTag::GlobalVar, 1001);
let rows = vec![EffectRowEntry {
def,
is_entry: true,
direct: DirectEffects::default(),
dispatches: vec![DispatchEntry {
cell: dispatch_cell,
narrowable: false,
fallback: DirectEffects {
calls: vec![call_atom(touch_state)],
..DirectEffects::default()
},
}],
}];
let manifest = manifest_with(
"touch_state",
CapabilityEffects {
reads: vec![],
writes: vec!["GameState".to_string()],
detect: std::collections::BTreeMap::new(),
},
);
let err = check_named_condition_purity(
&program,
&rows,
&manifest,
None::<&BrinkBindings<()>>,
"should_wake",
)
.unwrap_err();
assert!(
matches!(err, WakeConditionPurityError::ExternalWrites { .. }),
"got {err:?}"
);
}
#[derive(Event, Clone, Debug, PartialEq, bevy_brink_derive::BrinkCommand)]
struct TouchState {
id: i32,
}
#[derive(Resource, Default)]
struct TouchStateFiredCount(u32);
#[test]
fn check_named_condition_purity_rejects_a_command_bound_external() {
let (program, _tables, _ctx) = compile_test_story(GATED_STORY_WITH_EXTERNALS);
let def = program
.definition_id_for_path("should_wake")
.expect("should_wake resolves");
let touch_state = program
.name_id("touch_state")
.expect("interned as a call kind");
let rows = vec![calling_row(def, call_atom(touch_state))];
let mut app = App::new();
app.bind_brink_command::<(), TouchState>("touch_state");
let bindings = app.world().resource::<BrinkBindings<()>>();
let err = check_named_condition_purity(
&program,
&rows,
&CapabilityManifest::default(),
Some(bindings),
"should_wake",
)
.unwrap_err();
assert!(
matches!(
&err,
WakeConditionPurityError::CommandBinding { condition, external }
if condition == "should_wake" && external == "touch_state"
),
"got {err:?}"
);
}
#[test]
fn check_value_condition_purity_rejects_a_command_bound_external() {
let (program, _tables, _ctx) = compile_test_story(GATED_STORY_WITH_EXTERNALS);
let def = program
.definition_id_for_path("should_wake")
.expect("should_wake resolves");
let touch_state = program
.name_id("touch_state")
.expect("interned as a call kind");
let rows = vec![calling_row(def, call_atom(touch_state))];
let token = Value::FnRef(def);
let mut app = App::new();
app.bind_brink_command::<(), TouchState>("touch_state");
let bindings = app.world().resource::<BrinkBindings<()>>();
let err = check_value_condition_purity(
&program,
&rows,
&CapabilityManifest::default(),
Some(bindings),
&token,
)
.unwrap_err();
assert!(
matches!(
&err,
WakeConditionPurityError::CommandBinding { condition, external }
if condition == "should_wake" && external == "touch_state"
),
"got {err:?}"
);
}
#[test]
fn check_named_condition_purity_accepts_a_pure_binding_alongside_a_command_binding() {
let (program, _tables, _ctx) = compile_test_story(GATED_STORY_WITH_EXTERNALS);
let def = program
.definition_id_for_path("should_wake")
.expect("should_wake resolves");
let read_state = program
.name_id("read_state")
.expect("interned as a call kind");
let rows = vec![calling_row(def, call_atom(read_state))];
let mut app = App::new();
app.bind_brink_fn::<(), _, _>("read_state", |_args| Value::Int(0));
app.bind_brink_command::<(), TouchState>("touch_state");
let bindings = app.world().resource::<BrinkBindings<()>>();
assert!(
check_named_condition_purity(
&program,
&rows,
&CapabilityManifest::default(),
Some(bindings),
"should_wake",
)
.is_ok(),
"a pure (bind_brink_fn) binding must not be rejected just because the same registry \
also carries a command binding"
);
}
#[test]
fn command_bound_condition_is_rejected_and_never_evaluated_through_run_flow_sleep() {
let mut app = build_app();
app.bind_brink_command::<(), TouchState>("touch_state");
app.insert_resource(TouchStateFiredCount::default());
app.add_observer(
|_: On<TouchState>, mut count: ResMut<TouchStateFiredCount>| {
count.0 += 1;
},
);
let (program, tables, ctx) = compile_test_story(GATED_STORY_WITH_EXTERNALS);
let gate_idx = program.global_index("gate").expect("gate global exists");
let def = program
.definition_id_for_path("should_wake")
.expect("should_wake resolves");
let touch_state = program
.name_id("touch_state")
.expect("interned as a call kind");
let story = add_story_assets_with_effect_rows(
&mut app,
program,
tables,
ctx,
vec![calling_row(def, call_atom(touch_state))],
);
app.world_mut().spawn((
BrinkFlowRequest::<()>::builder().story(story).build(),
FlowSleep::<()>::persistent("should_wake").dormant(),
));
app.update();
set_gate(&mut app, gate_idx, 1);
pump(&mut app, 5);
assert_eq!(
single_sleep(&mut app),
SleepState::Faulted,
"a condition calling a bind_brink_command-bound EXTERNAL must land in Faulted, never \
Woken"
);
assert!(
app.world().resource::<TextLog>().0.is_empty(),
"the flow's turn must never run — the command-bound call was never evaluated"
);
assert_eq!(
app.world().resource::<TouchStateFiredCount>().0,
0,
"the command event must never fire — not even once (the #1096/#1609 determinism hazard \
this issue closes)"
);
}
#[test]
fn writing_external_condition_is_rejected_and_never_evaluated_through_run_flow_sleep() {
let mut app = build_app();
let (program, tables, ctx) = compile_test_story(GATED_STORY_WITH_EXTERNALS);
let gate_idx = program.global_index("gate").expect("gate global exists");
let def = program
.definition_id_for_path("should_wake")
.expect("should_wake resolves");
let touch_state = program
.name_id("touch_state")
.expect("interned as a call kind");
let story = add_story_assets_with_effect_rows(
&mut app,
program,
tables,
ctx,
vec![calling_row(def, call_atom(touch_state))],
);
app.insert_resource(manifest_with(
"touch_state",
CapabilityEffects {
reads: vec![],
writes: vec!["GameState".to_string()],
detect: std::collections::BTreeMap::new(),
},
));
app.register_capability::<(), GameStateCap>("GameState");
app.world_mut().spawn((
BrinkFlowRequest::<()>::builder().story(story).build(),
FlowSleep::<()>::persistent("should_wake").dormant(),
));
app.update();
set_gate(&mut app, gate_idx, 1);
pump(&mut app, 5);
assert_eq!(
single_sleep(&mut app),
SleepState::Faulted,
"a condition calling a manifest-declared-writing EXTERNAL must land in Faulted, never \
Woken"
);
assert!(
app.world().resource::<TextLog>().0.is_empty(),
"the flow's turn must never run — the externally-mediated write was never evaluated"
);
}
#[test]
fn check_value_condition_purity_checks_a_resolved_fn_value_token() {
let (program, _tables, _ctx) = compile_test_story(GATED_STORY);
let def = program
.definition_id_for_path("should_wake")
.expect("should_wake resolves");
let token = Value::FnRef(def);
let pure_rows = vec![no_write_row(def)];
assert!(
check_value_condition_purity(
&program,
&pure_rows,
&CapabilityManifest::default(),
None::<&BrinkBindings<()>>,
&token
)
.is_ok()
);
let write_id = DefinitionId::new(DefinitionTag::GlobalVar, 999);
let writing_rows = vec![writing_row(def, write_id)];
let err = check_value_condition_purity(
&program,
&writing_rows,
&CapabilityManifest::default(),
None::<&BrinkBindings<()>>,
&token,
)
.unwrap_err();
assert!(
matches!(err, WakeConditionPurityError::Writes { .. }),
"a dynamic fn-value condition token must be purity-checked exactly like a named one; \
got {err:?}"
);
}
#[test]
fn check_value_condition_purity_rejects_a_non_function_value() {
let (program, _tables, _ctx) = compile_test_story(GATED_STORY);
let def = program
.definition_id_for_path("should_wake")
.expect("should_wake resolves");
let rows = vec![no_write_row(def)];
let err = check_value_condition_purity(
&program,
&rows,
&CapabilityManifest::default(),
None::<&BrinkBindings<()>>,
&Value::Int(1),
)
.unwrap_err();
assert!(
matches!(err, WakeConditionPurityError::NotAFunctionValue),
"got {err:?}"
);
}
#[test]
fn pure_condition_wakes_normally_through_run_flow_sleep() {
let mut app = build_app();
let (program, tables, ctx) = compile_test_story(GATED_STORY);
let gate_idx = program.global_index("gate").expect("gate global exists");
let def = program
.definition_id_for_path("should_wake")
.expect("should_wake resolves");
let story =
add_story_assets_with_effect_rows(&mut app, program, tables, ctx, vec![no_write_row(def)]);
app.world_mut().spawn((
BrinkFlowRequest::<()>::builder().story(story).build(),
FlowSleep::<()>::persistent("should_wake").dormant(),
));
app.update();
set_gate(&mut app, gate_idx, 1);
pump(&mut app, 3);
let state = single_sleep(&mut app);
assert!(
matches!(state, SleepState::Woken | SleepState::Parked),
"a pure condition must wake the flow and let its turn run, not fault or vanish: got {state:?}"
);
assert!(
app.world().resource::<TextLog>().0.contains("Woke up!"),
"a pure condition must be evaluated and wake the flow normally"
);
}
#[test]
fn idle_turns_never_manufacture_a_spurious_wake_signal() {
let mut app = build_app();
let (program, tables, ctx) = compile_test_story(GATED_STORY);
let gate_idx = program.global_index("gate").expect("gate global exists");
let def = program
.definition_id_for_path("should_wake")
.expect("should_wake resolves");
let story =
add_story_assets_with_effect_rows(&mut app, program, tables, ctx, vec![no_write_row(def)]);
app.world_mut().spawn((
BrinkFlowRequest::<()>::builder().story(story).build(),
FlowSleep::<()>::persistent("should_wake").dormant(),
));
app.update();
set_gate(&mut app, gate_idx, 1);
pump(&mut app, 20);
assert_eq!(
single_sleep(&mut app),
SleepState::Parked,
"a persistent policy's one true evaluation must wake exactly once and then \
stay parked — a turn with nothing to collect must never manufacture another"
);
let log = &app.world().resource::<TextLog>().0;
assert!(
log.contains("Woke up!") && !log.contains("Second turn."),
"the flow must run its one woken turn and no further turn: got {log:?}"
);
}
#[test]
fn writing_condition_is_rejected_and_never_evaluated_through_run_flow_sleep() {
let mut app = build_app();
let (program, tables, ctx) = compile_test_story(GATED_STORY);
let gate_idx = program.global_index("gate").expect("gate global exists");
let def = program
.definition_id_for_path("should_wake")
.expect("should_wake resolves");
let write_id = DefinitionId::new(DefinitionTag::GlobalVar, 999);
let story = add_story_assets_with_effect_rows(
&mut app,
program,
tables,
ctx,
vec![writing_row(def, write_id)],
);
app.world_mut().spawn((
BrinkFlowRequest::<()>::builder().story(story).build(),
FlowSleep::<()>::persistent("should_wake").dormant(),
));
app.update();
set_gate(&mut app, gate_idx, 1);
pump(&mut app, 5);
assert_eq!(
single_sleep(&mut app),
SleepState::Faulted,
"an impure condition's policy must land in Faulted, never Woken"
);
assert!(
app.world().resource::<TextLog>().0.is_empty(),
"the flow's turn must never run — the writing condition was never evaluated"
);
}
#[test]
fn dynamic_fn_value_pure_condition_wakes_normally_through_run_flow_sleep() {
let mut app = build_app();
let (program, tables, ctx) = compile_test_story(GATED_STORY);
let gate_idx = program.global_index("gate").expect("gate global exists");
let def = program
.definition_id_for_path("should_wake")
.expect("should_wake resolves");
let token = Value::FnRef(def);
let story =
add_story_assets_with_effect_rows(&mut app, program, tables, ctx, vec![no_write_row(def)]);
app.world_mut().spawn((
BrinkFlowRequest::<()>::builder().story(story).build(),
FlowSleep::<()>::persistent("should_wake")
.with_condition_value(token)
.dormant(),
));
app.update();
set_gate(&mut app, gate_idx, 1);
pump(&mut app, 3);
let state = single_sleep(&mut app);
assert!(
matches!(state, SleepState::Woken | SleepState::Parked),
"a pure dynamic fn-value condition must wake the flow and let its turn run, not fault or vanish: got {state:?}"
);
assert!(
app.world().resource::<TextLog>().0.contains("Woke up!"),
"a pure dynamic fn-value condition must be evaluated and wake the flow normally"
);
}
#[test]
fn dynamic_fn_value_condition_with_writes_is_rejected_and_never_evaluated_through_run_flow_sleep() {
let mut app = build_app();
let (program, tables, ctx) = compile_test_story(GATED_STORY);
let gate_idx = program.global_index("gate").expect("gate global exists");
let def = program
.definition_id_for_path("should_wake")
.expect("should_wake resolves");
let token = Value::FnRef(def);
let write_id = DefinitionId::new(DefinitionTag::GlobalVar, 999);
let story = add_story_assets_with_effect_rows(
&mut app,
program,
tables,
ctx,
vec![writing_row(def, write_id)],
);
app.world_mut().spawn((
BrinkFlowRequest::<()>::builder().story(story).build(),
FlowSleep::<()>::persistent("should_wake")
.with_condition_value(token)
.dormant(),
));
app.update();
set_gate(&mut app, gate_idx, 1);
pump(&mut app, 5);
assert_eq!(
single_sleep(&mut app),
SleepState::Faulted,
"an impure dynamic fn-value condition's policy must land in Faulted, never Woken"
);
assert!(
app.world().resource::<TextLog>().0.is_empty(),
"the flow's turn must never run — the writing condition was never evaluated"
);
}
fn single_sleep(app: &mut App) -> SleepState {
let mut q = app.world_mut().query::<&FlowSleep<()>>();
let sleeps: Vec<SleepState> = q.iter(app.world()).map(FlowSleep::state).collect();
assert_eq!(sleeps.len(), 1, "expected exactly one FlowSleep policy");
sleeps[0]
}
#[test]
fn flow_sleep_reflects_as_component() {
use bevy_reflect::GetTypeRegistration as _;
let registration = FlowSleep::<()>::get_type_registration();
assert!(
registration
.data::<bevy_ecs::reflect::ReflectComponent>()
.is_some(),
"FlowSleep<()> is missing ReflectComponent type data — inspectors cannot see it as a \
component on the flow entity; add `#[reflect(Component)]` alongside `#[derive(Component, Reflect)]`"
);
}
const PEER_WAKE_STORY: &str = "VAR gate = 0\n\
-> waker\n\
=== waker ===\n\
Tick.\n-> DONE\n\
Tick.\n-> DONE\n\
Ping.\n\
~ gate = 1\n\
-> DONE\n\
=== sleeper ===\n\
Woke up!\n-> END\n\
=== function should_wake() ===\n~ return gate\n";
const IDLE_PEER_STORY: &str = "VAR gate = 0\n\
-> waker\n\
=== waker ===\n\
Tick.\n-> DONE\n-> waker\n\
=== sleeper ===\n\
Woke up!\n-> DONE\n\
Second turn.\n-> END\n\
=== function should_wake() ===\n~ return gate\n";
fn program_asset(program: Program, effect_rows: Vec<EffectRowEntry>) -> ProgramAsset {
let initial_context = crate::asset::fresh_context(&program);
ProgramAsset {
program,
initial_context,
effect_rows,
}
}
#[test]
fn a_compiled_conditions_read_row_names_the_global_it_reads() {
let (program, _tables, _ctx, effect_rows) =
crate::test_support::compile_test_story_with_effect_rows(GATED_STORY);
let gate_idx = program.global_index("gate").expect("gate global exists");
let asset = program_asset(program, effect_rows);
let sleep = FlowSleep::<()>::persistent("should_wake");
assert_eq!(
condition_reads(Some(&asset), &sleep),
ConditionReads::Globals([gate_idx].into_iter().collect()),
"the compiler's own inferred row for `should_wake` must resolve to exactly the `gate` \
slot — otherwise the row-directed path never engages and #1146's fix is inert"
);
}
#[test]
fn only_a_write_the_condition_reads_dirties_it() {
let (program, _tables, _ctx, effect_rows) =
crate::test_support::compile_test_story_with_effect_rows(GATED_STORY);
let gate_idx = program.global_index("gate").expect("gate global exists");
let asset = program_asset(program, effect_rows);
let sleep = FlowSleep::<()>::persistent("should_wake");
let reads = condition_reads(Some(&asset), &sleep);
let mut bookkeeping_only = WorldDelta::default();
bookkeeping_only.note_bookkeeping();
assert!(
!delta_touches_condition(&bookkeeping_only, &reads, &sleep),
"a visit-count/turn-index write must be inert for a `gate`-reading condition — that \
signal is what manufactured #1101's spurious re-wake"
);
let mut wrote_gate = WorldDelta::default();
wrote_gate.note_global(gate_idx);
assert!(
delta_touches_condition(&wrote_gate, &reads, &sleep),
"a write to a cell the condition reads must still dirty it"
);
let mut wrote_another_cell = WorldDelta::default();
wrote_another_cell.note_global(gate_idx + 7);
assert!(
!delta_touches_condition(&wrote_another_cell, &reads, &sleep),
"a write to an unrelated global must be inert — per-cell, not per-resource"
);
}
#[test]
fn a_declared_bookkeeping_reader_is_dirtied_by_bookkeeping_writes() {
let (program, _tables, _ctx, effect_rows) =
crate::test_support::compile_test_story_with_effect_rows(GATED_STORY);
let asset = program_asset(program, effect_rows);
let sleep = FlowSleep::<()>::persistent("should_wake").reads_bookkeeping();
let reads = condition_reads(Some(&asset), &sleep);
let mut bookkeeping_only = WorldDelta::default();
bookkeeping_only.note_bookkeeping();
assert!(
delta_touches_condition(&bookkeeping_only, &reads, &sleep),
"a condition the host declared a bookkeeping reader must re-evaluate when a visit \
count / turn index moved — the row cannot express that read"
);
}
#[test]
fn a_reads_bookkeeping_conditions_own_evaluation_reflags_it_with_no_real_change() {
let mut app = App::new();
app.init_resource::<CapabilityRegistry<()>>();
app.init_resource::<CapabilityChanges<()>>();
app.init_resource::<BrinkWorldDelta<()>>();
let entity = app
.world_mut()
.spawn(
FlowSleep::<()>::persistent("should_wake")
.reads_bookkeeping()
.dormant(),
)
.id();
app.world_mut()
.get_mut::<FlowSleep<()>>(entity)
.expect("policy present")
.evaluated_once = true;
app.world_mut()
.resource_mut::<BrinkWorldDelta<()>>()
.record_condition_evaluation();
app.world_mut()
.run_system_once(mark_wake_dirty::<()>)
.expect("mark_wake_dirty runs");
assert!(
app.world()
.entity(entity)
.get::<FlowSleep<()>>()
.expect("still attached")
.needs_eval,
"a `reads_bookkeeping()` policy's own prior evaluation residue — with no \
`BrinkGlobals` resource at all and no real dependency change — must still \
re-flag it for evaluation: a deliberate over-report, never a missed wake"
);
}
#[test]
fn an_opaque_or_absent_row_degrades_to_the_conservative_path() {
let (program, _tables, _ctx) = compile_test_story(GATED_STORY);
let def = program
.definition_id_for_path("should_wake")
.expect("should_wake resolves");
let sleep = FlowSleep::<()>::persistent("should_wake");
let mut bookkeeping_only = WorldDelta::default();
bookkeeping_only.note_bookkeeping();
let opaque = program_asset(program, vec![opaque_row(def)]);
let reads = condition_reads(Some(&opaque), &sleep);
assert_eq!(reads, ConditionReads::Unknown);
assert!(
delta_touches_condition(&bookkeeping_only, &reads, &sleep),
"an opaque row cannot bound the condition's reads — it must re-evaluate on any change"
);
assert!(
!delta_touches_condition(&WorldDelta::default(), &reads, &sleep),
"…but a window in which nothing was written is still no reason to re-evaluate"
);
assert_eq!(
condition_reads(None, &sleep),
ConditionReads::Unknown,
"no loaded program → conservative"
);
}
#[test]
fn a_peer_flows_attributed_write_still_wakes_a_sleeper() {
let mut app = build_app();
let (program, tables, ctx, effect_rows) =
crate::test_support::compile_test_story_with_effect_rows(PEER_WAKE_STORY);
let gate_idx = program.global_index("gate").expect("gate global exists");
let story = add_story_assets_with_effect_rows(&mut app, program, tables, ctx, effect_rows);
app.world_mut().spawn((
BrinkFlowRequest::<()>::builder()
.story(story.clone())
.start(crate::FlowStart::Address("sleeper".to_string()))
.build(),
FlowSleep::<()>::persistent("should_wake").dormant(),
));
app.world_mut()
.spawn(BrinkFlowRequest::<()>::builder().story(story).build());
app.update();
pump(&mut app, 10);
assert_eq!(
app.world()
.resource::<BrinkGlobals<()>>()
.inner
.global(gate_idx),
&Value::Int(1),
"fixture sanity: the waker's turn wrote the cell the sleeper's condition reads"
);
assert!(
app.world().resource::<TextLog>().0.contains("Woke up!"),
"row-directed dirtying must still wake a policy whose read row intersects the turn's \
writes: got {:?}",
app.world().resource::<TextLog>().0
);
}
#[test]
fn a_bookkeeping_only_peer_turn_never_re_wakes_a_global_reading_policy() {
assert_no_spurious_re_wake_under_an_idle_peer(build_app());
}
#[test]
fn a_bookkeeping_only_peer_turn_never_re_wakes_a_global_reading_policy_with_a_handle_kind() {
let mut app = build_app();
app.register_handle_kind::<(), SleepProbeKind>(SleepProbeKind);
assert_no_spurious_re_wake_under_an_idle_peer(app);
}
fn assert_no_spurious_re_wake_under_an_idle_peer(mut app: App) {
let (program, tables, ctx, effect_rows) =
crate::test_support::compile_test_story_with_effect_rows(IDLE_PEER_STORY);
let gate_idx = program.global_index("gate").expect("gate global exists");
let story = add_story_assets_with_effect_rows(&mut app, program, tables, ctx, effect_rows);
app.world_mut().spawn((
BrinkFlowRequest::<()>::builder()
.story(story.clone())
.start(crate::FlowStart::Address("sleeper".to_string()))
.build(),
FlowSleep::<()>::persistent("should_wake").dormant(),
));
app.world_mut()
.spawn(BrinkFlowRequest::<()>::builder().story(story).build());
app.update();
set_gate(&mut app, gate_idx, 1);
pump(&mut app, 20);
let log = &app.world().resource::<TextLog>().0;
assert!(
log.contains("Woke up!"),
"the one real dependency change must still wake the flow: got {log:?}"
);
assert!(
log.contains("Tick."),
"fixture sanity: the peer must actually be taking turns, so every frame carries a \
non-empty (bookkeeping-only) Apply: got {log:?}"
);
assert!(
!log.contains("Second turn."),
"a peer turn that wrote only visit counts / the turn index must not re-wake a policy \
whose condition reads `gate` — that is #1101's spurious wake: got {log:?}"
);
assert_eq!(
single_sleep(&mut app),
SleepState::Parked,
"the policy must still be attached and parked, not retired by a second turn's `-> END`"
);
}
#[test]
fn a_direct_host_write_still_wakes_a_flow_with_a_precise_row() {
let mut app = build_app();
let (program, tables, ctx, effect_rows) =
crate::test_support::compile_test_story_with_effect_rows(GATED_STORY);
let gate_idx = program.global_index("gate").expect("gate global exists");
let story = add_story_assets_with_effect_rows(&mut app, program, tables, ctx, effect_rows);
app.world_mut().spawn((
BrinkFlowRequest::<()>::builder().story(story).build(),
FlowSleep::<()>::persistent("should_wake").dormant(),
));
app.update(); pump(&mut app, 4);
assert!(
app.world().resource::<TextLog>().0.is_empty(),
"parked while gate == 0"
);
set_gate(&mut app, gate_idx, 1);
pump(&mut app, 6);
assert!(
app.world().resource::<TextLog>().0.contains("Woke up!"),
"a host write the changed-cell ledger cannot attribute must fall back to the coarse \
signal and wake the flow: got {:?}",
app.world().resource::<TextLog>().0
);
}
#[test]
fn advance_batch_parallel_observer_write_during_flush_flags_a_precise_row_sleeper() {
let mut app = make_test_app();
app.add_systems(Update, crate::advance_batch_parallel::<()>);
let (program, tables, ctx, effect_rows) =
crate::test_support::compile_test_story_with_effect_rows(
"VAR gate = 0\n\
-> waker\n\
=== waker ===\n\
Tick.\n-> DONE\n-> waker\n\
=== function should_wake() ===\n~ return gate\n",
);
let gate_idx = program.global_index("gate").expect("gate global exists");
let program_handle = app
.world_mut()
.resource_mut::<Assets<ProgramAsset>>()
.add(ProgramAsset {
program,
initial_context: ctx,
effect_rows,
});
let tables_handle = app
.world_mut()
.resource_mut::<Assets<LineTablesAsset>>()
.add(LineTablesAsset { tables });
let story = app
.world_mut()
.resource_mut::<Assets<BrinkStoryAsset>>()
.add(BrinkStoryAsset {
program: program_handle.clone(),
line_tables: tables_handle,
});
app.add_observer(
move |_t: On<BrinkTurnDone<()>>, mut globals: ResMut<BrinkGlobals<()>>| {
globals.inner.set_global(gate_idx, Value::Int(1));
},
);
app.world_mut()
.spawn(BrinkFlowRequest::<()>::builder().story(story).build());
app.update(); app.update();
app.world_mut()
.run_system_once(mark_wake_dirty::<()>)
.expect("mark_wake_dirty runs");
let sleeper = app
.world_mut()
.spawn((
FlowSleep::<()>::persistent("should_wake").dormant(),
BrinkProgram::<()>::new(program_handle),
))
.id();
{
let mut sleep = app
.world_mut()
.get_mut::<FlowSleep<()>>(sleeper)
.expect("just spawned");
sleep.evaluated_once = true;
sleep.needs_eval = false;
}
app.update();
{
let mut sleep = app
.world_mut()
.get_mut::<FlowSleep<()>>(sleeper)
.expect("still attached");
sleep.evaluated_once = true;
sleep.needs_eval = false;
}
app.world_mut()
.run_system_once(mark_wake_dirty::<()>)
.expect("mark_wake_dirty runs");
assert!(
app.world()
.entity(sleeper)
.get::<FlowSleep<()>>()
.expect("still attached")
.needs_eval,
"an observer's synchronous write to `gate` during the parallel driver's own \
deferred-command flush must still flag a `gate`-reading sleeper for \
re-evaluation — a same-tick Apply/observer write must not be reported as a \
complete, gate-omitting account"
);
}