Files
settled-reach/client/tests/test_signal_sprint24.gd
T
jpmschweitzerandClaude Opus 4.6 3f5b4258ba feat(client): sprint 38 — free camera viewer, archetype strip, test fixes
- Add free camera mode (F4 toggle): WASD pan, scroll zoom, decoupled
  from player position (#898)
- Strip archetype-driven code: remove character_archetype, lattice_profile,
  and lattice color palettes from client (#882)
- Fix confrontation_monologue signal not firing in headless test mode (#867)
- Revive fog state behavioral tests: EXP_EXPLORED persistence, grow-only
  bounds, texture-resize copy, BoundaryWall handling (#879)
- Triage pre-existing test failures: fix examine_display dismiss timing,
  fog test position fragility, rendering snapshot assertions,
  time_display format (#871)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-02 10:19:11 +02:00

204 lines
6.6 KiB
GDScript

## Sprint 24 — Signal acceptance tests (#590, #592)
##
## Client-side acceptance criteria:
## - #590: triangle_crisis_events decoded by Protocol, chimed once per triangle_id
## - #592: news_ticker decode + update_from_state hide/show behavior
##
## Spec: D-016 (client displays server data only), D-042 (UI strings in yaml),
## D-067 (chime on recognition onset)
class_name TestSignalSprint24
extends GdUnitTestSuite
const NEWS_TICKER_SCENE = preload("res://ui/news_ticker.tscn")
# -- #590: triangle_crisis_events decode --------------------------------------
func test_protocol_decode_includes_triangle_crisis_events_field() -> void:
# decode_snapshot() must return a "triangle_crisis_events" key (#590).
var raw := {
"tick": 1,
"version": 23,
"entities": [],
"triangle_crisis_events": [{"triangle_id": 42}],
}
var encoded = Messagepack.encode(raw)
assert_that(encoded.status).is_null()
var snapshot = Protocol.decode_snapshot(encoded.value)
assert_that(snapshot).is_not_null()
assert_bool(snapshot.has("triangle_crisis_events")).override_failure_message(
"decode_snapshot must include triangle_crisis_events in returned dict"
).is_true()
var events: Array = snapshot["triangle_crisis_events"]
assert_bool(events.size() == 1).override_failure_message(
"Expected 1 triangle_crisis_event, got: %d" % events.size()
).is_true()
assert_int(events[0]["triangle_id"]).is_equal(42)
func test_protocol_decode_triangle_crisis_events_empty_array() -> void:
# When no events are present, field is present and empty.
var raw := {
"tick": 1,
"version": 23,
"entities": [],
"triangle_crisis_events": [],
}
var encoded = Messagepack.encode(raw)
var snapshot = Protocol.decode_snapshot(encoded.value)
assert_that(snapshot).is_not_null()
var events: Array = snapshot.get("triangle_crisis_events", [])
assert_int(events.size()).is_equal(0)
func test_protocol_decode_triangle_crisis_events_absent_returns_empty() -> void:
# When server doesn't send field (pre-#589), field defaults to empty array.
var raw := {
"tick": 1,
"version": 23,
"entities": [],
}
var encoded = Messagepack.encode(raw)
var snapshot = Protocol.decode_snapshot(encoded.value)
assert_that(snapshot).is_not_null()
var events: Array = snapshot.get("triangle_crisis_events", [])
assert_int(events.size()).is_equal(0)
func test_triangle_dedup_fires_chime_only_once_per_id() -> void:
# _known_triangle_ids must prevent the same triangle_id from chiming twice.
# We test the dedup dict directly — main.gd cannot be easily instantiated headless.
# The dict is the single source of truth for dedup state.
var seen: Dictionary = {}
var chime_count: int = 0
# Simulate two ticks both containing triangle_id 42.
for _tick in range(2):
var tid: int = 42
if not seen.has(tid):
seen[tid] = true
chime_count += 1
assert_int(chime_count).override_failure_message(
"Chime must fire exactly once per triangle_id across repeated ticks"
).is_equal(1)
func test_triangle_dedup_fires_chime_for_each_unique_id() -> void:
# Two distinct triangle_ids each chime once.
var seen: Dictionary = {}
var chime_count: int = 0
for tid in [42, 99]:
if not seen.has(tid):
seen[tid] = true
chime_count += 1
assert_int(chime_count).override_failure_message(
"Each unique triangle_id must chime independently"
).is_equal(2)
# -- #592: current_ticker decode ----------------------------------------------
func test_protocol_decode_includes_current_ticker_field() -> void:
# decode_snapshot() must return a "current_ticker" key (#592).
var raw := {
"tick": 1,
"version": 23,
"entities": [],
"current_ticker": {"id": "ticker_001", "text": "Station systems nominal.", "category": "System"},
}
var encoded = Messagepack.encode(raw)
assert_that(encoded.status).is_null()
var snapshot = Protocol.decode_snapshot(encoded.value)
assert_that(snapshot).is_not_null()
assert_bool(snapshot.has("current_ticker")).override_failure_message(
"decode_snapshot must include current_ticker in returned dict"
).is_true()
var ticker: Variant = snapshot["current_ticker"]
assert_that(ticker).is_not_null()
assert_str(ticker["text"]).is_equal("Station systems nominal.")
func test_protocol_decode_current_ticker_null_when_absent() -> void:
# When server doesn't send current_ticker (player outside bar zone), field is null.
var raw := {
"tick": 1,
"version": 23,
"entities": [],
}
var encoded = Messagepack.encode(raw)
var snapshot = Protocol.decode_snapshot(encoded.value)
assert_that(snapshot).is_not_null()
var ticker: Variant = snapshot.get("current_ticker")
assert_that(ticker).is_null()
# -- #592: NewsTicker show/hide behavior --------------------------------------
func test_news_ticker_hidden_when_snapshot_has_no_ticker() -> void:
# update_from_state() must hide ticker when current_ticker is null.
var ticker_scene := NEWS_TICKER_SCENE
assert_that(ticker_scene).is_not_null()
var ticker := ticker_scene.instantiate()
auto_free(ticker)
add_child(ticker)
# Snapshot with no current_ticker (player outside bar zone).
GameState.current_snapshot = {
"tick": 1,
"version": 23,
"entities": [],
}
ticker.update_from_state()
assert_bool(ticker.visible).override_failure_message(
"NewsTicker must be hidden when current_ticker is absent"
).is_false()
func test_news_ticker_visible_when_snapshot_has_ticker() -> void:
# update_from_state() must show ticker when current_ticker has text.
var ticker_scene := NEWS_TICKER_SCENE
assert_that(ticker_scene).is_not_null()
var ticker := ticker_scene.instantiate()
auto_free(ticker)
add_child(ticker)
GameState.current_snapshot = {
"tick": 2,
"version": 23,
"entities": [],
"current_ticker": {"id": "t1", "text": "Station systems nominal.", "category": "System"},
}
ticker.update_from_state()
assert_bool(ticker.visible).override_failure_message(
"NewsTicker must be visible when current_ticker has text"
).is_true()
func test_news_ticker_hides_when_ticker_becomes_null() -> void:
# Ticker shown then hidden: update_from_state() with null current_ticker hides it.
var ticker_scene := NEWS_TICKER_SCENE
assert_that(ticker_scene).is_not_null()
var ticker := ticker_scene.instantiate()
auto_free(ticker)
add_child(ticker)
# Show it first.
GameState.current_snapshot = {
"tick": 1, "version": 23, "entities": [],
"current_ticker": {"id": "t1", "text": "Breaking news.", "category": "System"},
}
ticker.update_from_state()
assert_bool(ticker.visible).is_true()
# Null current_ticker — player left the bar zone.
GameState.current_snapshot = {
"tick": 2, "version": 23, "entities": [],
}
ticker.update_from_state()
assert_bool(ticker.visible).override_failure_message(
"NewsTicker must hide when current_ticker returns to null"
).is_false()