## Sprint 20 #557: GameState.apply_snapshot() refactor tests. ## ## Verifies D-020 compliance: apply_snapshot() reads server-authoritative values ## for stationary_ticks and zone_id directly from the snapshot when present, ## and degrades gracefully when the server has not yet added these fields. ## ## Does NOT replace test_game_state.gd or test_snapshot_zone_id.gd — those cover ## existing behaviour. This file covers the new code paths added in Sprint 20. ## ## D-030: fixture-based, server-free, no subprocess required. class_name TestGameStateSprint20 extends GdUnitTestSuite func before_each() -> void: GameState.stationary_ticks = 0 SnapshotHandler._prev_player_position = Vector2(-1e9, -1e9) GameState.current_zone_id = "" GameState.player_position = Vector2.ZERO GameState.visible_tiles = [] GameState.visible_positions = {} GameState.visibility_sectors = {} # -- stationary_ticks: server-authoritative path (D-020) ---------------------- func test_stationary_ticks_reads_server_value_when_present() -> void: # When snapshot includes stationary_ticks, apply_snapshot() must use the # server value directly without client-side accumulation (D-020). var snapshot := { "tick": 5, "entities": [ {"entity_id": 1, "x": 10.0, "y": 10.0, "z": 0, "kind": {"variant": "Player", "data": null}}, ], "stationary_ticks": 42, } GameState.apply_snapshot(snapshot) assert_int(GameState.stationary_ticks).override_failure_message( "apply_snapshot() must read stationary_ticks=42 from snapshot (D-020)" ).is_equal(42) func test_stationary_ticks_server_value_does_not_accumulate() -> void: # Server-sent value must be assigned directly — NOT added to existing value. # Two calls with stationary_ticks=10 must yield 10, not 20. var snapshot := { "tick": 1, "entities": [ {"entity_id": 1, "x": 5.0, "y": 5.0, "z": 0, "kind": {"variant": "Player", "data": null}}, ], "stationary_ticks": 10, } GameState.apply_snapshot(snapshot) GameState.apply_snapshot(snapshot) assert_int(GameState.stationary_ticks).override_failure_message( "Server value must be assigned directly, not accumulated (D-020)" ).is_equal(10) func test_stationary_ticks_server_can_reset_to_zero() -> void: # Server sends 0 when player moves — client must accept this reset. GameState.stationary_ticks = 50 var snapshot := { "tick": 2, "entities": [ {"entity_id": 1, "x": 5.0, "y": 5.0, "z": 0, "kind": {"variant": "Player", "data": null}}, ], "stationary_ticks": 0, } GameState.apply_snapshot(snapshot) assert_int(GameState.stationary_ticks).override_failure_message( "Server reset to 0 must override client-held value" ).is_equal(0) # -- stationary_ticks: DEPRECATED fallback (graceful degradation) -------------- func test_stationary_ticks_fallback_when_field_absent() -> void: # When snapshot has no stationary_ticks, client-side accumulation must still # run (backward compat until server ships field). No crash. var snapshot := { "tick": 1, "entities": [ {"entity_id": 1, "x": 10.0, "y": 10.0, "z": 0, "kind": {"variant": "Player", "data": null}}, ], } GameState.apply_snapshot(snapshot) # position changes from ZERO → resets to 0 GameState.apply_snapshot(snapshot) # position unchanged → increments assert_int(GameState.stationary_ticks).override_failure_message( "Client-side fallback must increment stationary_ticks when field absent" ).is_greater(0) func test_apply_snapshot_missing_stationary_ticks_no_crash() -> void: # Snapshots lacking stationary_ticks must not crash apply_snapshot(). var snapshot := {"tick": 1, "entities": []} # No assertion needed beyond confirming no exception is raised. GameState.apply_snapshot(snapshot) assert_bool(true).is_true() # -- zone_id: server-authoritative path (D-020/D-073) ------------------------- func test_current_zone_id_reads_server_value_when_present() -> void: # When snapshot includes top-level zone_id, apply_snapshot() must use it # directly without tile lookup (D-020). var snapshot := { "tick": 1, "entities": [ {"entity_id": 1, "x": 5.0, "y": 5.0, "z": 0, "kind": {"variant": "Player", "data": null}}, ], "zone_id": "zone_server_direct", } GameState.apply_snapshot(snapshot) assert_str(GameState.current_zone_id).override_failure_message( "apply_snapshot() must read zone_id='zone_server_direct' from snapshot (D-020)" ).is_equal("zone_server_direct") func test_current_zone_id_server_value_overrides_tile_data() -> void: # When snapshot has both zone_id and visible_tiles with a different zone, # the top-level zone_id field takes priority. var snapshot := { "tick": 1, "entities": [ {"entity_id": 1, "x": 5.0, "y": 5.0, "z": 0, "kind": {"variant": "Player", "data": null}}, ], "zone_id": "zone_from_server", "visible_tiles": [ {"x": 5, "y": 5, "z": 0, "type": "floor", "zone_id": "zone_from_tile"}, ], } GameState.apply_snapshot(snapshot) assert_str(GameState.current_zone_id).override_failure_message( "Top-level zone_id must override tile-derived zone when both present" ).is_equal("zone_from_server") # -- zone_id: DEPRECATED fallback (graceful degradation) ---------------------- func test_current_zone_id_fallback_to_tile_lookup_when_absent() -> void: # When snapshot lacks top-level zone_id, tile lookup fallback must run. # Existing test_snapshot_zone_id.gd covers detailed scenarios; this is a # smoke test confirming the fallback path still works after Sprint 20 refactor. var snapshot := { "tick": 1, "entities": [ {"entity_id": 1, "x": 3.0, "y": 3.0, "z": 0, "kind": {"variant": "Player", "data": null}}, ], "visible_tiles": [ {"x": 3, "y": 3, "z": 0, "type": "floor", "zone_id": "zone_tile_fallback"}, ], } GameState.apply_snapshot(snapshot) assert_str(GameState.current_zone_id).override_failure_message( "Tile lookup fallback must populate zone_id when top-level field absent" ).is_equal("zone_tile_fallback") func test_apply_snapshot_missing_zone_id_no_crash() -> void: # Snapshots lacking both zone_id and visible_tiles must not crash. var snapshot := {"tick": 1, "entities": []} GameState.apply_snapshot(snapshot) assert_str(GameState.current_zone_id).is_equal("")