## #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()