Files
settled-reach/client/tests/test_insert_off_behavior.gd
T
jpmschweitzerandClaude Opus 4.6 44d9941f04 fix(client): address PR #40 review — 13 suggestions
- Rename _insert to insert_state in main.gd (Hoshe #1)
- Add after_each() to test_bug_report_ring_buffer.gd for GameState
  cleanup on assertion failure (Hoshe #2)
- Add after_each() to test_insert_off_behavior.gd for stance/interaction
  restore on assertion failure (Hoshe #3)
- Fix assertion message: "unknown" → "unavailable" (Hoshe #4)
- Document memory ceiling of 60 JSON snapshots in ring buffer (Hoshe #5)
- Add precision warning for u64 rng_seed via JSON float (Hoshe #6)
- Promote _action_enum_to_wire to public action_enum_to_wire (Tyre #1)
- Add @warning_ignore for unused _tick parameter (Tyre #5)
- Document insert_active assumption for future no-insert characters (Tyre #4)
- Restructure OQ-07 decision amendments as bullet points (Tyre #7)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 12:19:41 +01:00

401 lines
14 KiB
GDScript

## #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
func after_each() -> void:
# Reset GameState fields mutated by tests to prevent cross-test leakage.
GameState.nearby_interactions = []
GameState.player_stance = "Walk"
GameState.insert_active = true
# -- 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()