- 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>
204 lines
6.6 KiB
GDScript
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()
|