OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_bot_end_to_end.gd

94 lines · gd ref:
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")