OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_room_state_fault.gd

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