OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / state_routes_gate.gd

180 lines · gd ref:
extends SceneTree
## HEADLESS GATE — the state table is complete, and it still says what the three
## hand-kept declarations used to say.
##
## Run: "$HYPER_TPS_GODOT" --headless --script scenes/test/state_routes_gate.gd
##
## #32 §7 asked for this in as many words: "A single table plus a gate asserting
## every routed prefix has decided its scope and authorship would have prevented at
## least two of today's bugs." Both of those bugs were SILENT — a key with no route
## falls through to {REALTIME, "peer"} and is broadcast to whoever is in the mesh
## and then lost forever, and a key missing from the authorship list reads as
## "shared content, anyone may write". Neither prints anything. world:level: was
## wrong in both ways at once for months.
##
## So this gate is not about tidiness. It is the thing that makes a missing column
## LOUD, and it runs without a room, a map or a peer.

var fails := 0


func _ok(what: String, cond: bool) -> void:
	if not cond:
		fails += 1
	print("  %-62s %s" % [what, "OK" if cond else "**FAIL**"])


func _eqs(what: String, got: String, want: String) -> void:
	var c := got == want
	if not c:
		fails += 1
	print("  %-62s %-22s want %-22s %s" % [what, got, want, "OK" if c else "**FAIL**"])


## DEFERRED, and against the REAL autoload. Loading game_state.gd with load() from
## _init instead compiles it before the autoloads exist, so `KeetService` is an
## unknown identifier — and the failed compile is then CACHED, so the engine hands
## the actual GameState singleton a broken script and every subsystem that touches
## it fails afterwards. The gate would have been testing a copy in a world where
## the original had been destroyed by the act of testing it.
func _init() -> void:
	_start.call_deferred()


func _start() -> void:
	print("=== State routes ===\n")

	# By PATH, not by name: a --script gate is compiled before the autoloads are
	# registered, so `GameState` is not an identifier yet even though the singleton
	# exists by the time this deferred call runs.
	var gs: Node = root.get_node_or_null("/root/GameState")
	if gs == null:
		print("  no GameState autoload — cannot gate the table")
		quit(1)
		return
	var table: Array = gs.STATE_ROUTES

	print("-- every row decides all four questions --")
	var complete := true
	var typed := true
	var seen := {}
	var dupes := ""
	for row: Dictionary in table:
		# A row that omits a field is the bug this gate exists for: `get(field, X)`
		# elsewhere would supply a plausible default and nobody would ever know the
		# question had not been answered.
		for field: String in ["prefix", "channel", "role", "map_scope", "author"]:
			if not row.has(field):
				complete = false
				print("    row %s is missing '%s'" % [str(row.get("prefix", "?")), field])
		if not (row.get("prefix", 0) is String and row.get("role", 0) is String
				and row.get("map_scope", 0) is String and row.get("author", 0) is Array):
			typed = false
		var p: String = String(row.get("prefix", ""))
		if seen.has(p):
			dupes += p + " "
		seen[p] = true
	_ok("every row names a prefix, channel, role, scope and authorship", complete)
	_ok("and each of those has the type the readers expect", typed)
	_ok("no prefix is declared twice (%s)" % ("none" if dupes == "" else dupes), dupes == "")

	print("\n-- a prefix that persists has decided WHO OWNS IT --")
	# The pairing that actually bites: DURABLE means the write survives and reaches
	# other peers, and that is exactly when authorship stops being cosmetic. A
	# durable row may still be shared — one sky, last write wins — but it must say
	# so by carrying an empty list rather than by omitting the field, which the
	# check above already refused.
	var durable := 0
	for row: Dictionary in table:
		if int(row["channel"]) == gs.Channel.DURABLE:
			durable += 1
	_ok("%d durable rows, each with an explicit authorship list" % durable, durable > 0)

	print("\n-- the derived lists still say what the hand-kept ones said --")
	# Pinned deliberately. The point of the refactor is that behaviour did not
	# change, and the only way to know that is to compare against what was there
	# before rather than against the table that now generates it.
	var want_map: Array[String] = [
		"world:terrain:", "world:players:", "world:creatures:", "world:objects:",
		"world:level:", "world:sky:",
		# Added with the bot registry (0.1139.0). Both world:bots: rows are
		# map-scoped deliberately -- that scoping IS bot containment, so the
		# registry row drops with the map's durable state and the pos row is
		# evicted with the map's realtime state. This list is hand-written on
		# purpose, so a new map scope has to be acknowledged here; it was not,
		# and this gate had been failing since that release.
		"world:bots:",
		# The Home starter marker (2026-10-05): per map, so each map at Home is
		# seeded from its own release starter exactly once. docs/home-world-design.md §6.
		"world:starter:"]
	# admin:cmd: since the admin-role change (2026-10-04): an admin command's key carries
	# its author's memberId, so its role is the AUTHOR's, never one it claims.
	var want_author: Array[String] = [
		"world:terrain:edit:", "world:objects:", "world:players:", "world:level:", "admin:cmd:"]
	var got_map: Array = gs.MAP_SCOPED_PREFIXES.duplicate()
	var got_author: Array = gs.AUTHOR_SCOPED_PREFIXES.duplicate()
	got_map.sort()
	got_author.sort()
	var wm: Array = want_map.duplicate()
	var wa: Array = want_author.duplicate()
	wm.sort()
	wa.sort()
	_eqs("map-scoped prefixes", str(got_map), str(wm))
	_eqs("author-scoped prefixes", str(got_author), str(wa))

	print("\n-- and a key of each kind behaves the way its row says --")
	gs.current_map_id = "map_dc_level"
	# Map-scoped: stored under the map, so two maps in one room do not share it.
	_eqs("a sky key is namespaced by map", gs._scoped("world:sky:preset"),
		"map:map_dc_level:world:sky:preset")
	# Room-global: presence is the room's, not the map's — namespacing it would
	# hide half the room's members every time someone walked through a teleporter.
	_eqs("a presence key is not", gs._scoped("presence:abc"), "presence:abc")
	# Authored: the segment after the prefix is the author, and can_write compares
	# it to the writer's member id.
	_eqs("a level key names its author", gs._author_of("world:level:MEMBER7:brush:9"), "MEMBER7")
	# Shared: no author, so the role gate alone decides — which is correct for one
	# sky, and catastrophic for brushes, which is why they are separate columns.
	_eqs("a sky key names none", gs._author_of("world:sky:preset"), "")
	# Finer than the route: world:terrain: is routed and map-scoped as a whole, but
	# only edits underneath it are owned by a peer.
	_eqs("a terrain EDIT names its author", gs._author_of("world:terrain:edit:M3:12"), "M3")
	_eqs("terrain settings name none", gs._author_of("world:terrain:noise:seed"), "")

	print("\n-- installing the table routes every prefix in it --")
	gs.install_routes()
	var routed := true
	var missing := ""
	for row: Dictionary in table:
		var pattern: String = String(row["prefix"]) + "*"
		if not gs._routes.has(pattern):
			routed = false
			missing += pattern + " "
		else:
			var got: Dictionary = gs._routes[pattern]
			var want_role: String = String(row["role"])
			if want_role == "":
				want_role = gs.default_write_role
			if int(got["channel"]) != int(row["channel"]) or String(got["role"]) != want_role:
				routed = false
				missing += pattern + "(mismatch) "
	_ok("every row is installed with its own channel and role (%s)"
			% ("all" if missing == "" else missing), routed)
	# Nothing EXTRA: a route installed from somewhere else is a fourth declaration,
	# which is the thing this table exists to stop being possible.
	_ok("and nothing else is routed", gs._routes.size() == table.size())

	print("\n-- the asset library is durable and room-global --")
	# world:assets:<kind>:<name> (AssetSync's MUTABLE pointers: models, PBR textures,
	# scripts) had NO ROW, so it fell through to {REALTIME, peer}: a model added to
	# the library reached whoever was meshed and was gone on reload. Room-global, as
	# asset_sync.gd says (a WAD is not part of one map); newest publish wins, so no
	# author. docs/home-world-design.md §9 P6.
	_eqs("an asset pointer is DURABLE", str(int(gs._resolve("world:assets:model:crate")["channel"])),
		str(int(gs.Channel.DURABLE)))
	_eqs("and room-global (not namespaced by map)", gs._scoped("world:assets:model:crate"),
		"world:assets:model:crate")

	print("\n%s" % ("PASS" if fails == 0 else "FAILED (%d)" % fails))
	quit(0 if fails == 0 else 1)