extends SceneTree
## Gate: when room state does not load, the log says WHICH fault it was.
##
## The message used to be:
##
## room-state load TIMED OUT after 8s — proceeding with defaults. The cold
## path measures ~2.6s, so this is a fault (daemon down, room not opening)
##
## which names two candidates and distinguishes neither. It fired once on
## stortron after a pgh app update and four controlled attempts failed to
## reproduce it, so the one occurrence had to carry its own evidence and did not.
##
## The conditions are separable from data already on hand at that moment:
##
## client.is_connected the UDS never came up
## current_room == "" NO ROOM SELECTED — _poll_state returns
## immediately in that case, so state_loaded can
## never fire. The old message did not mention this
## at all, and it is the cheapest thing to be wrong.
## get_rooms().size() == 0 the daemon answered but knows no rooms
## _consecutive_disconnect_polls polls skipped because the socket dropped
## the last error_occurred text the daemon said something; say what
##
## Gated as a PURE mapping from observations to message, so the rule can be
## tested without standing up a daemon — which is the only part that can be
## wrong in a way that matters. A separate structural check makes sure the
## timeout path actually calls it.
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 room_state_fault: %s" % ("PASS" if ok else "FAIL"))
if not ok:
quit(1)
return true
func _run() -> void:
var script: GDScript = load("res://scenes/game/game.gd")
_p(script != null, "game.gd compiles")
if script == null:
return
var argc := -1
for m: Dictionary in script.get_script_method_list():
if String(m.get("name", "")) == "_describe_room_state_fault":
argc = (m.get("args", []) as Array).size()
break
_p(argc >= 5, "game.gd exposes _describe_room_state_fault(...) — got %d args" % argc)
if argc < 5:
return
# (uds_connected, room_id, room_count, disconnect_polls, last_error)
var no_uds: String = script._describe_room_state_fault(false, "r1", 3, 4, "")
_p(no_uds.to_lower().contains("uds") or no_uds.to_lower().contains("socket"),
"a dead UDS is named as such (%s)" % no_uds)
var no_room: String = script._describe_room_state_fault(true, "", 3, 0, "")
_p(no_room.to_lower().contains("room") and no_room.to_lower().contains("select"),
"no room selected is named — the case the old message never mentioned (%s)" % no_room)
var no_rooms: String = script._describe_room_state_fault(true, "r1", 0, 0, "")
_p(no_rooms.contains("0"),
"a daemon that lists zero rooms is named (%s)" % no_rooms)
var err: String = script._describe_room_state_fault(true, "r1", 3, 0, "state.list blew up")
_p(err.contains("state.list blew up"),
"the daemon's own error text is quoted rather than summarised (%s)" % err)
var silent: String = script._describe_room_state_fault(true, "r1", 3, 0, "")
_p(silent != "" and silent.to_lower().contains("no error"),
"a healthy-looking daemon that simply never answered is its OWN diagnosis (%s)" % silent)
# ---- the four must be distinguishable from one another ----
var all: Array[String] = [no_uds, no_room, no_rooms, err, silent]
var seen: Dictionary = {}
var dupes := 0
for m2: String in all:
if seen.has(m2):
dupes += 1
seen[m2] = true
_p(dupes == 0, "all five conditions produce DIFFERENT messages (%d duplicates)" % dupes)
# SELF-CHECK: a function returning one constant would pass every "contains"
# above only if that constant contained every substring, and would certainly
# fail the distinctness check. Assert the discrimination directly too.
_p(no_uds != no_room and no_room != no_rooms,
"self-check: the message varies with the observations, so it reads them")
# ---- and the timeout path uses it ----
var src: String = FileAccess.get_file_as_string("res://scenes/game/game.gd")
var idx: int = src.find("func _await_room_state")
_p(idx >= 0, "_await_room_state exists")
if idx >= 0:
var body: String = src.substr(idx, 2600)
_p(body.contains("_describe_room_state_fault"),
"the timeout path calls it, rather than printing a fixed string")