OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_durable_load_order.gd

115 lines · gd ref:
extends SceneTree
## Gate: a bulk durable load hands handlers a COMPLETE snapshot.
##
## _on_durable_loaded used to store and emit in one pass:
##
##     for key in entries:
##         _durable[local_key] = entries[key]
##         state_changed.emit(local_key, entries[key], Channel.DURABLE)
##
## so every handler ran against a half-filled mirror, and which half depended on
## Dictionary order. That is invisible until a handler consults ANOTHER key to
## decide what to do -- which is exactly what AdminFacade does: it skips a command
## whose admin:result:<seq> marker is already present. "admin:cmd:" sorts before
## "admin:result:", so the marker was never in _durable yet when the command was
## emitted, the skip never fired, and EVERY command in the room re-executed on
## every cold load. That is hyper-games #77 item 5 -- ashatron replaying
## stortron's commands -- and the marker fix was correct all along; the fill order
## was defeating it.
##
## Measured in AI Testing Town: admin:result:pw-1789521468 EXISTS, and the
## pathwalker registration re-dispatched anyway, registering bots into whatever
## map happened to be current at boot.
##
## The invariant is not "admin keys sort a particular way" -- it is that a bulk
## load is ATOMIC from a handler's point of view. This gate asserts that directly,
## so any future handler that reads a sibling key is safe by construction.

var ok := true
var _ran := false

const SEQ := "gateorder1"
## Registering this is the side effect that proves the command executed.
const BOT_ID := "gate-replay-probe"
## A value no dispatch would ever produce. If AdminFacade re-runs the command it
## overwrites the marker with its own result, so the sentinel surviving IS the
## assertion that the command was skipped.
const SENTINEL := "gate-sentinel-do-not-overwrite"


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 durable_load_order: %s" % ("PASS" if ok else "FAIL"))
	if not ok:
		quit(1)
	return true


func _run() -> void:
	var gs: Node = root.get_node_or_null("GameState")
	_p(gs != null, "GameState autoload present")
	if gs == null:
		return
	_p(gs.has_method("_on_durable_loaded"), "GameState exposes the bulk-load entry point")
	if not gs.has_method("_on_durable_loaded"):
		return
	var af: Node = root.get_node_or_null("AdminFacade")
	_p(af != null, "AdminFacade is listening (it is the handler under test)")
	if af == null:
		return

	# ⛔ WITHOUT THIS THE GATE TESTS A DIFFERENT GAME. install_routes() is called by
	# game.gd, which a --script gate never loads, so _routes is empty and EVERY key
	# falls back to REALTIME -- _on_durable_loaded then fills _durable while
	# get_state reads _realtime, and the marker reads back null no matter what the
	# code under test does. An earlier run of this gate reported exactly that and it
	# was the gate's fault, not the product's.
	gs.install_routes()
	_p(int(gs._resolve("admin:result:x").channel) == gs.Channel.DURABLE,
		"admin:result: routes DURABLE, so get_state reads the store the load fills")

	# ⛔ THE OBSERVABLE CANNOT BE THE MARKER'S VALUE. Two earlier versions of this
	# gate failed as gates. A state_changed handler is too late (AdminFacade is
	# connected first, so it has already dispatched and rewritten the marker before
	# a gate handler looks). And the marker's VALUE after the load is destroyed by
	# the load itself: the unfixed code stores each key just before emitting it, so
	# the seeded row lands AFTER the dispatch's own write and overwrites it --
	# leaving the sentinel intact and the gate green against the bug.
	#
	# The honest observable is the command's SIDE EFFECT, which nothing in the load
	# undoes. If the command ran, the bot exists.
	var br: Node = root.get_node_or_null("BotRegistry")
	_p(br != null, "BotRegistry is present to receive the side effect")
	if br == null:
		return
	_p(_find_bot(br, BOT_ID) == false, "the probe bot does not exist before the load")

	var entries := {
		"admin:cmd:" + SEQ: "{\"op\": \"call\", \"path\": \"/global/bots.register\","
			+ " \"args\": [\"" + BOT_ID + "\", [\"room\"], \"none\", {\"kind\": \"none\"}],"
			+ " \"requester_role\": \"admin\"}",
		"admin:result:" + SEQ: SENTINEL,
	}
	gs._on_durable_loaded("gate-room", entries)

	_p(bool(gs.has_state("admin:cmd:" + SEQ)), "the command row is in the mirror afterwards")
	_p(bool(gs.has_state("admin:result:" + SEQ)), "and so is the result marker")
	_p(_find_bot(br, BOT_ID) == false,
		"the ALREADY-APPLIED command did not run again (no '%s' was registered)" % BOT_ID)


func _find_bot(br: Node, id: String) -> bool:
	for row: Variant in br.list():
		if row is Dictionary and String((row as Dictionary).get("id", "")) == id:
			return true
	return false