use super::*;
use bevy_app::App;
use bevy_asset::Assets;
use bevy_ecs::prelude::*;
use brink_format::Value;
use brink_runtime::{ExternalFnHandler, ExternalResult, FastRng, FlowInstance};
use crate::BrinkFlow;
use crate::asset::{LineTablesAsset, ProgramAsset};
use crate::test_support::{compile_test_story, compile_test_story_brink_gradual};
#[derive(Event, Clone, Debug, PartialEq, Eq)]
struct Ping {
label: String,
}
impl BrinkCommand for Ping {
fn from_ink_args(args: &[Value]) -> Result<Self, BrinkArgError> {
let label = args
.first()
.and_then(Value::as_str)
.ok_or(BrinkArgError::Type {
index: 0,
expected: "string",
})?
.to_string();
Ok(Self { label })
}
fn reply(&self) -> Value {
#[expect(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
reason = "test value, small"
)]
Value::Int(self.label.len() as i32)
}
}
#[derive(Event, Clone, Debug, PartialEq, bevy_brink_derive::BrinkCommand)]
struct SetVolume {
channel: i32,
level: f32,
}
fn app_with_double_and_ping() -> App {
let mut app = App::new();
app.bind_brink_fn::<(), _, _>("double", |args| {
args.first().and_then(Value::as_int).unwrap_or(0) * 2
});
app.bind_brink_command::<(), Ping>("ping");
app
}
#[test]
fn pure_fn_resolves_inline() {
let app = app_with_double_and_ping();
let bindings = app.world().resource::<BrinkBindings<()>>();
let handler = bindings.handler();
match handler.call("double", &[Value::Int(21)]) {
ExternalResult::Resolved(Value::Int(42)) => {}
other => panic!("expected Resolved(Int(42)), got {other:?}"),
}
assert_eq!(handler.queued_len(), 0, "pure fn buffers nothing");
}
#[test]
fn command_buffers_trigger_and_returns_reply() {
let app = app_with_double_and_ping();
let bindings = app.world().resource::<BrinkBindings<()>>();
let handler = bindings.handler();
match handler.call("ping", &[Value::from("hi")]) {
ExternalResult::Resolved(Value::Int(2)) => {}
other => panic!("expected Resolved(Int(2)), got {other:?}"),
}
assert_eq!(handler.queued_len(), 1, "command buffers one trigger");
}
#[test]
fn unknown_name_falls_back() {
let app = app_with_double_and_ping();
let bindings = app.world().resource::<BrinkBindings<()>>();
let handler = bindings.handler();
match handler.call("nonexistent", &[]) {
ExternalResult::Fallback => {}
other => panic!("expected Fallback, got {other:?}"),
}
}
#[test]
fn bad_command_args_resolve_null_without_buffering() {
let app = app_with_double_and_ping();
let bindings = app.world().resource::<BrinkBindings<()>>();
let handler = bindings.handler();
match handler.call("ping", &[Value::Int(7)]) {
ExternalResult::Resolved(Value::Null) => {}
other => panic!("expected Resolved(Null), got {other:?}"),
}
assert_eq!(handler.queued_len(), 0, "failed parse buffers nothing");
}
#[test]
fn binding_arg_is_a_snapshot_not_a_mutable_handle() {
let mut app = App::new();
app.bind_brink_fn::<(), _, _>("append_len", |args| {
let mut snap = args.first().cloned().unwrap_or(Value::Null);
match snap.array_make_mut() {
Some(items) => {
items.push(Value::Int(99));
#[expect(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
Value::Int(items.len() as i32)
}
None => Value::Int(-1),
}
});
let bindings = app.world().resource::<BrinkBindings<()>>();
let handler = bindings.handler();
let original = Value::array(vec![Value::Int(1), Value::Int(2)]);
match handler.call("append_len", std::slice::from_ref(&original)) {
ExternalResult::Resolved(Value::Int(3)) => {}
other => panic!("expected Resolved(Int(3)), got {other:?}"),
}
assert_eq!(
original.as_array().expect("array").as_slice(),
&[Value::Int(1), Value::Int(2)],
"binding mutated only its snapshot; the caller's value is unchanged",
);
}
#[test]
fn binding_can_return_a_collection_snapshot() {
use brink_format::{MapKey, OrderedMap};
let mut app = App::new();
app.bind_brink_fn::<(), _, _>("make_map", |_args| {
let m: OrderedMap = [
(MapKey::from("a"), Value::Int(1)),
(MapKey::from("b"), Value::Int(2)),
]
.into_iter()
.collect();
Value::map(m)
});
let bindings = app.world().resource::<BrinkBindings<()>>();
let handler = bindings.handler();
match handler.call("make_map", &[]) {
ExternalResult::Resolved(v) => {
let map = v.as_map().expect("returned a map");
assert_eq!(map.len(), 2);
assert_eq!(map.get(&MapKey::from("a")), Some(&Value::Int(1)));
}
other => panic!("expected Resolved(map), got {other:?}"),
}
}
#[test]
fn ref_projection_argument_to_an_external_call_fails_to_compile() {
use brink_compiler::{AnalysisOptions, CompileError, Dialect};
let src = "EXTERNAL heal(target, amount)\n\
STRUCT NPC = #{hp: int}\n\
VAR npc = 0\n\
=== main ===\n\
~ npc = NPC#{hp: 1}\n\
~ heal(ref npc.hp, 5)\n\
-> END\n";
let err = brink_compiler::compile_with_options(
"audit.ink",
|path| {
if path == "audit.ink" {
Ok(src.to_string())
} else {
Err(std::io::Error::new(std::io::ErrorKind::NotFound, path))
}
},
AnalysisOptions {
dialect: Dialect::Brink,
..AnalysisOptions::default()
},
)
.expect_err("a ref-projection argument to an EXTERNAL target must not compile");
let CompileError::Diagnostics(diags) = err else {
panic!("expected a diagnostics rejection, got {err:?}");
};
assert!(
!diags.is_empty(),
"expected at least one diagnostic rejecting the construct"
);
}
#[test]
fn value_read_through_a_ref_projection_reaches_a_binding_as_a_plain_snapshot() {
let src = "STRUCT NPC = #{hp: int, name: string}\n\
VAR npc = 0\n\
EXTERNAL report(x)\n\n\
~ npc = NPC#{hp: 7, name: \"x\"}\n\
~ temp result = heal(ref npc.hp, 5)\n\
Result: {result}\n\
-> END\n\n\
=== function heal(ref hp, amount) ===\n\
~ temp seen = report(hp)\n\
~ hp = hp + amount\n\
~ return hp\n";
let (program, tables, _ctx) = compile_test_story_brink_gradual(src);
let seen: std::sync::Arc<std::sync::Mutex<Vec<Value>>> = std::sync::Arc::default();
let seen_for_binding = std::sync::Arc::clone(&seen);
let mut app = App::new();
app.bind_brink_fn::<(), _, _>("report", move |args| {
seen_for_binding
.lock()
.expect("lock")
.push(args.first().cloned().unwrap_or(Value::Null));
Value::Null
});
let bindings = app.world().resource::<BrinkBindings<()>>();
let handler = bindings.handler();
let (mut flow, mut ctx) = FlowInstance::new_at_root(&program);
let mut text = String::new();
loop {
let line = flow
.step_single_line::<FastRng>(&program, &tables, &mut ctx, &handler, None)
.unwrap();
text.push_str(line.text());
if line.is_terminal() {
break;
}
}
assert!(
text.contains("Result: 12"),
"heal(ref npc.hp, 5) should read 7, add 5, and return 12; got {text:?}"
);
let captured = seen.lock().expect("lock");
assert_eq!(
captured.as_slice(),
&[Value::Int(7)],
"the binding must see the dereferenced value (7), never a Value::Projection: \
got {captured:?}"
);
}
#[test]
fn e2e_pure_fn_value_appears_in_text() {
let (program, tables, _ctx) =
compile_test_story("EXTERNAL double(x)\nResult: {double(21)}.\n-> END\n");
let app = app_with_double_and_ping();
let bindings = app.world().resource::<BrinkBindings<()>>();
let handler = bindings.handler();
let (mut flow, mut ctx) = FlowInstance::new_at_root(&program);
let mut text = String::new();
loop {
let line = flow
.step_single_line::<FastRng>(&program, &tables, &mut ctx, &handler, None)
.unwrap();
text.push_str(line.text());
if line.is_terminal() {
break;
}
}
assert!(
text.contains("Result: 42"),
"expected 'Result: 42' in story text; got {text:?}"
);
}
#[test]
fn e2e_command_triggers_event() {
#[derive(Resource, Default)]
struct PingLog(Vec<String>);
let (program, tables, _ctx) =
compile_test_story("EXTERNAL ping(label)\nA{ping(\"hi\")}B\n-> END\n");
let mut app = app_with_double_and_ping();
app.init_resource::<PingLog>();
app.add_observer(|on: On<Ping>, mut log: ResMut<PingLog>| {
log.0.push(on.event().label.clone());
});
let triggers = {
let bindings = app.world().resource::<BrinkBindings<()>>();
let handler = bindings.handler();
let (mut flow, mut ctx) = FlowInstance::new_at_root(&program);
loop {
let line = flow
.step_single_line::<FastRng>(&program, &tables, &mut ctx, &handler, None)
.unwrap();
if line.is_terminal() {
break;
}
}
handler.queued.into_inner()
};
assert_eq!(triggers.len(), 1, "exactly one ping trigger buffered");
for trigger in triggers {
trigger(app.world_mut());
}
let log = app.world().resource::<PingLog>();
assert_eq!(
log.0,
vec!["hi".to_string()],
"observer should see ping(\"hi\")"
);
}
#[test]
fn derived_from_ink_args_parses_strictly() {
let ok = SetVolume::from_ink_args(&[Value::Int(2), Value::Float(0.5)]).unwrap();
assert_eq!(
ok,
SetVolume {
channel: 2,
level: 0.5
}
);
assert!(matches!(ok.reply(), Value::Null));
assert_eq!(
SetVolume::from_ink_args(&[Value::Int(2)]),
Err(BrinkArgError::Count {
expected: 2,
got: 1
})
);
assert_eq!(
SetVolume::from_ink_args(&[Value::Int(2), Value::Int(3)]),
Err(BrinkArgError::Type {
index: 1,
expected: "float"
})
);
}
#[test]
fn e2e_derived_command_triggers_event() {
#[derive(Resource, Default)]
struct VolumeLog(Vec<(i32, f32)>);
let (program, tables, _ctx) =
compile_test_story("EXTERNAL set_volume(ch, lvl)\nA{set_volume(2, 0.5)}B\n-> END\n");
let mut app = App::new();
app.bind_brink_command::<(), SetVolume>("set_volume");
app.init_resource::<VolumeLog>();
app.add_observer(|on: On<SetVolume>, mut log: ResMut<VolumeLog>| {
log.0.push((on.event().channel, on.event().level));
});
let triggers = {
let bindings = app.world().resource::<BrinkBindings<()>>();
let handler = bindings.handler();
let (mut flow, mut ctx) = FlowInstance::new_at_root(&program);
loop {
let line = flow
.step_single_line::<FastRng>(&program, &tables, &mut ctx, &handler, None)
.unwrap();
if line.is_terminal() {
break;
}
}
handler.queued.into_inner()
};
assert_eq!(triggers.len(), 1);
for trigger in triggers {
trigger(app.world_mut());
}
let log = app.world().resource::<VolumeLog>();
assert_eq!(
log.0,
vec![(2, 0.5)],
"observer should see set_volume(2, 0.5)"
);
}
#[derive(Component)]
struct Enemy;
fn enemy_count(In((_entity, _args)): In<BrinkQueryInput>, enemies: Query<&Enemy>) -> Value {
#[expect(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
Value::Int(enemies.iter().count() as i32)
}
#[test]
fn call_ink_function_resolves_world_query() {
use crate::BrinkFlowRequest;
use crate::test_support::{add_story_assets, make_test_app};
let mut app = make_test_app();
app.bind_brink_query::<(), _, _>("enemy_count", enemy_count);
let (program, tables, ctx) = compile_test_story(
"EXTERNAL enemy_count()\n-> END\n=== function can_spawn() ===\n~ return enemy_count() < 3\n",
);
let story = add_story_assets(&mut app, program, tables, ctx);
app.world_mut().spawn(Enemy);
app.world_mut().spawn(Enemy);
let entity = app
.world_mut()
.spawn(BrinkFlowRequest::<()>::builder().story(story).build())
.id();
app.update();
let result = call_ink_function::<()>(app.world_mut(), entity, "can_spawn", &[]).unwrap();
assert_eq!(result.as_bool(), Some(true), "2 enemies < 3 → can spawn");
app.world_mut().spawn(Enemy);
app.world_mut().spawn(Enemy);
let result = call_ink_function::<()>(app.world_mut(), entity, "can_spawn", &[]).unwrap();
assert_eq!(
result.as_bool(),
Some(false),
"4 enemies !< 3 → cannot spawn"
);
}
#[test]
fn call_ink_function_value_reinvokes_an_opaque_token() {
use crate::BrinkFlowRequest;
use crate::test_support::{add_story_assets, make_test_app};
let mut app = make_test_app();
app.bind_brink_fn::<(), _, _>("_unused", |_| 0);
let (program, tables, ctx) = crate::test_support::compile_test_story_brink(
"-> END\n\
=== function make_doubler() ===\n~ return #fn(double)\n\
=== function make_adder() ===\n~ return bind(#fn(add), 10)\n\
=== function double(x: int): int ===\n~ return x + x\n\
=== function add(a: int, b: int): int ===\n~ return a + b\n",
);
let story = add_story_assets(&mut app, program, tables, ctx);
let entity = app
.world_mut()
.spawn(BrinkFlowRequest::<()>::builder().story(story).build())
.id();
app.update();
let double_token =
call_ink_function::<()>(app.world_mut(), entity, "make_doubler", &[]).unwrap();
let adder_token = call_ink_function::<()>(app.world_mut(), entity, "make_adder", &[]).unwrap();
let doubled =
call_ink_function_value::<()>(app.world_mut(), entity, &double_token, &[Value::Int(21)])
.unwrap();
assert_eq!(doubled.as_int(), Some(42), "double(21)");
let summed =
call_ink_function_value::<()>(app.world_mut(), entity, &adder_token, &[Value::Int(5)])
.unwrap();
assert_eq!(summed.as_int(), Some(15), "add(10, 5) via bound closure");
let err = call_ink_function_value::<()>(
app.world_mut(),
entity,
&double_token,
&[Value::Int(1), Value::Int(2)],
)
.unwrap_err();
assert!(
matches!(err, BrinkCallError::Runtime(_)),
"over-supplied args must fault, got {err:?}"
);
}
#[test]
fn call_ink_function_pure_and_errors() {
use crate::BrinkFlowRequest;
use crate::test_support::{add_story_assets, make_test_app};
let mut app = make_test_app();
app.bind_brink_fn::<(), _, _>("triple", |args| {
args.first().and_then(Value::as_int).unwrap_or(0) * 3
});
let (program, tables, ctx) = compile_test_story(
"EXTERNAL triple(n)\n-> END\n=== function scaled(n) ===\n~ return triple(n) + 1\n",
);
let story = add_story_assets(&mut app, program, tables, ctx);
let entity = app
.world_mut()
.spawn(BrinkFlowRequest::<()>::builder().story(story).build())
.id();
app.update();
let result =
call_ink_function::<()>(app.world_mut(), entity, "scaled", &[Value::Int(7)]).unwrap();
assert_eq!(result, Value::Int(22));
let err = call_ink_function::<()>(app.world_mut(), entity, "nope", &[]).unwrap_err();
assert!(
matches!(err, BrinkCallError::FunctionNotFound(_)),
"got {err:?}"
);
}
#[test]
fn call_ink_function_fires_command_event_not_silent_fallback() {
use crate::BrinkFlowRequest;
use crate::test_support::{add_story_assets, make_test_app};
#[derive(Resource, Default)]
struct PingLog(Vec<String>);
let mut app = make_test_app();
app.bind_brink_command::<(), Ping>("ping");
app.init_resource::<PingLog>();
app.add_observer(|on: On<Ping>, mut log: ResMut<PingLog>| {
log.0.push(on.event().label.clone());
});
let (program, tables, ctx) = compile_test_story(
"EXTERNAL ping(label)\n\
-> END\n\
=== function ping(label) ===\n\
~ return -1\n\
=== function sound_alarm() ===\n\
~ return ping(\"intruder\")\n",
);
let story = add_story_assets(&mut app, program, tables, ctx);
let entity = app
.world_mut()
.spawn(BrinkFlowRequest::<()>::builder().story(story).build())
.id();
app.update();
let result = call_ink_function::<()>(app.world_mut(), entity, "sound_alarm", &[]).unwrap();
assert_eq!(
result,
Value::Int(8),
"expected the command binding's reply (label length 8), not the \
in-story fallback's -1 — the command binding must resolve the call"
);
let log = app.world().resource::<PingLog>();
assert_eq!(
log.0,
vec!["intruder".to_string()],
"call_ink_function must fire the bind_brink_command event, not \
silently fall back to the in-story body; observer log: {:?}",
log.0
);
}
#[test]
fn call_ink_function_value_fires_command_event_not_silent_fallback() {
use crate::BrinkFlowRequest;
use crate::test_support::{add_story_assets, make_test_app};
#[derive(Resource, Default)]
struct PingLog(Vec<String>);
let mut app = make_test_app();
app.bind_brink_command::<(), Ping>("ping");
app.init_resource::<PingLog>();
app.add_observer(|on: On<Ping>, mut log: ResMut<PingLog>| {
log.0.push(on.event().label.clone());
});
let (program, tables, ctx) = crate::test_support::compile_test_story_brink_gradual(
"EXTERNAL ping(label)\n\
-> END\n\
=== function ping(label) ===\n\
~ return -1\n\
=== function sound_alarm() ===\n\
~ return ping(\"intruder\")\n\
=== function make_alarm_caller() ===\n\
~ return #fn(sound_alarm)\n",
);
let story = add_story_assets(&mut app, program, tables, ctx);
let entity = app
.world_mut()
.spawn(BrinkFlowRequest::<()>::builder().story(story).build())
.id();
app.update();
let token = call_ink_function::<()>(app.world_mut(), entity, "make_alarm_caller", &[]).unwrap();
let result = call_ink_function_value::<()>(app.world_mut(), entity, &token, &[]).unwrap();
assert_eq!(
result,
Value::Int(8),
"expected the command binding's reply (label length 8) via the function-value \
re-entry path, not the in-story fallback's -1"
);
let log = app.world().resource::<PingLog>();
assert_eq!(
log.0,
vec!["intruder".to_string()],
"call_ink_function_value must fire the bind_brink_command event, not \
silently fall back to the in-story body; observer log: {:?}",
log.0
);
}
#[test]
fn advance_flow_resolves_inline_query_during_playback() {
use crate::BrinkFlowRequest;
use crate::test_support::{add_story_assets, make_test_app};
let mut app = make_test_app();
app.bind_brink_query::<(), _, _>("enemy_count", enemy_count);
let (program, tables, ctx) =
compile_test_story("EXTERNAL enemy_count()\nEnemies near: {enemy_count()}.\n-> END\n");
let story = add_story_assets(&mut app, program, tables, ctx);
app.world_mut().spawn(Enemy);
app.world_mut().spawn(Enemy);
let entity = app
.world_mut()
.spawn(BrinkFlowRequest::<()>::builder().story(story).build())
.id();
app.update();
let line = advance_flow::<()>(app.world_mut(), entity).unwrap();
assert!(
line.text().contains("Enemies near: 2."),
"inline query should resolve to 2; got {:?}",
line.text()
);
}
#[test]
#[expect(clippy::type_complexity, reason = "bevy driver closure query tuple")]
fn step_one_query_pauses_and_plugin_resolver_resumes() {
use crate::test_support::{add_story_assets, make_test_app};
use crate::{Advance, BrinkContext, BrinkFlow, BrinkFlowRequest, BrinkLocale, BrinkProgram};
use bevy_app::Update;
#[derive(Resource, Default)]
struct Lines(Vec<String>);
let mut app = make_test_app();
app.init_resource::<Lines>();
app.bind_brink_query::<(), _, _>("enemy_count", enemy_count);
let (program, tables, ctx) =
compile_test_story("EXTERNAL enemy_count()\nEnemies near: {enemy_count()}.\n-> END\n");
let story = add_story_assets(&mut app, program, tables, ctx);
app.world_mut().spawn(Enemy);
app.world_mut().spawn(Enemy);
app.world_mut()
.spawn(BrinkFlowRequest::<()>::builder().story(story).build());
app.add_systems(
Update,
|mut flows: Query<(
Entity,
&mut BrinkFlow<()>,
&mut BrinkContext<()>,
&BrinkProgram<()>,
&BrinkLocale<()>,
)>,
globals: Option<ResMut<crate::BrinkGlobals<()>>>,
programs: Res<Assets<ProgramAsset>>,
tables: Res<Assets<LineTablesAsset>>,
bindings: Res<BrinkBindings<()>>,
mut commands: Commands,
mut out: ResMut<Lines>| {
let Some(mut globals) = globals else {
return;
};
for (entity, mut flow, mut ctx, prog, loc) in &mut flows {
if flow.inner.has_pending_external() {
continue; }
let (Some(p), Some(t)) = (programs.get(&prog.handle), tables.get(&loc.handle))
else {
continue;
};
let handler = bindings.handler();
let mut view = crate::globals::flow_context_view(&mut globals, &mut ctx);
if let Ok(Advance::Step(line)) = flow.step_one(
&p.program,
&t.tables,
&mut view,
&handler,
entity,
&mut commands,
) {
out.0.push(line.text().to_string());
}
handler.flush(&mut commands);
}
},
);
for _ in 0..6 {
app.update();
}
let lines = &app.world().resource::<Lines>().0;
assert!(
lines.iter().any(|l| l.contains("Enemies near: 2.")),
"expected the resolved inline-query line; got {lines:?}"
);
}
fn compile_accumulator_story() -> (
brink_runtime::Program,
Vec<Vec<brink_format::LineEntry>>,
brink_runtime::World,
) {
crate::test_support::compile_test_story(
"VAR total = 0\n-> END\n\
=== function add(n) ===\n~ total = total + n\n~ return total\n\
=== function get() ===\n~ return total\n",
)
}
fn accumulator_app() -> (bevy_app::App, Entity) {
use crate::BrinkFlowRequest;
use crate::test_support::{add_story_assets, make_test_app};
let mut app = make_test_app();
let (p, t, c) = compile_accumulator_story();
let story = add_story_assets(&mut app, p, t, c);
app.world_mut()
.spawn(BrinkFlowRequest::<()>::builder().story(story).build());
app.update();
let flow = app
.world_mut()
.query_filtered::<Entity, With<BrinkFlow<()>>>()
.iter(app.world())
.next()
.expect("flow fulfilled");
(app, flow)
}
#[test]
fn batch_equals_n_single_calls() {
let calls: Vec<(&str, Vec<Value>)> = vec![
("add", vec![Value::Int(1)]),
("add", vec![Value::Int(10)]),
("add", vec![Value::Int(100)]),
("get", vec![]),
];
let (mut batch_app, batch_flow) = accumulator_app();
let batch: Vec<Value> =
call_ink_functions::<(), _, _>(batch_app.world_mut(), batch_flow, calls.clone())
.into_iter()
.map(|r| r.unwrap())
.collect();
let (mut single_app, single_flow) = accumulator_app();
let single: Vec<Value> = calls
.iter()
.map(|(name, args)| {
call_ink_function::<()>(single_app.world_mut(), single_flow, name, args).unwrap()
})
.collect();
assert_eq!(batch, single, "batch must equal N single calls");
assert_eq!(
batch,
vec![
Value::Int(1),
Value::Int(11),
Value::Int(111),
Value::Int(111)
],
"ordered accumulation across the batch"
);
}
#[test]
fn batch_isolates_errors_and_preserves_order() {
use crate::BrinkFlowRequest;
use crate::test_support::{add_story_assets, make_test_app};
let mut app = make_test_app();
let (p, t, c) = compile_accumulator_story();
let story = add_story_assets(&mut app, p, t, c);
let flow = app
.world_mut()
.spawn(BrinkFlowRequest::<()>::builder().story(story).build())
.id();
app.update();
let calls: Vec<(&str, Vec<Value>)> = vec![
("add", vec![Value::Int(1)]),
("nope", vec![]), ("add", vec![Value::Int(10)]),
("get", vec![]),
];
let results = call_ink_functions::<(), _, _>(app.world_mut(), flow, calls);
assert_eq!(results.len(), 4, "one slot per call, no drops");
assert_eq!(results[0].as_ref().unwrap(), &Value::Int(1));
assert!(
matches!(results[1], Err(BrinkCallError::FunctionNotFound(_))),
"the bad call fails in its own slot; got {:?}",
results[1]
);
assert_eq!(results[2].as_ref().unwrap(), &Value::Int(11));
assert_eq!(results[3].as_ref().unwrap(), &Value::Int(11));
}
#[test]
fn batch_drives_world_queries_across_calls() {
use crate::BrinkFlowRequest;
use crate::test_support::{add_story_assets, make_test_app};
let mut app = make_test_app();
app.bind_brink_query::<(), _, _>("enemy_count", enemy_count);
let (program, tables, ctx) = compile_test_story(
"EXTERNAL enemy_count()\n-> END\n\
=== function seen() ===\n~ return enemy_count()\n",
);
let story = add_story_assets(&mut app, program, tables, ctx);
app.world_mut().spawn(Enemy);
app.world_mut().spawn(Enemy);
let flow = app
.world_mut()
.spawn(BrinkFlowRequest::<()>::builder().story(story).build())
.id();
app.update();
let calls: Vec<(&str, Vec<Value>)> = vec![("seen", vec![]), ("seen", vec![]), ("seen", vec![])];
let results: Vec<Value> = call_ink_functions::<(), _, _>(app.world_mut(), flow, calls)
.into_iter()
.map(|r| r.unwrap())
.collect();
assert_eq!(
results,
vec![Value::Int(2), Value::Int(2), Value::Int(2)],
"each batched query resolves against the World"
);
}
#[test]
fn call_ink_functions_flushes_each_calls_command_before_the_next_call_runs() {
use crate::BrinkFlowRequest;
use crate::test_support::{add_story_assets, make_test_app};
#[derive(Event, Clone, Debug)]
struct Bump;
impl BrinkCommand for Bump {
fn from_ink_args(_args: &[Value]) -> Result<Self, BrinkArgError> {
Ok(Self)
}
}
#[derive(Resource, Default)]
struct Counter(i32);
#[expect(
clippy::needless_pass_by_value,
reason = "bind_brink_query system param, not a plain function argument"
)]
fn read_counter(In((_entity, _args)): In<BrinkQueryInput>, counter: Res<Counter>) -> Value {
Value::Int(counter.0)
}
let mut app = make_test_app();
app.bind_brink_command::<(), Bump>("bump");
app.bind_brink_query::<(), _, _>("read_counter", read_counter);
app.init_resource::<Counter>();
app.add_observer(|_on: On<Bump>, mut counter: ResMut<Counter>| {
counter.0 += 1;
});
let (program, tables, ctx) = compile_test_story(
"EXTERNAL bump()\n\
EXTERNAL read_counter()\n\
-> END\n\
=== function do_bump() ===\n\
~ return bump()\n\
=== function do_read() ===\n\
~ return read_counter()\n",
);
let story = add_story_assets(&mut app, program, tables, ctx);
let entity = app
.world_mut()
.spawn(BrinkFlowRequest::<()>::builder().story(story).build())
.id();
app.update();
let calls: Vec<(&str, Vec<Value>)> = vec![("do_bump", vec![]), ("do_read", vec![])];
let results: Vec<Value> = call_ink_functions::<(), _, _>(app.world_mut(), entity, calls)
.into_iter()
.map(|r| r.unwrap())
.collect();
assert_eq!(
results[1],
Value::Int(1),
"do_read (call 2) must observe do_bump's (call 1) World effect — proving \
call_ink_functions flushes each call's command triggers before the next \
call runs, not once at the end of the batch; got {results:?}"
);
}