Files
settled-reach/client/tests/test_anti_tedium.gd
T
jpmschweitzerandClaude Opus 5.5 c597ec9131 docs(tooling): T-1253 — sweep the live references to retired tool paths
A script scanned every tracked doc, rule, skill, agent, hook and source file
for tooling/ paths that no longer exist, skipping historical records (sprints,
discussions, workshops, governance, generated wiki pages). It found 62. The
ones that tell a reader what to RUN now name the reach verb:

- The atlas skill still sent agents to tooling/atlas, atlas-verify,
  atlas-update-field and atlas-commit-and-sync — about forty lines, all
  retired in T-1285. They now name the `reach atlas` verbs, and the skill
  records that commit-and-sync STAGES by default (--commit to commit) and
  takes --corridor as an option.
- The clerk agent named tooling/clerk-review (now `reach dev clerk`). The Si
  and clerk briefings sent those agents to the retired tooling/db/decision
  and sqlite-query CLIs and to decisions/*.md paths that moved to
  governance/ in the pql migration. They now name pql.
- The ticket-cli rule documented `pql decisions read`, which does not exist;
  `show` already includes the body.
- The culture authoring guide and the RON sources name
  `reach validate ron`, with the same arguments as before.
- The 41 Blender payloads' usage lines ran the retired tooling/blender
  wrapper, and the docstrings still cited pre-carve-out paths. They now read
  `reach blender run <payload>`.
- Doc comments in server/, client/, wiki TOMLs and the domain modules.

What is left is deliberate: "Formerly …" provenance, dated plans and findings
docs, the retired-pipeline doc, and a build-artefact path.

project.yaml 0.4.14 (mirrored to the client). Comment-only, but four touched
files are in the canvas-version registry (trait_catalog_reader.rs, since
T-1289, canvas_sources.py itself, and two client files). The gate is
path-based and has no override. The previous push was rejected on exactly
this.

Three of the edits are stamped ledger sources, so systems.db is regenerated
and the stamp is fresh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 20:13:58 +02:00

619 lines
22 KiB
GDScript

## Anti-tedium regression tests (#494):
## Guard against UI clutter that makes testing tedious.
##
## Test 1: F12 bug report capture — pressing F12 must not crash; when #495 lands,
## the handler should pause, show capture dialog, save 3 files, unpause.
## Test 2: Gauntlet progress UI hidden — in non-Gauntlet mode (no room_id or
## gauntlet_mode flag in snapshot), timer and personal-bests must not appear.
##
## #495 blocked by #481/#490 (server). #496 blocked by #487 (server).
## Tests stub blocked features and verify anti-tedium guards.
## Spec ref: Sprint 9 briefing (client.md), #495, #496.
class_name TestAntiTedium
extends GdUnitTestSuite
const MAIN_SCENE = preload("res://scenes/main.tscn")
var GauntletHUDScript = load("res://ui/gauntlet_hud.gd")
const BugReportDialogScene = preload("res://ui/bug_report_dialog.tscn")
var _instance: Node = null
func before_test() -> void:
SimBridge.reset_test_state()
SimBridge._last_snapshot = null
GameState.current_tick = 0
GameState.player_position = Vector2.ZERO
GameState.visible_entities = []
GameState.visible_tiles = []
GameState.visible_positions = {}
GameState.current_monologue = null
GameState.current_dialogue = null
GameState.game_time = {}
GameState.pending_recognitions = []
GameState.room_id = null
GameState.gauntlet_mode = false
MetaStack._stack.clear()
func after_test() -> void:
if _instance and is_instance_valid(_instance):
_instance.queue_free()
_instance = null
# -- Helpers -------------------------------------------------------------------
## Encode a minimal valid snapshot as MessagePack bytes (same helper as P0 tests).
func _make_snapshot_bytes(overrides: Dictionary = {}) -> PackedByteArray:
var snapshot := {
"tick": overrides.get("tick", 1),
"version": 23,
"entities": overrides.get("entities", [{
"entity_id": 1,
"x": 10.0,
"y": 10.0,
"z": 0,
"kind": "Player",
"visibility": "Forward",
}]),
"game_time": {
"day": 0,
"time_of_day": 0,
"day_phase": "Morning",
"tick_rate": overrides.get("tick_rate", "Full"),
},
"player_facing": "North",
"player_stance": "Walk",
"player_inventory": [],
"visible_tiles": [],
"nearby_interactions": [],
"pending_recognitions": [],
}
if overrides.has("current_monologue"):
snapshot["current_monologue"] = overrides["current_monologue"]
if overrides.has("current_dialogue"):
snapshot["current_dialogue"] = overrides["current_dialogue"]
# Gauntlet fields: only include if explicitly provided (absence = non-gauntlet)
if overrides.has("room_id"):
snapshot["room_id"] = overrides["room_id"]
if overrides.has("gauntlet_mode"):
snapshot["gauntlet_mode"] = overrides["gauntlet_mode"]
var result = Messagepack.encode(snapshot)
return result.value
func _make_gauntlet_hud() -> Control:
var hud = Control.new()
hud.set_script(GauntletHUDScript)
auto_free(hud)
add_child(hud)
hud._personal_bests = {} # Clear any stats loaded from disk
return hud
func _make_bug_report_dialog() -> Control:
# Instantiate via .tscn — preserves the MetaScreen runtime stack.
# (Sprint 36 migrated bug_report_dialog.gd to extends MetaScreen; bare
# Control.new() + set_script() no longer satisfies the base contract.)
var dialog: Control = BugReportDialogScene.instantiate()
auto_free(dialog)
add_child(dialog)
return dialog
# -- Test 1: F12 Bug Report Capture -------------------------------------------
# #495: BUG_REPORT action triggers the WRONG button capture flow.
# Verifies: dialog exists, activates on action, no state corruption.
# Note: Input.parse_input_event + _unhandled_input is unreliable in headless
# test mode. Tests drive the input queue directly for deterministic coverage.
func test_bug_report_dialog_exists_in_scene() -> void:
# Verify the BugReportDialog node is present and hidden by default.
var scene := MAIN_SCENE
_instance = scene.instantiate()
auto_free(_instance)
add_child(_instance)
_instance._process(0.016)
var dialog: Node = _find_node_recursive(_instance, "BugReportDialog")
assert_that(dialog).override_failure_message(
"BugReportDialog node should exist in scene tree"
).is_not_null()
if dialog is CanvasItem:
assert_that((dialog as CanvasItem).visible).override_failure_message(
"BugReportDialog should be hidden by default"
).is_false()
func test_bug_report_activates_on_action() -> void:
# Inject BUG_REPORT action directly into the queue and verify main.gd
# triggers the dialog. This tests the full main._process() handling path.
var scene := MAIN_SCENE
_instance = scene.instantiate()
auto_free(_instance)
add_child(_instance)
_instance._process(0.016)
# Record baseline state
var mono_before: Variant = GameState.current_monologue
var dialogue_before: Variant = GameState.current_dialogue
# Inject BUG_REPORT action into InputMapper queue
InputMapper.input_queue.append({
"action": InputMapper.Action.BUG_REPORT,
"timestamp_msec": Time.get_ticks_msec(),
})
# Process a frame — main.gd flushes the queue and triggers dialog
_instance._process(0.016)
# Assert: dialog should be active
var dialog: Node = _find_node_recursive(_instance, "BugReportDialog")
assert_that(dialog).is_not_null()
if dialog and dialog.has_method("is_active"):
assert_that(dialog.is_active()).override_failure_message(
"BugReportDialog should be active after BUG_REPORT action"
).is_true()
# Assert: no unintended state corruption
assert_that(GameState.current_monologue).override_failure_message(
"BUG_REPORT should not spawn a monologue"
).is_equal(mono_before)
assert_that(GameState.current_dialogue).override_failure_message(
"BUG_REPORT should not spawn a dialogue"
).is_equal(dialogue_before)
# -- Test 2: Gauntlet Progress UI Hidden in Non-Gauntlet Mode -----------------
# When #496 lands, it adds a room timer and personal-bests overlay.
# Anti-tedium guard: these must NOT be visible in normal (non-Gauntlet) play.
#
# Current state: no gauntlet UI exists in the scene tree.
# This test guards against #496 accidentally showing gauntlet UI in all modes.
func test_no_gauntlet_ui_visible_in_default_mode() -> void:
# Load main scene — represents non-Gauntlet (default) play mode
var scene := MAIN_SCENE
_instance = scene.instantiate()
auto_free(_instance)
add_child(_instance)
# Process one frame to initialize all children
_instance._process(0.016)
# Check that no gauntlet-specific UI nodes are visible in the scene tree.
# When #496 adds GauntletHUD/RoomTimer/PersonalBests, they must be hidden
# by default (only shown when gauntlet_mode is true in the snapshot).
var gauntlet_node_names := [
"GauntletHUD", "RoomTimer", "PersonalBests", "GauntletOverlay",
"GauntletTimer", "GauntletProgress",
]
for node_name in gauntlet_node_names:
var node: Node = _find_node_recursive(_instance, node_name)
if node != null and node is CanvasItem:
assert_that((node as CanvasItem).visible).override_failure_message(
"Gauntlet UI node '%s' must not be visible in non-Gauntlet mode" % node_name
).is_false()
func test_snapshot_without_room_id_shows_no_gauntlet_ui() -> void:
# A snapshot that lacks room_id and gauntlet_mode fields represents
# normal play. Apply it and verify no gauntlet state leaks into GameState.
var bytes := _make_snapshot_bytes({"tick": 1})
SimBridge.receive_bytes(bytes)
var snapshot: Variant = SimBridge._last_snapshot
assert_that(snapshot).is_not_null()
# Non-gauntlet snapshot: gauntlet fields must be present with default values.
# (Protocol.decode_snapshot always decodes gauntlet fields; non-gauntlet
# snapshots produce false/null defaults. Check values, not key presence.)
assert_that(snapshot.get("gauntlet_mode", false)).override_failure_message(
"Non-gauntlet snapshot must decode gauntlet_mode == false"
).is_false()
assert_that(snapshot.get("room_id")).override_failure_message(
"Non-gauntlet snapshot must decode room_id == null"
).is_null()
# Apply to GameState — gauntlet-related state should not exist
GameState.apply_snapshot(snapshot)
# #496: GameState now has room_id (Variant, null) and gauntlet_mode (bool, false).
# A non-gauntlet snapshot must leave these at their defaults.
assert_that(GameState.room_id).override_failure_message(
"GameState.room_id should be null in non-gauntlet mode"
).is_null()
assert_that(GameState.gauntlet_mode).override_failure_message(
"GameState.gauntlet_mode should be false in non-gauntlet mode"
).is_false()
func test_gauntlet_ui_stays_hidden_after_multiple_snapshots() -> void:
# Simulate several ticks of normal play — gauntlet UI must never appear.
var scene := MAIN_SCENE
_instance = scene.instantiate()
auto_free(_instance)
add_child(_instance)
# Process 5 frames with normal (non-gauntlet) snapshots
for tick in range(1, 6):
var bytes := _make_snapshot_bytes({"tick": tick})
SimBridge.receive_bytes(bytes)
_instance._process(0.016)
# After 5 frames, no gauntlet nodes should have appeared
var gauntlet_node_names := [
"GauntletHUD", "RoomTimer", "PersonalBests", "GauntletOverlay",
"GauntletTimer", "GauntletProgress",
]
for node_name in gauntlet_node_names:
var node: Node = _find_node_recursive(_instance, node_name)
if node != null and node is CanvasItem:
assert_that((node as CanvasItem).visible).override_failure_message(
"Gauntlet UI '%s' must stay hidden after %d non-gauntlet ticks" % [node_name, 5]
).is_false()
# -- GauntletHUD Feature Tests (#496) ----------------------------------------
# Timer lifecycle, personal bests, room change, visibility.
# Spec: sprint-9/client.md #496. Ticket: pql ticket show T-496.
func test_format_time_zero() -> void:
# Static utility: 0 seconds → "00:00"
var hud := _make_gauntlet_hud()
assert_that(hud._format_time(0.0)).is_equal("00:00")
func test_format_time_sub_minute() -> void:
# 47.9 seconds (truncates, no rounding) → "00:47"
var hud := _make_gauntlet_hud()
assert_that(hud._format_time(47.9)).is_equal("00:47")
func test_format_time_over_minute() -> void:
# 98.3 seconds → 1 min 38 sec → "01:38"
var hud := _make_gauntlet_hud()
assert_that(hud._format_time(98.3)).is_equal("01:38")
func test_gauntlet_hud_shows_when_gauntlet_mode_active() -> void:
# HUD must become visible when gauntlet_mode is true in the snapshot.
var hud := _make_gauntlet_hud()
assert_that(hud.visible).is_false()
GameState.gauntlet_mode = true
GameState.room_id = "room_1"
hud.update_from_state()
assert_that(hud.visible).override_failure_message(
"GauntletHUD must be visible when gauntlet_mode is true"
).is_true()
func test_gauntlet_hud_hides_when_gauntlet_mode_deactivates() -> void:
# HUD must hide when gauntlet_mode transitions from true to false.
var hud := _make_gauntlet_hud()
GameState.gauntlet_mode = true
GameState.room_id = "room_1"
hud.update_from_state()
assert_that(hud.visible).is_true()
# Switch to non-gauntlet
GameState.gauntlet_mode = false
GameState.room_id = null
hud.update_from_state()
assert_that(hud.visible).override_failure_message(
"GauntletHUD must hide when gauntlet_mode becomes false"
).is_false()
func test_gauntlet_hud_timer_starts_on_room_entry() -> void:
# Timer starts running and tracks room_id after room entry.
var hud := _make_gauntlet_hud()
GameState.gauntlet_mode = true
GameState.room_id = "room_1"
hud.update_from_state()
assert_that(hud.is_timer_running()).override_failure_message(
"Timer should be running after room entry"
).is_true()
assert_that(hud.get_current_room_id()).is_equal("room_1")
func test_gauntlet_hud_timer_increments_with_delta() -> void:
# Timer accumulates real time via _process(delta).
var hud := _make_gauntlet_hud()
GameState.gauntlet_mode = true
GameState.room_id = "room_1"
hud.update_from_state()
hud._process(0.5)
assert_that(hud.get_timer_seconds()).override_failure_message(
"Timer should increment by delta (0.5s)"
).is_equal_approx(0.5, 0.01)
hud._process(1.0)
assert_that(hud.get_timer_seconds()).override_failure_message(
"Timer should accumulate (0.5 + 1.0 = 1.5s)"
).is_equal_approx(1.5, 0.01)
func test_gauntlet_hud_timer_resets_on_room_change() -> void:
# Changing room_id resets the timer to 0 and starts counting for the new room.
var hud := _make_gauntlet_hud()
GameState.gauntlet_mode = true
GameState.room_id = "room_1"
hud.update_from_state()
hud._process(5.0)
assert_that(hud.get_timer_seconds() > 4.0).is_true()
# Change room
GameState.room_id = "room_2"
hud.update_from_state()
assert_that(hud.get_timer_seconds()).override_failure_message(
"Timer should reset to 0 on room change"
).is_equal_approx(0.0, 0.01)
assert_that(hud.get_current_room_id()).is_equal("room_2")
assert_that(hud.is_timer_running()).is_true()
func test_gauntlet_hud_records_personal_best() -> void:
# Completing a room (by entering a new room) records a personal best.
var hud := _make_gauntlet_hud()
GameState.gauntlet_mode = true
GameState.room_id = "room_1"
hud.update_from_state()
hud._process(10.0)
# Entering room_2 triggers _record_room_completion for room_1
GameState.room_id = "room_2"
hud.update_from_state()
assert_that(hud.get_personal_best("room_1")).override_failure_message(
"PB for room_1 should be ~10.0s after first completion"
).is_equal_approx(10.0, 0.1)
func test_gauntlet_hud_non_pb_does_not_overwrite() -> void:
# A worse time must not overwrite an existing personal best.
var hud := _make_gauntlet_hud()
hud._personal_bests["room_1"] = 5.0 # Existing PB of 5 seconds
GameState.gauntlet_mode = true
GameState.room_id = "room_1"
hud.update_from_state()
hud._process(10.0) # Worse time than existing PB
GameState.room_id = "room_2"
hud.update_from_state()
assert_that(hud.get_personal_best("room_1")).override_failure_message(
"PB should remain 5.0 when new time (10.0) is worse"
).is_equal_approx(5.0, 0.01)
func test_gauntlet_hud_pb_overwrites_when_faster() -> void:
# A faster time must overwrite an existing personal best.
var hud := _make_gauntlet_hud()
hud._personal_bests["room_1"] = 15.0 # Existing PB of 15 seconds
GameState.gauntlet_mode = true
GameState.room_id = "room_1"
hud.update_from_state()
hud._process(8.0) # Better time
GameState.room_id = "room_2"
hud.update_from_state()
assert_that(hud.get_personal_best("room_1")).override_failure_message(
"PB should update to 8.0 when new time beats old PB (15.0)"
).is_equal_approx(8.0, 0.1)
func test_gauntlet_hud_null_room_in_gauntlet_mode() -> void:
# Gauntlet mode active but no room_id yet — HUD visible, timer stopped.
var hud := _make_gauntlet_hud()
GameState.gauntlet_mode = true
GameState.room_id = null
hud.update_from_state()
assert_that(hud.visible).override_failure_message(
"HUD should be visible in gauntlet mode even without room_id"
).is_true()
assert_that(hud.is_timer_running()).override_failure_message(
"Timer should NOT run when room_id is null"
).is_false()
func test_gauntlet_hud_timer_paused_when_not_visible() -> void:
# Timer should not accumulate when HUD is not visible (non-gauntlet mode).
var hud := _make_gauntlet_hud()
GameState.gauntlet_mode = true
GameState.room_id = "room_1"
hud.update_from_state()
hud._process(2.0)
assert_that(hud.get_timer_seconds()).is_equal_approx(2.0, 0.01)
# Switch to non-gauntlet (hides HUD)
GameState.gauntlet_mode = false
hud.update_from_state()
hud._process(5.0) # Should NOT accumulate
assert_that(hud.get_timer_seconds()).override_failure_message(
"Timer must not accumulate when HUD is hidden"
).is_equal_approx(2.0, 0.01)
func test_gauntlet_hud_finalize_records_current_room() -> void:
# finalize() records the current room's time and stops the timer.
# Called on disconnect via _on_connection_state_changed.
var hud := _make_gauntlet_hud()
GameState.gauntlet_mode = true
GameState.room_id = "room_1"
hud.update_from_state()
hud._process(7.5)
hud.finalize()
assert_that(hud.is_timer_running()).override_failure_message(
"Timer should stop after finalize"
).is_false()
assert_that(hud.get_personal_best("room_1")).override_failure_message(
"Finalize should record current room's time as PB"
).is_equal_approx(7.5, 0.1)
func test_gauntlet_hud_session_tracks_attempts() -> void:
# Each room entry increments the attempt counter in _session_rooms.
var hud := _make_gauntlet_hud()
GameState.gauntlet_mode = true
GameState.room_id = "room_1"
hud.update_from_state()
hud._process(3.0)
# Re-enter same room (via room change and back)
GameState.room_id = "room_2"
hud.update_from_state()
GameState.room_id = "room_1"
hud.update_from_state()
assert_that(hud._session_rooms.has("room_1")).is_true()
assert_that(hud._session_rooms["room_1"]["attempts"]).override_failure_message(
"Room should show 2 attempts after two entries"
).is_equal(2)
func test_gauntlet_snapshot_roundtrip_via_apply() -> void:
# Snapshot with gauntlet fields applied to GameState, consumed by HUD.
# End-to-end: bytes → decode → apply → update_from_state.
var bytes := _make_snapshot_bytes({
"tick": 10,
"gauntlet_mode": true,
"room_id": "warehouse_01",
})
SimBridge.receive_bytes(bytes)
GameState.apply_snapshot(SimBridge._last_snapshot)
assert_that(GameState.gauntlet_mode).is_true()
assert_that(GameState.room_id).is_equal("warehouse_01")
var hud := _make_gauntlet_hud()
hud.update_from_state()
assert_that(hud.visible).is_true()
assert_that(hud.get_current_room_id()).is_equal("warehouse_01")
assert_that(hud.is_timer_running()).is_true()
# -- BugReportDialog Feature Tests (#495) ------------------------------------
# Pause/unpause lifecycle, wire guard, text render, state machine.
# Spec: sprint-9/client.md #495. Ticket: pql ticket show T-495.
func test_bug_report_sends_pause_on_open() -> void:
# start_capture() must send Pause to the server.
SimBridge.connect_to_sim()
SimBridge._test_input_queue.clear()
var dialog := _make_bug_report_dialog()
dialog.start_capture()
assert_that(dialog.is_active()).is_true()
assert_that(SimBridge._test_input_queue.has("Pause")).override_failure_message(
"Opening bug report should send Pause to server"
).is_true()
func test_bug_report_sends_unpause_on_close() -> void:
# _close() must send Unpause to the server.
SimBridge.connect_to_sim()
var dialog := _make_bug_report_dialog()
dialog.start_capture()
SimBridge._test_input_queue.clear()
dialog.close()
assert_that(dialog.is_active()).is_false()
assert_that(SimBridge._test_input_queue.has("Unpause")).override_failure_message(
"Closing bug report should send Unpause to server"
).is_true()
func test_bug_report_not_double_activatable() -> void:
# Calling start_capture() twice must not double-activate or send duplicate Pause.
SimBridge.connect_to_sim()
var dialog := _make_bug_report_dialog()
dialog.start_capture()
assert_that(dialog.is_active()).is_true()
SimBridge._test_input_queue.clear()
dialog.start_capture() # Second call — should be blocked
assert_that(SimBridge._test_input_queue.size()).override_failure_message(
"Double-activation should be blocked (no second Pause sent)"
).is_equal(0)
func test_bug_report_action_not_on_wire() -> void:
# BUG_REPORT is client-only — must not produce a wire-format action name.
var wire_name := SimBridge.action_enum_to_wire(InputMapper.Action.BUG_REPORT)
assert_that(wire_name).override_failure_message(
"BUG_REPORT must not produce a wire action name (client-only)"
).is_equal("")
func test_bug_report_render_text_contains_tick_and_entities() -> void:
# _render_snapshot_text() must include tick number and entity data from GameState.
GameState.current_tick = 42
GameState.player_position = Vector2(5.0, 7.0)
GameState.player_facing = "South"
GameState.player_stance = "Crouch"
GameState.visible_entities = [{
"entity_id": 1, "x": 5.0, "y": 7.0, "z": 0,
"kind": {"variant": "Player", "data": null}, "visibility": "Forward",
}]
GameState.visible_tiles = []
GameState.game_time = {"day": 1, "day_phase": "Evening", "tick_rate": "Full"}
GameState.current_monologue = null
GameState.current_dialogue = null
var dialog := _make_bug_report_dialog()
var text: String = dialog._render_snapshot_text()
assert_that(text.contains("t42")).override_failure_message(
"Render text should contain tick number"
).is_true()
assert_that(text.contains("Entities (1)")).override_failure_message(
"Render text should show entity count"
).is_true()
assert_that(text.contains("Player")).override_failure_message(
"Render text should contain entity kind"
).is_true()
assert_that(text.contains("South")).override_failure_message(
"Render text should contain player facing"
).is_true()
func test_bug_report_render_text_with_monologue_and_dialogue() -> void:
# _render_snapshot_text() should include active monologue and dialogue.
GameState.current_tick = 10
GameState.player_position = Vector2(3.0, 3.0)
GameState.player_facing = "North"
GameState.player_stance = "Walk"
GameState.visible_entities = []
GameState.visible_tiles = []
GameState.game_time = {"day": 0, "day_phase": "Morning", "tick_rate": "Full"}
GameState.current_monologue = {"text": "Something is wrong here."}
GameState.current_dialogue = {"npc_name": "Kael", "speech": "Who are you?"}
var dialog := _make_bug_report_dialog()
var text: String = dialog._render_snapshot_text()
assert_that(text.contains("Something is wrong here.")).override_failure_message(
"Render text should include monologue text"
).is_true()
assert_that(text.contains("Kael")).override_failure_message(
"Render text should include dialogue NPC name"
).is_true()
assert_that(text.contains("Who are you?")).override_failure_message(
"Render text should include dialogue speech"
).is_true()
func test_bug_report_render_text_empty_snapshot() -> void:
# Edge case: F12 pressed before any snapshot data arrives.
GameState.current_tick = 0
GameState.player_position = Vector2.ZERO
GameState.player_facing = "North"
GameState.player_stance = "Walk"
GameState.visible_entities = []
GameState.visible_tiles = []
GameState.game_time = {}
GameState.current_monologue = null
GameState.current_dialogue = null
var dialog := _make_bug_report_dialog()
var text: String = dialog._render_snapshot_text()
# Should not crash, should produce some output
assert_that(text.length() > 0).override_failure_message(
"Render text should not be empty even with no snapshot data"
).is_true()
assert_that(text.contains("Entities (0)")).override_failure_message(
"Empty snapshot should show 0 entities"
).is_true()
# -- Helper: recursive node search --------------------------------------------
func _find_node_recursive(root: Node, target_name: String) -> Node:
if root.name == target_name:
return root
for child in root.get_children():
var found := _find_node_recursive(child, target_name)
if found != null:
return found
return null