Files
settled-reach/client/tests/test_sprint2_proof.gd
T
jpmschweitzerandClaude Opus 4.6 88202ad679 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>
2026-04-20 19:33:57 +02:00

195 lines
6.4 KiB
GDScript

## Sprint 2 Proof: Fog of Perception (#357)
## Verifies all 7 acceptance criteria through the full server pipeline.
##
## 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.
## Move East+North around the wall → NPC1 becomes visible.
class_name TestSprint2Proof
extends GdUnitTestSuite
const CONNECT_TIMEOUT: float = 3.0
const RESPONSE_TIMEOUT: float = 5.0
const MAX_PORT_ATTEMPTS: int = 5
var _server_pid: int = -1
var _bridge: LocalBridge = null
var _test_port: int = 0
func _server_binary_path() -> String:
var project_dir := ProjectSettings.globalize_path("res://")
return project_dir.path_join("../server/target/debug/settled-reach-server")
static func _random_test_port() -> int:
return 49152 + (randi() % (65535 - 49152 + 1))
func _spawn_server(server_path: String) -> bool:
for attempt in range(MAX_PORT_ATTEMPTS):
_test_port = _random_test_port()
var addr := "127.0.0.1:%d" % _test_port
_server_pid = OS.create_process(server_path, [addr])
if _server_pid <= 0:
continue
await get_tree().create_timer(0.15).timeout
if OS.is_process_running(_server_pid):
return true
_server_pid = -1
return false
func after_test() -> void:
if _bridge != null:
_bridge.disconnect_from_server()
_bridge = null
if _server_pid > 0 and OS.is_process_running(_server_pid):
OS.kill(_server_pid)
_server_pid = -1
## Send a batch input and receive the snapshot response.
func _send_and_receive(action_name: String, tick: int = 0) -> Variant:
var inputs: Array = [{"tick": tick, "action_name": action_name}]
var encoded := Protocol.encode_player_inputs(inputs)
assert_that(encoded.size()).is_greater(0)
var send_err := _bridge.send_message(encoded)
assert_that(send_err).is_equal(OK)
var snapshot_bytes := PackedByteArray()
var elapsed := 0.0
while elapsed < RESPONSE_TIMEOUT:
_bridge.poll()
snapshot_bytes = _bridge.poll_message()
if snapshot_bytes.size() > 0:
break
await get_tree().create_timer(0.05).timeout
elapsed += 0.05
assert_that(snapshot_bytes.size()).is_greater(0)
var snapshot: Variant = Protocol.decode_snapshot(snapshot_bytes)
assert_that(snapshot).is_not_null()
return snapshot
## Connect to server, returning true on success.
func _connect_to_server() -> bool:
var server_path := _server_binary_path()
if not FileAccess.file_exists(server_path):
push_warning("Sprint 2 proof skipped: server binary not found at %s" % server_path)
return false
var spawned := await _spawn_server(server_path)
if not spawned:
return false
_bridge = LocalBridge.new()
var elapsed := 0.0
while elapsed < CONNECT_TIMEOUT:
if _server_pid > 0 and not OS.is_process_running(_server_pid):
push_warning("Server process died during connection")
return false
if _bridge.get_status() == StreamPeerTCP.STATUS_NONE:
_bridge.connect_to_server("127.0.0.1", _test_port)
_bridge.poll()
if _bridge.get_status() == StreamPeerTCP.STATUS_CONNECTED:
return true
if _bridge.get_status() == StreamPeerTCP.STATUS_ERROR:
_bridge.disconnect_from_server()
_bridge.reset()
await get_tree().create_timer(0.1).timeout
elapsed += 0.1
return false
# -- AC#1, AC#2, AC#3, AC#5: Movement, camera, tiles, fog -------------------------
func skip_test_proof_player_moves_and_v2_snapshot() -> void:
var ok := await _connect_to_server()
if not ok:
return
var snapshot: Dictionary = await _send_and_receive("MoveNorth")
# AC#1: Player moved from (16,16) to (16,15)
var player: Dictionary = {}
for entity in snapshot.entities:
if entity.kind.variant == "Player":
player = entity
break
assert_that(player.size()).is_greater(0)
assert_float(player.x).is_equal_approx(16.5, 0.001)
assert_float(player.y).is_equal_approx(15.5, 0.001)
# v4 protocol fields present
assert_that(snapshot.version).is_equal(Protocol.PROTOCOL_VERSION)
assert_that(snapshot.player_facing).is_equal("North")
assert_that(snapshot.game_time).is_not_null()
# AC#3: Tiles flowing through bridge (visible_tiles non-empty)
assert_that(snapshot.visible_tiles.size()).is_greater(0)
# AC#5: Not all 1024 tiles visible — blind spot behind player
assert_that(snapshot.visible_tiles.size()).is_less(1024)
# -- AC#6: Wall hides entity -------------------------------------------------------
func skip_test_proof_wall_hides_entity() -> void:
var ok := await _connect_to_server()
if not ok:
return
# After MoveNorth: player at (16,15) facing North.
# Wall at (16,14) blocks LOS to NPC1 at (16,13).
var snapshot: Dictionary = await _send_and_receive("MoveNorth")
# NPC1 at (16,13) should be hidden — wall at (16,14) blocks LOS.
# Other NPCs (NPC2 at (14,18), NPC3 at (18,14)) may be visible.
var hidden_npc_visible := false
for entity in snapshot.entities:
if entity.kind.variant == "Npc":
if is_equal_approx(entity.x, 16.5) and is_equal_approx(entity.y, 13.5):
hidden_npc_visible = true
break
assert_that(hidden_npc_visible).is_false()
# -- AC#4, AC#7: Entity appears via LOS / corner reveal ----------------------------
func skip_test_proof_corner_reveal() -> void:
var ok := await _connect_to_server()
if not ok:
return
# Step 1: Move East twice to get beside the wall
# (16,16) → MoveEast → (17,16) → MoveEast → (18,16)
await _send_and_receive("MoveEast", 0)
await _send_and_receive("MoveEast", 1)
# Step 2: Move North past the wall line (y=14)
# (18,16) → MoveNorth → (18,15) → MoveNorth → (18,14) → MoveNorth → (18,13)
await _send_and_receive("MoveNorth", 2)
await _send_and_receive("MoveNorth", 3)
var snapshot: Dictionary = await _send_and_receive("MoveNorth", 4)
# Player at (18,13) facing North. NPC1 at (16,13) is 2 tiles west —
# within peripheral cone, no wall between. NPC1 should be visible.
var npc1_found := false
for entity in snapshot.entities:
if entity.kind.variant == "Npc":
if is_equal_approx(entity.x, 16.5) and is_equal_approx(entity.y, 13.5):
npc1_found = true
break
assert_that(npc1_found).is_true()