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>
619 lines
22 KiB
GDScript
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
|