extends SceneTree
## Gate: a bot, end to end — BotRegistry -> ScriptHost -> GameState -> containment.
##
## Every piece of this is already unit-covered. What is NOT covered anywhere else
## is that they compose: that registering a bot actually compiles and runs a brain,
## that the brain's writes reach shared state, that a tick happens at all, and that
## a map change tears the whole chain down INCLUDING the state the bot claimed.
##
## That last one is the point. Containment was only ever asserted on the registry's
## own roster; here it is asserted on the CONSEQUENCE — the bot's key is gone from
## GameState, because _bot_exit ran, because ScriptHost unloaded it, because the
## registry despawned it, because its binding ended.
var ok := true
var _ran := false
var _frames := 0
var _br: Node = null
var _gs: Node = null
const BOT := "gate_beacon"
const KEY := "state:bot:beacon:alive"
func _p(c: bool, m: String) -> void:
print(("ok " if c else "FAIL: ") + m)
if not c:
ok = false
func _process(_delta: float) -> bool:
_frames += 1
if not _ran:
_ran = true
_setup()
return false # let a few frames pass so _bot_tick actually runs
if _frames < 5:
return false
_teardown()
print("GATE bot_end_to_end: %s" % ("PASS" if ok else "FAIL"))
quit(0 if ok else 1) # returning true alone exits 0 — a FAIL never reached CI
return true
func _setup() -> void:
_br = root.get_node_or_null(^"/root/BotRegistry")
_gs = root.get_node_or_null(^"/root/GameState")
var sh: Node = root.get_node_or_null(^"/root/ScriptHost")
if _br == null or _gs == null or sh == null:
_p(false, "BotRegistry, GameState and ScriptHost autoloads present")
return
_gs.install_routes()
_gs.current_map_id = "gate_e2e_a"
_p(not _gs.has_state(KEY), "the beacon key is absent before the bot exists")
var r: Dictionary = _br.register(BOT, ["map"], "none",
{"kind": "script_file", "path": "res://bots/beacon.gd"})
_p(bool(r.get("ok", false)), "registered a body=none bot with a local script brain (%s)"
% str(r.get("error", "")))
_p(_br.is_registered(BOT), "bot is on the roster")
# The brain runs under the BOT id, not the file name, so despawn can find it.
_p(sh.is_loaded(BOT), "ScriptHost compiled and is holding the brain under the bot id")
# Registry owns teardown, so the brain must NOT also be map-bound in ScriptHost:
# two owners tearing down one thing is how a despawn hits something already gone.
_p(sh.bindings_of(BOT) == ["session"],
"brain is session-bound inside ScriptHost so the registry is sole owner (got %s)"
% str(sh.bindings_of(BOT)))
# _bot_init ran, and its write reached shared state — the bot is visible to the
# room rather than only to this process.
_p(bool(_gs.get_state(KEY, false)), "_bot_init's write reached GameState")
func _teardown() -> void:
if _br == null or _gs == null:
return
var sh: Node = root.get_node_or_null(^"/root/ScriptHost")
# A map change ends the ONLY binding this bot has.
_gs.enter_map("gate_e2e_b")
_p(not _br.is_registered(BOT), "map change despawned the bot")
_p(not sh.is_loaded(BOT), "and unloaded its brain")
# THE ASSERTION THIS GATE EXISTS FOR: containment reached the bot's own state.
# _bot_exit ran because the chain tore down in order, so nothing it claimed
# outlived it. A key left here is the data half of the creature leak.
_p(not _gs.has_state(KEY), "_bot_exit released the claim — no state outlived the bot")
# And it is startable again afterwards: teardown left nothing stuck.
var again: Dictionary = _br.register(BOT, ["session"], "none",
{"kind": "script_file", "path": "res://bots/beacon.gd"})
_p(bool(again.get("ok", false)), "the same bot id is reusable after despawn")
_br.despawn(BOT)
_p(not _gs.has_state(KEY), "explicit despawn also releases the claim")