test(client): add tests for ring buffer and insert-off behavior (#507, #522)

33 tests: 17 for ring buffer (capacity, overwrite, JSONL format, seed),
16 for insert-off behavior (diegetic test, cursor transitions, cross-system).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-19 12:05:47 +01:00
co-authored by Claude Opus 4.6
parent c96f1463dc
commit 2d9bc33760
2 changed files with 739 additions and 0 deletions
+346
View File
@@ -0,0 +1,346 @@
## #507: WRONG button ring buffer tests.
## Tests the 60-tick rolling history upgrade to bug_report_dialog.gd.
## Covers ring buffer capacity, circular overwrite, JSONL format compliance,
## replay compatibility, seed file, and regression against MVP behavior.
##
## MVP baseline (must still pass): snapshot.json, render.txt, description.txt.
## New outputs: inputs.jsonl, snapshots.jsonl, seed.txt.
##
## Spec refs: D-030 (testability), D-020 (ObserverSnapshot boundary)
## Sprint 11 client.md §Notes #507
class_name TestBugReportRingBuffer
extends GdUnitTestSuite
var BugReportDialogScript = load("res://ui/bug_report_dialog.gd")
# Expected ring buffer capacity per spec.
const EXPECTED_CAPACITY := 60
# -- Helpers -------------------------------------------------------------------
func _make_dialog() -> Control:
var dialog = Control.new()
dialog.set_script(BugReportDialogScript)
auto_free(dialog)
add_child(dialog)
return dialog
## Create a minimal valid PlayerInput dict matching the replay.rs wire format.
## replay.rs expects: {"tick": N, "action": "MoveNorth"} or action as dict.
func _make_input(tick: int, action: String = "MoveNorth") -> Dictionary:
return {"tick": tick, "action": action}
## Make a minimal snapshot JSON string for snapshot buffer testing.
func _make_snapshot_json(tick: int) -> String:
return JSON.stringify({
"tick": tick,
"version": Protocol.PROTOCOL_VERSION,
"entities": [],
})
## Check a string is valid JSON array — mirrors replay.rs line expectation.
func _is_valid_json_array(line: String) -> bool:
if line.is_empty():
return false
var result = JSON.parse_string(line)
return result != null and result is Array
# -- Ring buffer capacity ------------------------------------------------------
func test_buffer_capacity_is_60() -> void:
# Spec: "Maintain a 60-entry circular buffer"
var dialog = _make_dialog()
if not dialog.has_method("_get_buffer_capacity"):
push_warning("TestBugReportRingBuffer: _get_buffer_capacity not found — awaiting #507")
return
assert_that(dialog._get_buffer_capacity()).override_failure_message(
"Ring buffer capacity must be 60 per spec"
).is_equal(EXPECTED_CAPACITY)
func test_snapshot_buffer_capacity_is_60() -> void:
# Parallel snapshot buffer must match input buffer capacity.
var dialog = _make_dialog()
if not dialog.has_method("_get_snapshot_buffer_capacity"):
push_warning("TestBugReportRingBuffer: _get_snapshot_buffer_capacity not found — awaiting #507")
return
assert_that(dialog._get_snapshot_buffer_capacity()).is_equal(EXPECTED_CAPACITY)
# -- Pre-allocation -----------------------------------------------------------
func test_input_buffer_preallocated_at_ready() -> void:
# Spec: "pre-allocate the 60-slot arrays at startup. Do not allocate on every tick."
var dialog = _make_dialog()
if not dialog.has_method("_get_input_buffer"):
push_warning("TestBugReportRingBuffer: _get_input_buffer not found — awaiting #507")
return
var buf = dialog._get_input_buffer()
assert_that(buf is Array).override_failure_message(
"Input buffer must be an Array"
).is_true()
assert_that(buf.size()).override_failure_message(
"Input buffer must be pre-allocated at capacity (%d slots)" % EXPECTED_CAPACITY
).is_equal(EXPECTED_CAPACITY)
# -- Push and fill behavior ---------------------------------------------------
func test_push_fills_input_buffer() -> void:
var dialog = _make_dialog()
if not dialog.has_method("_push_tick_inputs"):
push_warning("TestBugReportRingBuffer: _push_tick_inputs not found — awaiting #507")
return
if not dialog.has_method("_get_filled_input_count"):
push_warning("TestBugReportRingBuffer: _get_filled_input_count not found — awaiting #507")
return
for i in range(10):
dialog._push_tick_inputs(i, [_make_input(i)])
assert_that(dialog._get_filled_input_count()).override_failure_message(
"10 pushed ticks should produce filled count of 10"
).is_equal(10)
func test_push_fills_snapshot_buffer() -> void:
var dialog = _make_dialog()
if not dialog.has_method("_push_tick_snapshot"):
push_warning("TestBugReportRingBuffer: _push_tick_snapshot not found — awaiting #507")
return
if not dialog.has_method("_get_filled_snapshot_count"):
push_warning("TestBugReportRingBuffer: _get_filled_snapshot_count not found — awaiting #507")
return
for i in range(10):
dialog._push_tick_snapshot(_make_snapshot_json(i))
assert_that(dialog._get_filled_snapshot_count()).is_equal(10)
# -- Circular overwrite -------------------------------------------------------
func test_circular_overwrite_evicts_oldest() -> void:
# After 61 pushes, filled count must be 60 (oldest evicted).
var dialog = _make_dialog()
if not dialog.has_method("_push_tick_inputs") or not dialog.has_method("_get_filled_input_count"):
push_warning("TestBugReportRingBuffer: ring buffer API not found — awaiting #507")
return
for i in range(EXPECTED_CAPACITY + 1):
dialog._push_tick_inputs(i, [_make_input(i)])
assert_that(dialog._get_filled_input_count()).override_failure_message(
"After 61 pushes, buffer must hold exactly 60 entries (oldest evicted)"
).is_equal(EXPECTED_CAPACITY)
func test_circular_overwrite_keeps_newest_inputs() -> void:
# After 61 pushes, the flushed JSONL must contain tick 1..60 (not tick 0).
var dialog = _make_dialog()
if not dialog.has_method("_push_tick_inputs") or not dialog.has_method("_format_inputs_jsonl"):
push_warning("TestBugReportRingBuffer: ring buffer flush API not found — awaiting #507")
return
# Push 61 ticks. Tick 0 should be evicted; ticks 1-60 should be present.
for i in range(EXPECTED_CAPACITY + 1):
dialog._push_tick_inputs(i, [_make_input(i, "MoveNorth")])
var jsonl: String = dialog._format_inputs_jsonl()
# Tick 0 action would be at position 0, but after 61 pushes, position 0 was overwritten.
# We check that the output has exactly 60 lines.
var lines: PackedStringArray = jsonl.split("\n", false)
# Filter out blank lines (PackedStringArray has no filter — convert to Array first)
var non_blank: Array = Array(lines).filter(func(l): return not l.strip_edges().is_empty())
assert_that(non_blank.size()).override_failure_message(
"After 61 pushes, JSONL output must have exactly 60 non-blank lines"
).is_equal(EXPECTED_CAPACITY)
func test_exact_60_pushes_no_eviction() -> void:
# After exactly 60 pushes, all 60 are present.
var dialog = _make_dialog()
if not dialog.has_method("_push_tick_inputs") or not dialog.has_method("_get_filled_input_count"):
push_warning("TestBugReportRingBuffer: ring buffer API not found — awaiting #507")
return
for i in range(EXPECTED_CAPACITY):
dialog._push_tick_inputs(i, [_make_input(i)])
assert_that(dialog._get_filled_input_count()).override_failure_message(
"After exactly 60 pushes, all 60 entries should be present"
).is_equal(EXPECTED_CAPACITY)
# -- JSONL format compliance (replay.rs contract) ----------------------------
func test_inputs_jsonl_each_line_is_json_array() -> void:
# replay.rs: "Each line is a JSON array of PlayerInput for one tick."
var dialog = _make_dialog()
if not dialog.has_method("_push_tick_inputs") or not dialog.has_method("_format_inputs_jsonl"):
push_warning("TestBugReportRingBuffer: JSONL API not found — awaiting #507")
return
for i in range(3):
dialog._push_tick_inputs(i, [_make_input(i)])
var jsonl: String = dialog._format_inputs_jsonl()
var lines: PackedStringArray = jsonl.split("\n", false)
var non_blank: Array = Array(lines).filter(func(l): return not l.strip_edges().is_empty())
for line in non_blank:
assert_that(_is_valid_json_array(line)).override_failure_message(
"Each JSONL line must be a valid JSON array, got: %s" % line
).is_true()
func test_inputs_jsonl_idle_tick_is_empty_array() -> void:
# replay.rs: "Empty array = idle tick (no input sent)."
var dialog = _make_dialog()
if not dialog.has_method("_push_tick_inputs") or not dialog.has_method("_format_inputs_jsonl"):
push_warning("TestBugReportRingBuffer: JSONL API not found — awaiting #507")
return
dialog._push_tick_inputs(0, []) # Idle tick: no inputs
var jsonl: String = dialog._format_inputs_jsonl()
var lines: PackedStringArray = jsonl.split("\n", false)
var non_blank: Array = Array(lines).filter(func(l): return not l.strip_edges().is_empty())
assert_that(non_blank.size()).override_failure_message(
"One idle tick should produce exactly 1 JSONL line"
).is_equal(1)
assert_that(non_blank[0].strip_edges()).override_failure_message(
"Idle tick line must be an empty JSON array '[]'"
).is_equal("[]")
func test_inputs_jsonl_has_tick_field() -> void:
# replay.rs PlayerInput expects {"tick": N, "action": "..."}
var dialog = _make_dialog()
if not dialog.has_method("_push_tick_inputs") or not dialog.has_method("_format_inputs_jsonl"):
push_warning("TestBugReportRingBuffer: JSONL API not found — awaiting #507")
return
dialog._push_tick_inputs(42, [_make_input(42, "MoveEast")])
var jsonl: String = dialog._format_inputs_jsonl()
# Parse the first line and verify structure
var lines: PackedStringArray = jsonl.split("\n", false)
var non_blank: Array = Array(lines).filter(func(l): return not l.strip_edges().is_empty())
assert_that(non_blank.size()).is_greater(0)
var arr = JSON.parse_string(non_blank[0])
assert_that(arr is Array).is_true()
assert_that(arr.size() > 0).is_true()
var first_input: Dictionary = arr[0]
assert_that(first_input.has("tick")).override_failure_message(
"Each PlayerInput in JSONL must have 'tick' field"
).is_true()
assert_that(first_input.has("action")).override_failure_message(
"Each PlayerInput in JSONL must have 'action' field"
).is_true()
func test_inputs_jsonl_multiple_actions_per_tick() -> void:
# replay.rs supports multiple PlayerInput per tick (one array entry per action).
var dialog = _make_dialog()
if not dialog.has_method("_push_tick_inputs") or not dialog.has_method("_format_inputs_jsonl"):
push_warning("TestBugReportRingBuffer: JSONL API not found — awaiting #507")
return
dialog._push_tick_inputs(0, [
_make_input(0, "MoveNorth"),
_make_input(0, "Pause"),
])
var jsonl: String = dialog._format_inputs_jsonl()
var lines: PackedStringArray = jsonl.split("\n", false)
var non_blank: Array = Array(lines).filter(func(l): return not l.strip_edges().is_empty())
assert_that(non_blank.size()).is_equal(1)
var arr = JSON.parse_string(non_blank[0])
assert_that(arr is Array).is_true()
assert_that(arr.size()).override_failure_message(
"Tick with 2 actions must have 2 entries in the JSONL array"
).is_equal(2)
func test_snapshots_jsonl_each_line_is_valid_json() -> void:
# Each snapshot line should be valid JSON (parsed snapshot).
var dialog = _make_dialog()
if not dialog.has_method("_push_tick_snapshot") or not dialog.has_method("_format_snapshots_jsonl"):
push_warning("TestBugReportRingBuffer: snapshots JSONL API not found — awaiting #507")
return
for i in range(3):
dialog._push_tick_snapshot(_make_snapshot_json(i))
var jsonl: String = dialog._format_snapshots_jsonl()
var lines: PackedStringArray = jsonl.split("\n", false)
var non_blank: Array = Array(lines).filter(func(l): return not l.strip_edges().is_empty())
assert_that(non_blank.size()).is_equal(3)
for line in non_blank:
var parsed = JSON.parse_string(line)
assert_that(parsed != null).override_failure_message(
"Each snapshot line must be valid JSON, got: %s" % line
).is_true()
assert_that(parsed is Dictionary).override_failure_message(
"Each snapshot line must parse to a Dictionary"
).is_true()
# -- Seed file ----------------------------------------------------------------
func test_seed_written_on_capture() -> void:
# Spec: "Include the server's current RNG seed... Output as seed.txt"
# We can't easily verify file output in headless mode, but we verify
# the dialog has the method to produce the seed value.
var dialog = _make_dialog()
if not dialog.has_method("_get_current_seed"):
push_warning("TestBugReportRingBuffer: _get_current_seed not found — awaiting #507")
return
# Should return some Variant (int or String) without crashing
var seed_val = dialog._get_current_seed()
assert_that(seed_val != null).override_failure_message(
"_get_current_seed() must not return null (use 0 or 'unknown' if seed unavailable)"
).is_true()
# -- Room metadata regression -------------------------------------------------
func test_description_txt_still_contains_room_id() -> void:
# Regression: MVP description.txt must still include room metadata.
GameState.current_tick = 5
GameState.room_id = "warehouse_01"
GameState.player_stance = "Walk"
GameState.player_facing = "North"
GameState.player_position = Vector2(10.0, 10.0)
GameState.visible_entities = []
GameState.visible_tiles = []
GameState.game_time = {}
GameState.current_monologue = null
GameState.current_dialogue = null
var dialog = _make_dialog()
# Call the render text method to verify room shows up in render output
# (description.txt itself writes via FileAccess, difficult in headless — test the render path)
if dialog.has_method("_render_snapshot_text"):
# The text render is for render.txt, but description.txt room comes from GameState.room_id
# We indirectly verify by checking that the game state has the right fields.
assert_that(GameState.room_id).is_equal("warehouse_01")
GameState.room_id = null
# -- MVP regression -----------------------------------------------------------
func test_render_snapshot_text_still_works() -> void:
# Regression: MVP _render_snapshot_text() must still work after upgrade.
GameState.current_tick = 10
GameState.player_position = Vector2(5.0, 5.0)
GameState.player_facing = "East"
GameState.player_stance = "Careful"
GameState.visible_entities = []
GameState.visible_tiles = []
GameState.game_time = {}
GameState.current_monologue = null
GameState.current_dialogue = null
var dialog = _make_dialog()
if not dialog.has_method("_render_snapshot_text"):
push_warning("TestBugReportRingBuffer: _render_snapshot_text missing — regression risk")
return
var text: String = dialog._render_snapshot_text()
assert_that(text.length() > 0).is_true()
assert_that(text.contains("t10")).is_true()
func test_dialog_is_active_api_unchanged() -> void:
# Regression: public API from MVP must be unchanged.
var dialog = _make_dialog()
assert_that(dialog.has_method("start_capture")).override_failure_message(
"start_capture() must still be present (MVP regression)"
).is_true()
assert_that(dialog.has_method("is_active")).override_failure_message(
"is_active() must still be present (MVP regression)"
).is_true()
+393
View File
@@ -0,0 +1,393 @@
## #522: OQ-07 resolution — no-insert interaction behavior.
## Tests that when insert_active == false, interaction labels disappear
## (diegetic test from D-056 and D-057), and that cursor and interaction
## list agree on insert state.
##
## D-056: "Interaction labels render on z-layer 6 (insert overlay).
## If the insert is off, labels disappear."
## D-057: "Labels render on z-layer 6. If insert is off, labels disappear."
##
## OQ-07 resolution (expected option a): cursor shape still changes
## (character physically orients), but verb labels are suppressed.
##
## Spec refs: D-056, D-057, D-048, D-049
## Ticket #522
class_name TestInsertOffBehavior
extends GdUnitTestSuite
# -- Helpers -------------------------------------------------------------------
func _make_cursor() -> Node:
for path in ["res://ui/cursor_state_machine.tscn", "res://scenes/cursor.tscn", "res://ui/cursor.tscn"]:
if ResourceLoader.exists(path):
var scene = load(path)
var cursor = scene.instantiate()
add_child(cursor)
return cursor
return null
func _make_cursor_or_skip() -> Node:
var cursor = _make_cursor()
if cursor == null:
push_warning("TestInsertOffBehavior: cursor scene not found — test skipped (awaiting #522)")
return cursor
func _make_interaction_list() -> Node:
for path in ["res://ui/interaction_list.tscn", "res://ui/entity_interaction_list.tscn",
"res://scenes/interaction_list.tscn"]:
if ResourceLoader.exists(path):
var scene = load(path)
var node = scene.instantiate()
add_child(node)
return node
return null
func _make_list_or_skip() -> Node:
var list = _make_interaction_list()
if list == null:
push_warning("TestInsertOffBehavior: interaction list scene not found — test skipped (awaiting #522)")
return list
func _set_test_interactions() -> void:
GameState.nearby_interactions = [{
"entity_id": 2,
"entity_type": "Npc",
"distance": 1,
"verbs": [
{"kind": "Talk", "label": "Talk", "priority": 1, "available": true},
],
}]
func _clear_test_interactions() -> void:
GameState.nearby_interactions = []
# -- Interaction list: diegetic test (D-056, D-057) ---------------------------
func test_insert_off_hides_interaction_list() -> void:
# D-056/D-057 diegetic test: "If the insert is off, labels disappear."
var list = _make_list_or_skip()
if list == null:
return
_set_test_interactions()
if list.has_method("set_insert_active"):
list.set_insert_active(false)
if list.has_method("update_from_state"):
list.update_from_state()
if list.has_method("is_showing"):
assert_that(list.is_showing()).override_failure_message(
"D-056/D-057 diegetic test: interaction list must be hidden when insert is off"
).is_false()
elif list.has_method("get_visible_verb_count"):
assert_that(list.get_visible_verb_count()).override_failure_message(
"Interaction list must show 0 verbs when insert is off"
).is_equal(0)
list.queue_free()
_clear_test_interactions()
func test_insert_on_shows_interaction_list() -> void:
# When insert is on (normal state), verbs should be visible.
var list = _make_list_or_skip()
if list == null:
return
_set_test_interactions()
if list.has_method("set_insert_active"):
list.set_insert_active(true)
if list.has_method("update_from_state"):
list.update_from_state()
if list.has_method("is_showing"):
assert_that(list.is_showing()).override_failure_message(
"Interaction list must be visible when insert is on and verbs present"
).is_true()
list.queue_free()
_clear_test_interactions()
func test_insert_reenable_restores_list() -> void:
# Toggling insert off then on restores verb list visibility.
var list = _make_list_or_skip()
if list == null:
return
_set_test_interactions()
if not list.has_method("set_insert_active"):
list.queue_free()
_clear_test_interactions()
return
list.set_insert_active(false)
if list.has_method("update_from_state"):
list.update_from_state()
list.set_insert_active(true)
if list.has_method("update_from_state"):
list.update_from_state()
if list.has_method("is_showing"):
assert_that(list.is_showing()).override_failure_message(
"Re-enabling insert must restore list visibility"
).is_true()
list.queue_free()
_clear_test_interactions()
# -- Cursor: insert-off behavior (OQ-07 resolution option a) ------------------
func test_insert_off_suppresses_should_show_interactions() -> void:
# OQ-07 option (a): labels are suppressed when insert is off.
# Regardless of cursor visual state, should_show_interactions() must return false.
var cursor = _make_cursor_or_skip()
if cursor == null:
return
if not cursor.has_method("set_insert_active"):
push_warning("TestInsertOffBehavior: cursor.set_insert_active not found — awaiting #522")
cursor.queue_free()
return
cursor.set_insert_active(false)
if cursor.has_method("should_show_interactions"):
assert_that(cursor.should_show_interactions()).override_failure_message(
"should_show_interactions() must return false when insert is off"
).is_false()
cursor.queue_free()
func test_insert_on_restores_should_show_interactions() -> void:
# After re-enabling insert, interactions should show again (not in weapon mode).
var cursor = _make_cursor_or_skip()
if cursor == null:
return
if not cursor.has_method("set_insert_active"):
push_warning("TestInsertOffBehavior: cursor.set_insert_active not found — awaiting #522")
cursor.queue_free()
return
cursor.set_insert_active(false)
cursor.set_insert_active(true)
if cursor.has_method("should_show_interactions"):
assert_that(cursor.should_show_interactions()).override_failure_message(
"should_show_interactions() must return true after re-enabling insert"
).is_true()
cursor.queue_free()
func test_insert_off_at_startup_no_corruption() -> void:
# Insert can be off from the start — no state machine corruption.
var cursor = _make_cursor_or_skip()
if cursor == null:
return
if not cursor.has_method("set_insert_active") or not cursor.has_method("get_state"):
cursor.queue_free()
return
cursor.set_insert_active(false)
# Cursor must be in a valid state (not null/error)
var state_str := str(cursor.get_state())
assert_that(["Default", "EntityHover", "ObjectHover", "WeaponAim"].has(state_str)).override_failure_message(
"Cursor must be in a valid state after insert-off at startup, got: %s" % state_str
).is_true()
cursor.queue_free()
# -- OQ-07 option (a): cursor shape still changes, labels suppressed ----------
func test_insert_off_option_a_cursor_still_transitions() -> void:
# Option (a): cursor shape changes even when insert off
# (character physically orients to target, just no labels).
# If implementation chose option (b) instead, this test would fail — that's informative.
var cursor = _make_cursor_or_skip()
if cursor == null:
return
if not cursor.has_method("set_insert_active") or not cursor.has_method("set_hover_target"):
push_warning("TestInsertOffBehavior: set_insert_active or set_hover_target not found — awaiting #522")
cursor.queue_free()
return
cursor.set_insert_active(false)
cursor.set_hover_target({"entity_id": 2, "kind": "Npc", "relationship": "Unknown", "in_los": true})
# Option (a): cursor SHOULD be in EntityHover despite insert being off
# Option (b): cursor would stay in Default
# Test documents expected behavior — fail message explains which option is active
if cursor.has_method("get_state"):
var state := str(cursor.get_state())
# If this assertion fails, option (b) was implemented (cursor full-suppressed).
# Neither (a) nor (b) is wrong — this test documents which was chosen.
# Consult decisions/perception.md for the OQ-07 amendment.
assert_that(state).override_failure_message(
"OQ-07 option (a): cursor should still transition to EntityHover when insert is off. "
+ "If this fails, option (b) was implemented (full suppression) — update this test to assert 'Default' instead."
).is_equal("EntityHover")
cursor.queue_free()
# -- Edge cases ---------------------------------------------------------------
func test_insert_off_plus_weapon_mode_stays_suppressed() -> void:
# Insert-off AND weapon mode: should_show_interactions() must still return false.
var cursor = _make_cursor_or_skip()
if cursor == null:
return
if not cursor.has_method("set_insert_active") or not cursor.has_method("set_weapon_mode"):
cursor.queue_free()
return
cursor.set_insert_active(false)
cursor.set_weapon_mode(true)
if cursor.has_method("should_show_interactions"):
assert_that(cursor.should_show_interactions()).override_failure_message(
"Insert-off + weapon mode must keep should_show_interactions() false"
).is_false()
cursor.queue_free()
func test_insert_off_shift_override_still_suppressed() -> void:
# OQ-07: Insert-off trumps Shift override.
# Shift restores interactions in weapon mode, but insert-off is a harder constraint.
var cursor = _make_cursor_or_skip()
if cursor == null:
return
if not cursor.has_method("set_insert_active") or not cursor.has_method("set_shift_held"):
cursor.queue_free()
return
cursor.set_insert_active(false)
if cursor.has_method("set_shift_held"):
cursor.set_shift_held(true)
if cursor.has_method("should_show_interactions"):
assert_that(cursor.should_show_interactions()).override_failure_message(
"Insert-off must suppress interactions even with Shift held (insert-off > shift override)"
).is_false()
cursor.queue_free()
func test_insert_off_rapid_toggle_no_corruption() -> void:
# Rapid on/off toggle must not leave either system in inconsistent state.
var cursor = _make_cursor_or_skip()
if cursor == null:
return
if not cursor.has_method("set_insert_active"):
cursor.queue_free()
return
for _i in range(10):
cursor.set_insert_active(false)
cursor.set_insert_active(true)
# After 10 toggles, insert should be on
if cursor.has_method("should_show_interactions"):
assert_that(cursor.should_show_interactions()).override_failure_message(
"After rapid toggle ending on 'on', interactions must be visible"
).is_true()
cursor.queue_free()
# -- Cross-system consistency (cursor + list agree) ---------------------------
func test_cursor_and_list_agree_on_insert_off() -> void:
# Both cursor and interaction list must be suppressed when insert is off.
# This tests the integration contract — both must read from the same source of truth.
var cursor = _make_cursor_or_skip()
var list = _make_list_or_skip()
if cursor == null or list == null:
if cursor != null: cursor.queue_free()
if list != null: list.queue_free()
return
_set_test_interactions()
if cursor.has_method("set_insert_active") and list.has_method("set_insert_active"):
cursor.set_insert_active(false)
list.set_insert_active(false)
if list.has_method("update_from_state"):
list.update_from_state()
var cursor_suppressed := true
if cursor.has_method("should_show_interactions"):
cursor_suppressed = not cursor.should_show_interactions()
var list_suppressed := true
if list.has_method("is_showing"):
list_suppressed = not list.is_showing()
elif list.has_method("get_visible_verb_count"):
list_suppressed = list.get_visible_verb_count() == 0
assert_that(cursor_suppressed).override_failure_message(
"Cursor: should_show_interactions() must return false when insert is off"
).is_true()
assert_that(list_suppressed).override_failure_message(
"Interaction list: must be hidden when insert is off"
).is_true()
cursor.queue_free()
list.queue_free()
_clear_test_interactions()
func test_game_state_insert_active_field_exists() -> void:
# GameState must have an insert_active field after #522 implementation.
assert_that(GameState.get("insert_active") != null or "insert_active" in GameState).override_failure_message(
"GameState must have insert_active field after #522 — awaiting implementation"
).is_true()
func test_game_state_insert_active_default_true() -> void:
# Default state: insert is on (player starts with a functioning neural insert).
if not ("insert_active" in GameState):
push_warning("TestInsertOffBehavior: GameState.insert_active not found — awaiting #522")
return
assert_that(GameState.insert_active).override_failure_message(
"GameState.insert_active should default to true (insert is normally on)"
).is_true()
# -- D-049: z-layer verification (labels on layer 6) -------------------------
func test_interaction_list_on_insert_layer() -> void:
# D-056/D-057: "Labels render on z-layer 6 (insert overlay)"
# This is already tested in test_interaction_list.gd but we verify here
# that the z-layer is CANVAS_INSERT (the insert overlay layer).
var list = _make_list_or_skip()
if list == null:
return
if list.has_method("get_z_layer"):
assert_that(list.get_z_layer()).override_failure_message(
"Interaction list labels must render on CANVAS_INSERT (z-layer 6 per D-049)"
).is_equal(Constants.CANVAS_INSERT)
list.queue_free()
# -- Regression: existing behavior unchanged ----------------------------------
func test_sprint_suppression_still_works_with_insert_on() -> void:
# D-055 regression: Sprint suppresses interaction list regardless of insert state.
var list = _make_list_or_skip()
if list == null:
return
_set_test_interactions()
# Ensure insert is on (should not affect sprint suppression)
if list.has_method("set_insert_active"):
list.set_insert_active(true)
GameState.player_stance = "Sprint"
if list.has_method("update_from_state"):
list.update_from_state()
if list.has_method("is_showing"):
assert_that(list.is_showing()).override_failure_message(
"Sprint must suppress interaction list even when insert is on (D-055 regression)"
).is_false()
elif list.has_method("get_visible_verb_count"):
assert_that(list.get_visible_verb_count()).is_equal(0)
list.queue_free()
_clear_test_interactions()
GameState.player_stance = "Walk"
func test_weapon_mode_suppression_still_works_with_insert_on() -> void:
# D-056 regression: weapon mode suppresses interactions when insert is on.
var cursor = _make_cursor_or_skip()
if cursor == null:
return
if cursor.has_method("set_insert_active"):
cursor.set_insert_active(true) # Insert is on
if cursor.has_method("set_weapon_mode"):
cursor.set_weapon_mode(true)
if cursor.has_method("should_show_interactions"):
assert_that(cursor.should_show_interactions()).override_failure_message(
"Weapon mode must suppress interactions when insert is on (D-056 regression)"
).is_false()
cursor.queue_free()