Files
settled-reach/client/tests/test_hub_teleport.gd
T
jpmschweitzerandClaude Sonnet 4.6 d72fcc7847 fix(protocol): drop PROTOCOL_VERSION lockstep — D-192 (#875)
Removes the version-mismatch guard from Protocol.decode_snapshot() and the
PROTOCOL_VERSION constant from the client (server side done in #874).

Core changes:
- protocol.gd: remove const PROTOCOL_VERSION, remove version mismatch guard,
  remove "version" from return dict, add gauntlet_mode/room_id decode
- sim_bridge.gd: remove handshake version check; relax handshake guard to
  require only a valid Dictionary (server no longer sends protocol_version);
  emit handshake_complete(0) for API compat
- loading_screen.gd: drop "· protocol N" suffix from version label
- test_harness.gd: replace Protocol.PROTOCOL_VERSION with literal 23

Test updates (21 files): replace "version": Protocol.PROTOCOL_VERSION with
"version": 23 in all snapshot bytes dicts; remove snapshot.version == N
assertions; remove version-rejection tests (test_rejects_version_6,
test_decode_snapshot_rejects_missing_version, test_decode_snapshot_rejects_old_version,
test_protocol_rejects_version_mismatch, test_sim_bridge_test_snapshot_uses_current_protocol_version).

Also includes: #872 bookmark_catalog carry-forward regression test, and
#873 merge-path flow tests (test_merge_path_flows_sprint37.gd).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 17:29:49 +02:00

289 lines
10 KiB
GDScript

## #501: Hub teleport client UX — QA test suite
## Spec refs: D-020 (protocol), D-030 (testability)
## Sprint Completion Proof (joint.md):
## - Home key sends TeleportToHub in Gauntlet mode
## - 0.3s fade-to-black-and-back plays on teleport
## - Dialogue/monologue/interaction buffer cleared on teleport
## - Non-Gauntlet: action rejected, client shows no effect
class_name TestHubTeleport
extends GdUnitTestSuite
const _THRESHOLD: float = 5.0 # Mirror of main.gd TELEPORT_DISTANCE_THRESHOLD
# -- Fixtures ------------------------------------------------------------------
var _gauntlet_snapshot := {
"tick": 1,
"version": 23,
"game_time": {"day": 0, "time_of_day": 100, "day_phase": "Morning", "tick_rate": "Full"},
"player_facing": "North",
"player_stance": "Walk",
"player_inventory": [],
"entities": [
{"entity_id": 1, "x": 50.0, "y": 50.0, "z": 0, "kind": {"variant": "Player", "data": null}, "visibility": "Forward"},
],
"tiles": [],
"visible_tiles": [],
"visible_positions": [],
"nearby_interactions": [],
"current_monologue": null,
"current_dialogue": null,
"pending_recognitions": [],
"gauntlet_mode": true,
"room_id": "proof_room",
}
var _normal_snapshot := {
"tick": 1,
"version": 23,
"game_time": {"day": 0, "time_of_day": 100, "day_phase": "Morning", "tick_rate": "Full"},
"player_facing": "North",
"player_stance": "Walk",
"player_inventory": [],
"entities": [
{"entity_id": 1, "x": 50.0, "y": 50.0, "z": 0, "kind": {"variant": "Player", "data": null}, "visibility": "Forward"},
],
"tiles": [],
"visible_tiles": [],
"visible_positions": [],
"nearby_interactions": [],
"current_monologue": null,
"current_dialogue": null,
"pending_recognitions": [],
}
func before_test() -> void:
GameState.gauntlet_mode = false
GameState.current_monologue = null
GameState.current_dialogue = null
GameState.dialogue_active = false
GameState.room_id = null
InputMapper.input_queue.clear()
SimBridge.reset_test_state()
# -- InputMapper: TELEPORT_HUB action enum ------------------------------------
func test_teleport_hub_action_exists() -> void:
# Verify the enum value exists and is distinct
var action: int = InputMapper.Action.TELEPORT_HUB
assert_that(action).is_not_equal(InputMapper.Action.INTERACT)
assert_that(action).is_not_equal(InputMapper.Action.MOVE_NORTH)
# -- InputMapper: Gauntlet mode guard -----------------------------------------
func test_teleport_hub_blocked_outside_gauntlet() -> void:
# Non-gauntlet: Home key input should NOT queue TELEPORT_HUB
GameState.gauntlet_mode = false
InputMapper.input_queue.clear()
var event := InputEventKey.new()
event.physical_keycode = KEY_HOME
event.pressed = true
InputMapper._unhandled_input(event)
var has_teleport := false
for entry in InputMapper.input_queue:
if entry.action == InputMapper.Action.TELEPORT_HUB:
has_teleport = true
assert_that(has_teleport).is_false()
func test_teleport_hub_allowed_in_gauntlet_mode() -> void:
# Gauntlet mode: Home key SHOULD queue TELEPORT_HUB
GameState.gauntlet_mode = true
InputMapper.input_queue.clear()
var event := InputEventKey.new()
event.physical_keycode = KEY_HOME
event.pressed = true
InputMapper._unhandled_input(event)
var has_teleport := false
for entry in InputMapper.input_queue:
if entry.action == InputMapper.Action.TELEPORT_HUB:
has_teleport = true
assert_that(has_teleport).is_true()
# -- GameState: gauntlet_mode from snapshot ------------------------------------
func test_gauntlet_mode_set_from_snapshot() -> void:
GameState.apply_snapshot(_gauntlet_snapshot)
assert_that(GameState.gauntlet_mode).is_true()
assert_that(GameState.room_id).is_equal("proof_room")
func test_gauntlet_mode_false_when_absent() -> void:
GameState.apply_snapshot(_normal_snapshot)
assert_that(GameState.gauntlet_mode).is_false()
assert_that(GameState.room_id).is_null()
func test_gauntlet_mode_transitions_off() -> void:
# Gauntlet on → off: mode should clear
GameState.apply_snapshot(_gauntlet_snapshot)
assert_that(GameState.gauntlet_mode).is_true()
GameState.apply_snapshot(_normal_snapshot)
assert_that(GameState.gauntlet_mode).is_false()
# -- SimBridge: wire format encoding -------------------------------------------
func test_teleport_hub_wire_name() -> void:
# TELEPORT_HUB must encode to "TeleportToHub" on the wire (matching Rust PlayerAction)
var wire_name := SimBridge.action_enum_to_wire(InputMapper.Action.TELEPORT_HUB)
assert_that(wire_name).is_equal("TeleportToHub")
func test_teleport_hub_wire_not_empty() -> void:
# Wire name must not be empty (empty = client-only, not sent to server)
var wire_name := SimBridge.action_enum_to_wire(InputMapper.Action.TELEPORT_HUB)
assert_that(wire_name.is_empty()).is_false()
func test_teleport_hub_encode_roundtrip() -> void:
# Verify MessagePack encode→decode roundtrip for TeleportToHub
var encoded: PackedByteArray = Protocol.encode_player_input(42, "TeleportToHub")
assert_that(encoded.size()).is_greater(0)
var decoded: Variant = Protocol.decode_player_input(encoded)
assert_that(decoded).is_not_null()
assert_that(decoded.tick).is_equal(42)
assert_that(decoded.action.variant).is_equal("TeleportToHub")
assert_that(decoded.action.data).is_null()
# -- SimBridge: test mode teleport behavior ------------------------------------
func test_test_mode_teleport_resets_position() -> void:
# In test mode, TeleportToHub should reset player to hub spawn (10, 10)
SimBridge.reset_test_state()
# Move player away first
SimBridge._test_player_pos = Vector2i(50, 50)
SimBridge._test_input_queue.append("TeleportToHub")
var snap: Dictionary = SimBridge._test_snapshot()
# Player should be back at hub spawn
var player_entity: Dictionary = snap.entities[0]
assert_that(player_entity.x).is_equal(10.0)
assert_that(player_entity.y).is_equal(10.0)
func test_test_mode_teleport_clears_dialogue() -> void:
# TeleportToHub in test mode should clear dialogue state
SimBridge.reset_test_state()
SimBridge._test_in_dialogue = true
SimBridge._test_input_queue.append("TeleportToHub")
SimBridge._test_snapshot()
assert_that(SimBridge._test_in_dialogue).is_false()
# -- Teleport detection -------------------------------------------------------
# Threshold constant lives on the main scene node (TELEPORT_DISTANCE_THRESHOLD = 5.0).
# These tests verify the distance math against that threshold.
func test_detect_teleport_large_jump() -> void:
# Position jump > threshold should be detected as teleport
var old_pos := Vector2(10.0, 10.0)
var new_pos := Vector2(50.0, 50.0)
assert_that(old_pos.distance_to(new_pos) > _THRESHOLD).is_true()
func test_detect_teleport_normal_movement() -> void:
# Normal 1-tile movement should NOT be detected as teleport
var old_pos := Vector2(10.0, 10.0)
var new_pos := Vector2(11.0, 10.0)
assert_that(old_pos.distance_to(new_pos) > _THRESHOLD).is_false()
func test_detect_teleport_diagonal_movement() -> void:
# Diagonal movement (1,1) — distance ~1.41, not a teleport
var old_pos := Vector2(10.0, 10.0)
var new_pos := Vector2(11.0, 11.0)
assert_that(old_pos.distance_to(new_pos) > _THRESHOLD).is_false()
func test_detect_teleport_boundary_exactly_threshold() -> void:
# Exactly threshold — should NOT trigger (> not >=)
var old_pos := Vector2(10.0, 10.0)
var new_pos := Vector2(15.0, 10.0)
assert_that(old_pos.distance_to(new_pos) > _THRESHOLD).is_false()
func test_detect_teleport_boundary_just_over() -> void:
# Just over threshold — should trigger
var old_pos := Vector2(10.0, 10.0)
var new_pos := Vector2(15.1, 10.0)
assert_that(old_pos.distance_to(new_pos) > _THRESHOLD).is_true()
# -- Buffer clearing on teleport -----------------------------------------------
func test_teleport_clears_dialogue_in_test_mode() -> void:
# TeleportToHub in test mode should clear dialogue state via the pipeline
SimBridge.reset_test_state()
SimBridge._test_in_dialogue = true
SimBridge._test_input_queue.append("TeleportToHub")
var snap: Dictionary = SimBridge._test_snapshot()
# Dialogue should be cleared by teleport
assert_that(SimBridge._test_in_dialogue).is_false()
assert_that(snap.current_dialogue).is_null()
func test_teleport_resets_position_in_test_mode() -> void:
# TeleportToHub must reset to hub spawn and clear dialogue (integration)
SimBridge.reset_test_state()
SimBridge._test_player_pos = Vector2i(50, 50)
SimBridge._test_in_dialogue = true
SimBridge._test_input_queue.append("TeleportToHub")
var snap: Dictionary = SimBridge._test_snapshot()
var player: Dictionary = snap.entities[0]
assert_that(player.x).is_equal(10.0)
assert_that(player.y).is_equal(10.0)
assert_that(SimBridge._test_in_dialogue).is_false()
# -- Send input integration (test mode) ----------------------------------------
func test_send_teleport_hub_in_test_mode() -> void:
# Verify send_input accepts TELEPORT_HUB in test mode
SimBridge.reset_test_state()
SimBridge.state = SimBridge.ConnectionState.CONNECTED
var err := SimBridge.send_input({
"action": InputMapper.Action.TELEPORT_HUB,
"timestamp_msec": 12345,
})
assert_that(err).is_equal(OK)
func test_send_teleport_hub_queues_wire_action() -> void:
# Verify TELEPORT_HUB is queued as "TeleportToHub" in test mode
SimBridge.reset_test_state()
SimBridge.state = SimBridge.ConnectionState.CONNECTED
SimBridge.send_input({
"action": InputMapper.Action.TELEPORT_HUB,
"timestamp_msec": 12345,
})
assert_that(SimBridge._test_input_queue.has("TeleportToHub")).is_true()
func test_gauntlet_mode_from_test_snapshot() -> void:
# Verify test snapshot includes gauntlet_mode field
SimBridge.reset_test_state()
SimBridge._test_gauntlet_mode = true
var snap: Dictionary = SimBridge._test_snapshot()
assert_that(snap.gauntlet_mode).is_true()
SimBridge._test_gauntlet_mode = false
snap = SimBridge._test_snapshot()
assert_that(snap.gauntlet_mode).is_false()
# -- Live mode outbound encoding -----------------------------------------------
func test_teleport_hub_outbound_entry() -> void:
# In live mode, TELEPORT_HUB should produce a valid outbound buffer entry
# (We can't test full live mode in unit tests, but we test the encode path)
var encoded: PackedByteArray = Protocol.encode_player_input(100, "TeleportToHub")
assert_that(encoded.size()).is_greater(0)
# Decode and verify
var decoded: Variant = Protocol.decode_player_input(encoded)
assert_that(decoded.action.variant).is_equal("TeleportToHub")