OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_bot_registry.gd

199 lines · gd ref:
extends SceneTree
## Gate: BotRegistry — spec/bot-testbed.md (e2).
##
## The property under test is that CONTAINMENT IS DERIVED, not remembered: one
## declaration, and any binding's end ends the bot. The interesting cases are the
## refusals, because a registration that silently degrades is the leak returning —
## an unknown binding means "this container never tears me down".
##
## Body runs in _process: autoloads do not exist during a SceneTree script's
## _initialize (measured — see gate_containment.gd).

var ok := true
var _ran := false


func _p(c: bool, m: String) -> void:
	print(("ok   " if c else "FAIL: ") + m)
	if not c:
		ok = false


func _process(_delta: float) -> bool:
	if _ran:
		return true
	_ran = true
	_run()
	print("GATE bot_registry: %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 _run() -> void:
	var br: Node = root.get_node_or_null(^"/root/BotRegistry")
	var gs: Node = root.get_node_or_null(^"/root/GameState")
	if br == null or gs == null:
		_p(false, "BotRegistry and GameState autoloads present (%s / %s)"
				% [str(br != null), str(gs != null)])
		return
	gs.install_routes()

	# --- the default is map, because leaking must not be the default ---
	var r: Dictionary = br.register("b_default")
	_p(bool(r.get("ok", false)), "register with defaults ok")
	_p(br.bindings_of("b_default") == ["map"],
		"default binding is ['map'], not session (got %s)" % str(br.bindings_of("b_default")))

	# --- refusals: each would otherwise be a bot that looks registered ---
	_p(not bool((br.register("b_bad", ["galaxy"]) as Dictionary).get("ok", true)),
		"unknown binding refused (silently means 'never torn down')")
	_p(not bool((br.register("b_body", [], "player") as Dictionary).get("ok", true)),
		"body 'player' refused — not implemented, so it must not pretend")
	_p(not bool((br.register("b_body2", [], "position") as Dictionary).get("ok", true)),
		"body 'position' refused for the same reason")
	_p(not bool((br.register("b_brain", [], "none", {"kind": "telepathy"}) as Dictionary).get("ok", true)),
		"unknown brain kind refused")
	_p(not bool((br.register("b_default") as Dictionary).get("ok", true)),
		"duplicate id refused")
	_p(not bool((br.register("") as Dictionary).get("ok", true)), "empty id refused")
	# A script brain with no name cannot run, so it must not register.
	_p(not bool((br.register("b_noname", [], "none", {"kind": "script"}) as Dictionary).get("ok", true)),
		"brain.kind=script without a name refused")
	_p(not br.is_registered("b_noname"), "a refused registration leaves no roster entry")

	# --- bindings are a SET and death is their UNION ---
	br.register("b_multi", ["map", "peer:7"])
	_p(br.bindings_of("b_multi") == ["map", "peer:7"], "bindings kept as a set")
	br.register("b_session", ["session"])
	br.register("b_peer", ["peer:9"])
	br.register("b_room", ["room"])

	# --- a map change ends map-bound bots and NOTHING else ---
	gs.current_map_id = "gate_reg_a"
	gs.enter_map("gate_reg_b")
	_p(not br.is_registered("b_default"), "map change despawned the map-bound bot")
	_p(not br.is_registered("b_multi"), "map change despawned a bot bound to map AND peer (union)")
	_p(br.is_registered("b_session"), "session-bound bot survived the map change")
	_p(br.is_registered("b_peer"), "peer-bound bot survived the map change")
	_p(br.is_registered("b_room"), "room-bound bot survived the map change")

	# --- a peer leaving ends only that peer's bots ---
	br._on_peer_left(9)
	_p(not br.is_registered("b_peer"), "peer:9 leaving despawned the peer-bound bot")
	_p(br.is_registered("b_session"), "an unrelated peer leaving left others alone")
	# and the wiring is real, not just the handler
	_p(br.multiplayer == null or br.multiplayer.peer_disconnected.is_connected(br._on_peer_left),
		"peer_disconnected is actually connected")

	# --- rebind is the explicit travel op ---
	br.register("b_travel", ["map"])
	var rb: Dictionary = br.rebind("b_travel", ["session"])
	_p(bool(rb.get("ok", false)), "rebind ok")
	_p(br.bindings_of("b_travel") == ["session"], "rebind replaced the bindings")
	gs.enter_map("gate_reg_c")
	_p(br.is_registered("b_travel"), "a rebound bot survives the map it was rebound away from")
	_p(not bool((br.rebind("b_travel", ["nowhere"]) as Dictionary).get("ok", true)),
		"rebind refuses an unknown binding too")

	# --- room ends everything inside it ---
	_p(br.is_registered("b_room"), "room-bound bot still alive before the room switch")
	gs.reset_for_room_switch()
	_p(not br.is_registered("b_room"), "room switch despawned the room-bound bot")

	# --- despawn is idempotent, so teardown racing an explicit call is not an error
	var d: Dictionary = br.despawn("b_session")
	_p(bool(d.get("ok", false)), "explicit despawn ok")
	_p(bool((br.despawn("b_session") as Dictionary).get("already_gone", false)),
		"despawning twice is not an error")
	_p(bool((br.despawn("never_existed") as Dictionary).get("ok", false)),
		"despawning an unknown id is not an error")

	# --- list reports the roster shape the facade exposes ---
	br.register("b_list", ["session"], "none", {"kind": "none"})
	var rows: Array = br.list()
	var found := false
	for row: Variant in rows:
		var d2: Dictionary = row as Dictionary
		if String(d2.get("id", "")) == "b_list":
			found = true
			_p(String(d2.get("body", "")) == "none", "list reports body")
			_p(String(d2.get("brain", "")) == "none", "list reports brain kind")
			_p((d2.get("bindings", []) as Array) == ["session"], "list reports bindings")
	_p(found, "list includes a registered bot")
	br.despawn("b_list")

	_run_position(br, gs)


## body = position: a bot with a replicated presence. Appended as a second phase
## because it needs a stand-in map -- _container_for refuses a map-bound body when
## there is no map, which is the "refuse rather than degrade" rule applied to the
## node lifetime.
func _run_position(br: Node, gs: Node) -> void:
	var fake_map := Node3D.new()
	fake_map.name = "GateMapForBodies"
	root.add_child(fake_map)
	br.map_container = fake_map

	# Refusal first: a map-bound body with no map must not register.
	br.map_container = null
	var nomap: Dictionary = br.register("b_nomap", ["map"], "position")
	_p(not bool(nomap.get("ok", true)),
		"body=position bound to a map that does not exist is refused")
	br.map_container = fake_map

	# world:bots:registry: is role "mod" and this gate holds no room role, so the
	# durable row is refused -- and the whole registration must be refused with it.
	# Reporting ok here would hand back a bot with a node, a position row and a
	# roster entry that NO OTHER PEER can ever see, because the row announcing it
	# was never written. That is the bug this assertion exists for.
	var refused: Dictionary = br.register("b_norole", ["map"], "position")
	_p(not bool(refused.get("ok", true)),
		"body=position refused when the durable registry row is (%s)" % str(refused.get("error", "")))
	_p(fake_map.get_node_or_null(^"Bot_b_norole") == null,
		"a refused position body leaves no node behind")
	_p(not br.is_registered("b_norole"), "and no roster entry")

	# Everything below needs the durable row to land, so run it against an injected
	# permissive state double. The ROLE is a room concern and is covered by
	# GameState's own gate; what is under test here is the body, not the policy.
	var real_state: Object = br.state
	br.state = load("res://scenes/test/permissive_state.gd").new()
	var r: Dictionary = br.register("b_pos", ["map"], "position")
	_p(bool(r.get("ok", false)), "body=position registers (%s)" % str(r.get("error", "")))

	var node: Node = fake_map.get_node_or_null(^"Bot_b_pos")
	_p(node != null, "body is parented to the MAP, not to the registry")
	_p(br.state.has_state("world:bots:registry:b_pos"), "durable identity row written")
	_p(br.state.has_state("world:bots:pos:b_pos"), "realtime position row written")

	var mv: Dictionary = br.move("b_pos", Vector3(3, 0, 4))
	_p(bool(mv.get("ok", false)), "move() ok")
	var p: Variant = br.state.get_state("world:bots:pos:b_pos")
	_p(p is Array and (p as Array).size() == 3 and float((p as Array)[0]) == 3.0,
		"move() wrote the realtime row (%s)" % str(p))
	if node != null:
		_p((node as Node3D).global_position == Vector3(3, 0, 4), "and moved the local node")

	_p(not bool((br.move("b_default_none", Vector3.ZERO) as Dictionary).get("ok", true)),
		"move() on an unregistered bot refuses")

	# Despawn must erase BOTH rows: leaving the durable one resurrects the body on
	# any peer that reloads room state.
	br.despawn("b_pos")
	_p(not br.state.has_state("world:bots:registry:b_pos"), "despawn erased the identity row")
	_p(not br.state.has_state("world:bots:pos:b_pos"), "despawn erased the position row")

	# A map change evicts the realtime row automatically, because world:bots:pos:
	# is map-scoped -- the containment fix covers the new route with no extra code.
	br.register("b_pos2", ["map"], "position")
	br.state = real_state              # the REAL GameState owns eviction
	gs.set_state("world:bots:pos:b_pos2", [1.0, 2.0, 3.0])
	_p(gs.has_state("world:bots:pos:b_pos2"), "a map-scoped position row exists")
	gs.enter_map("gate_reg_bodies")
	_p(not gs.has_state("world:bots:pos:b_pos2"),
		"map change evicted the map-scoped position row")
	_p(not br.is_registered("b_pos2"), "and despawned the bot")

	fake_map.queue_free()