test(client): triage 3 chronically broken test suites (Task #21)

Clean the regression signal for the remaining MetaScreen workstreams
by either fixing or surgically skipping tests that had been failing
for design reasons or against stale APIs.

test_sprint2_proof.gd — all 3 tests prefixed skip_test_. Root cause:
hardcoded Sprint 2 room coordinates + protocol v1 assumptions; not
adaptable to current protocol v23 or Gauntlet layout. Suite now reports
0 tests rather than 14 failures / 3 errors.

test_dialogue_sprint18.gd — 40 tests pass (was 48 errors / 3 failures).
Root cause of the errors: GameState.has() calls hitting Node.has()
which does not exist. Fixed by removing guards and accessing
GameState.current_examine_result directly (present since v14 / #174).
Two real bugs surfaced after the error noise cleared; skipped with
ticket references:
- #866 (high): dialogue_box._escape_bbcode chains .replace('[','[lb]')
  .replace(']','[rb]') which turns [lb] into [lb[rb]. BBCode injection
  guard broken.
- #867: confrontation_monologue signal doesn't fire in headless; the
  create_tween call in _start_confrontation_beat likely aborts before
  the emit.

test_client_p2.gd — 26 tests pass (was 2 failures). Three #117-fallout
camera-smoothing tests skipped (main.gd disables
position_smoothing_enabled permanently by design since #117 manual
lerp). One MonologueDisplay API test skipped pending #864 (asserts
mono.is_visible, but the display was refactored to _visible:
Array[Dictionary]).

No production code changes. Every skipped test carries a skip_test_
prefix + inline TODO pointing at the owning ticket. Bug tickets #864,
#866, #867 filed to the backlog.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-20 19:33:57 +02:00
co-authored by Claude Opus 4.6
parent 256703e9b7
commit 88202ad679
3 changed files with 38 additions and 47 deletions
+13 -12
View File
@@ -67,10 +67,10 @@ func test_camera_zoom_default_2x() -> void:
assert_that(camera.zoom).is_equal(Vector2(2, 2))
func test_camera_smoothing_convergence() -> void:
# P2-C02: After first _process, smoothing re-enables for gameplay feel.
# After several frames, camera position should still match player position
# (smoothing converges because target == position when stationary).
func skip_test_camera_smoothing_convergence() -> void:
# P2-C02: STALE — #117 permanently disables Camera2D.position_smoothing_enabled
# in main.gd _ready() (manual lerp approach). Assertion is_true() no longer valid.
# TODO: rewrite against manual lerp behaviour once lerp test API is available.
var inst := _make_scene()
var camera: Camera2D = inst.get_node("Camera2D")
# First frame re-enables smoothing
@@ -97,8 +97,11 @@ func test_camera_viewport_tracks_player_position() -> void:
).is_equal(expected)
func test_camera_follows_player_after_movement() -> void:
# P2-C04: After player moves, camera position updates to new player position.
func skip_test_camera_follows_player_after_movement() -> void:
# P2-C04: STALE — #117 switched camera to manual lerp; after 1 frame the camera
# has not converged to player_position * TILE_SIZE. Exact equality assertion fails.
# TODO: rewrite to assert directional movement only (y > initial_pos.y) OR
# run enough frames for lerp convergence before asserting exact position.
var inst := _make_scene()
var camera: Camera2D = inst.get_node("Camera2D")
var initial_pos := camera.global_position
@@ -192,12 +195,10 @@ func test_entity_player_color_regardless_of_sector() -> void:
# -- UI (7) --------------------------------------------------------------------
func test_monologue_display_visible_hidden() -> void:
# P2-U01: MonologueDisplay starts hidden, becomes visible after show_monologue.
# Note: mono.is_visible is a custom bool property on MonologueDisplay
# (monologue_display.gd:11), not the built-in CanvasItem.is_visible() method.
# The monologue uses tween alpha for visual hide/show, so the built-in
# .visible stays true — we test the script's own state tracking.
func skip_test_monologue_display_visible_hidden() -> void:
# P2-U01: BROKEN — MonologueDisplay no longer has an `is_visible` bool property.
# Current API uses `_visible: Array[Dictionary]` (monologue_display.gd).
# TODO: rewrite against _visible array and/or a public visibility accessor.
var inst := _make_scene()
var mono = inst.get_node("UILayer/MonologueDisplay")
assert_that(mono.is_visible).override_failure_message(
+13 -26
View File
@@ -53,14 +53,12 @@ func _make_options(texts: Array[String], confrontation_flags: Array[bool] = [])
func before_test() -> void:
GameState.current_dialogue = null
GameState.dialogue_active = false
if GameState.has("current_examine_result"):
GameState.current_examine_result = null
GameState.current_examine_result = null
func after_test() -> void:
GameState.current_dialogue = null
GameState.dialogue_active = false
if GameState.has("current_examine_result"):
GameState.current_examine_result = null
GameState.current_examine_result = null
# ---------------------------------------------------------------------------
@@ -184,8 +182,10 @@ func test_d063_dim_alpha_is_set() -> void:
box.queue_free()
func test_d063_confrontation_signal_fires_on_confrontation_option() -> void:
func skip_test_d063_confrontation_signal_fires_on_confrontation_option() -> void:
## D-063: Selecting a confrontation option fires confrontation_monologue signal.
## BROKEN (#867): signal_fired stays false in headless; create_tween() before emit
## may abort _start_confrontation_beat if panel node is null. Bug filed.
## This delivers the 1-2 second internal monologue beat to MonologueDisplay.
var box := _make_dialogue_box()
if box == null: return
@@ -335,28 +335,22 @@ func test_gamestate_current_dialogue_options_survive_roundtrip() -> void:
# ---------------------------------------------------------------------------
func test_gamestate_examine_result_field_exists() -> void:
## GameState must have a current_examine_result field (Sprint 18, #174).
## Fails until Stig adds the field to game_state.gd.
assert_bool(GameState.has("current_examine_result")).override_failure_message(
"GameState must have 'current_examine_result' field (Sprint 18 #174 — add to game_state.gd)"
## GameState must have a current_examine_result field (v14, #174).
## Field confirmed present in game_state.gd — verified by property existence check.
assert_bool("current_examine_result" in GameState).override_failure_message(
"GameState must have 'current_examine_result' field (v14, #174)"
).is_true()
func test_gamestate_examine_result_null_by_default() -> void:
## current_examine_result defaults to null (no examine active).
if not GameState.has("current_examine_result"):
push_warning("test_gamestate_examine_result_null_by_default: field not yet added — skip")
return
GameState.current_examine_result = null
assert_that(GameState.current_examine_result).is_null()
func test_gamestate_examine_result_set_from_snapshot() -> void:
## apply_snapshot with examine_result dict populates current_examine_result.
## Wire format (joint.md): {entity_id: int, text: String, confidence: String}
if not GameState.has("current_examine_result"):
push_warning("test_gamestate_examine_result_set_from_snapshot: field not yet added — skip")
return
## Wire format: {entity_id: int, text: String, confidence: String}
GameState.apply_snapshot({
"tick": 5,
"examine_result": {
@@ -372,9 +366,6 @@ func test_gamestate_examine_result_set_from_snapshot() -> void:
func test_gamestate_examine_result_null_when_absent() -> void:
## apply_snapshot without examine_result must clear the field.
## Prevents stale examine overlay persisting beyond auto-dismiss window.
if not GameState.has("current_examine_result"):
push_warning("test_gamestate_examine_result_null_when_absent: field not yet added — skip")
return
GameState.current_examine_result = {"entity_id": 5, "text": "Stale.", "confidence": "Suspects"}
GameState.apply_snapshot({"tick": 6})
assert_that(GameState.current_examine_result).is_null()
@@ -382,18 +373,12 @@ func test_gamestate_examine_result_null_when_absent() -> void:
func test_gamestate_examine_result_null_when_non_dict() -> void:
## Malformed examine_result (not a dict) must be rejected.
if not GameState.has("current_examine_result"):
push_warning("test_gamestate_examine_result_null_when_non_dict: field not yet added — skip")
return
GameState.apply_snapshot({"tick": 1, "examine_result": "bad-value"})
assert_that(GameState.current_examine_result).is_null()
func test_gamestate_examine_result_entity_id_survives_roundtrip() -> void:
## entity_id is needed to anchor the overlay above the correct entity.
if not GameState.has("current_examine_result"):
push_warning("test_gamestate_examine_result_entity_id_survives_roundtrip: field not yet added — skip")
return
GameState.apply_snapshot({
"tick": 1,
"examine_result": {"entity_id": 99, "text": "Observed.", "confidence": "Direct"},
@@ -407,8 +392,10 @@ func test_gamestate_examine_result_entity_id_survives_roundtrip() -> void:
## Note: dialogue_box.gd has no class_name — call _escape_bbcode via instance.
# ---------------------------------------------------------------------------
func test_escape_bbcode_brackets_in_server_text() -> void:
func skip_test_escape_bbcode_brackets_in_server_text() -> void:
## _escape_bbcode must convert '[' to '[lb]' to prevent BBCode injection.
## BROKEN (#866): chained replace('[', '[lb]').replace(']', '[rb]') corrupts the
## [lb] escape — result is [lb[rb]...] instead of [lb]...]]. Bug filed.
## Regression test: a malicious NPC name like "[wave]Evil[/wave]" must render
## as plain text in the dialogue log.
var box := _make_dialogue_box()
+12 -9
View File
@@ -1,11 +1,14 @@
## Sprint 2 Proof: Fog of Perception (#357)
## Verifies all 7 acceptance criteria through the full server pipeline:
## AC1: Player moves, AC2: Camera follows (via player_position),
## AC3: Tiles render (visible_tiles non-empty), AC4: Entities via LOS,
## AC5: Fog (not all tiles visible), AC6: Walls hide, AC7: Corner reveal.
## Requires: server binary built (cargo build in server/)
## Verifies all 7 acceptance criteria through the full server pipeline.
##
## Server proof room layout:
## SUITE DISABLED (sprint-36): Sprint 2 ACs are long satisfied.
## The room coordinates and player spawn positions below are hardcoded from
## the Sprint 2 room layout, which has evolved (protocol is now v23; Gauntlet
## room layout is different). Live server testing via the Gauntlet infrastructure
## supersedes these tests. Rewrite against the current Gauntlet rooms if
## per-AC regression coverage is needed again.
##
## Server proof room layout (Sprint 2 — stale):
## (16,13) = NPC1 (16,14) = WALL (16,16) = Player start
## (14,18) = NPC2 (18,14) = NPC3
## Player facing North → NPC1 blocked by wall.
@@ -111,7 +114,7 @@ func _connect_to_server() -> bool:
# -- AC#1, AC#2, AC#3, AC#5: Movement, camera, tiles, fog -------------------------
func test_proof_player_moves_and_v2_snapshot() -> void:
func skip_test_proof_player_moves_and_v2_snapshot() -> void:
var ok := await _connect_to_server()
if not ok:
return
@@ -142,7 +145,7 @@ func test_proof_player_moves_and_v2_snapshot() -> void:
# -- AC#6: Wall hides entity -------------------------------------------------------
func test_proof_wall_hides_entity() -> void:
func skip_test_proof_wall_hides_entity() -> void:
var ok := await _connect_to_server()
if not ok:
return
@@ -164,7 +167,7 @@ func test_proof_wall_hides_entity() -> void:
# -- AC#4, AC#7: Entity appears via LOS / corner reveal ----------------------------
func test_proof_corner_reveal() -> void:
func skip_test_proof_corner_reveal() -> void:
var ok := await _connect_to_server()
if not ok:
return