From 286513b1f9f193262693a122931ee5cbc0e08614 Mon Sep 17 00:00:00 2001 From: Jackson Moore Date: Thu, 3 Sep 2026 22:09:43 +0200 Subject: Implement state transitions --- tests/run.lua | 367 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 366 insertions(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/run.lua b/tests/run.lua index f984123..2eb49b3 100644 --- a/tests/run.lua +++ b/tests/run.lua @@ -9,6 +9,8 @@ package.path = table.concat({ local domain = require("clever_f.domain") local capabilities = require("clever_f.capabilities") local policy = require("clever_f.policy") +local sequence_state = require("clever_f.sequence_state") +local state_transitions = require("clever_f.state_transitions") local MemoryHost = require("clever_f.testing.memory_host") local tests = {} @@ -832,6 +834,369 @@ test("Unsupported Migemo policy mutation updates live configuration", function() truthy(service:get_boolean("use_migemo")) end) +local function fresh_sequence_state() + local transitions = state_transitions.new() + transitions:ClearTemporaryOverlays() + transitions:DiagnosticFullReset() + return sequence_state.get(), transitions +end + +local function map_size(values) + local count = 0 + for _ in pairs(values) do + count = count + 1 + end + return count +end + +test("SequenceState has one empty plugin-global instance", function() + local state, transitions = fresh_sequence_state() + same(state, sequence_state.new()) + same(state, sequence_state.global) + same(state, transitions:state()) + same(state, state_transitions.new():state()) + + same(0, map_size(state.previous_descriptor)) + same(0, map_size(state.previous_landing)) + same(0, map_size(state.first_move)) + same(0, map_size(state.previous_target)) + same(0, map_size(state.migemo_cache)) + same(nil, state.last_input_context) + falsy(state.moved_forward) + falsy(state.moved_forward_initialized) + same(0, state.repeat_timestamp_ms) + same(1, state.repeat_timestamp_ms + 1) + same(nil, state.highlight_timer) + same(0, #state.target_overlays) + same(0, #state.temporary_overlays) + same(0, #state.finalizers) +end) + +test("Per-context transitions use normalized ModeContext keys", function() + local state, transitions = fresh_sequence_state() + local operator = domain.ModeContext.from_full_mode("no") + local target = domain.TargetValue.character("x", 120) + + transitions:BeginAcquisition("nov", "F") + transitions:CommitAcquiredTarget("no" .. string.char(0x16), target, 12.5) + + same(domain.Descriptor.FIND_BACKWARD, state:get_previous_descriptor("noV")) + truthy(state:get_first_move(operator)) + same(target, state:get_previous_target("no")) + same(operator, state.last_input_context) + same(12.5, state.repeat_timestamp_ms) + same(1, map_size(state.previous_descriptor)) + same(domain.Descriptor.FIND_BACKWARD, state.previous_descriptor[operator]) + + transitions:CommitCommandSuccess("noV", { line = 3, byte_column = 6 }, false) + local serialized = state:to_table() + same(3, serialized.contexts.no.previous_landing.line) + same(6, serialized.contexts.no.previous_landing.byte_column) + same(2, map_size(serialized.contexts.no.previous_landing)) + same(nil, serialized.contexts.nov) + + local normal_target = domain.TargetValue.character("a", 97) + local visual_target = domain.TargetValue.character("b", 98) + transitions:BeginAcquisition("n", "f") + transitions:CommitAcquiredTarget("n", normal_target) + transitions:BeginAcquisition("v", "t") + transitions:CommitAcquiredTarget("v", visual_target) + + same(normal_target, state:get_previous_target("n")) + same(visual_target, state:get_previous_target("v")) + same(domain.Descriptor.FIND_FORWARD, state:get_previous_descriptor("n")) + same(domain.Descriptor.TILL_FORWARD, state:get_previous_descriptor("v")) + same(domain.ModeContext.from_full_mode("v"), state.last_input_context) +end) + +test("Success transitions commit only their mode-sensitive fields", function() + local state, transitions = fresh_sequence_state() + local target = domain.TargetValue.character("h", 104) + local command_destination = domain.Position.new(2, 4) + + transitions:BeginAcquisition("n", "f") + transitions:CommitAcquiredTarget("n", target, 31) + transitions:CommitCommandSuccess( + "n", + command_destination, + domain.Direction.FORWARD + ) + + same(command_destination, state:get_previous_landing("n")) + falsy(state:get_first_move("n")) + truthy(state.moved_forward) + truthy(state.moved_forward_initialized) + + local visual_destination = domain.Position.new(3, 2) + transitions:BeginAcquisition("v", "T") + transitions:CommitAcquiredTarget("v", target) + transitions:CommitVisualSuccess("v", visual_destination) + + same(visual_destination, state:get_previous_landing("v")) + falsy(state:get_first_move("v")) + truthy(state.moved_forward, "Visual success must retain movement direction") + same(31, state.repeat_timestamp_ms, "an absent acquisition time must be retained") + + transitions:CommitCommandSuccess("n", domain.Position.new(1, 1), "backward") + falsy(state.moved_forward) + truthy(state.moved_forward_initialized) +end) + +test("Failed and partial outcomes preserve successful history silently", function() + local state, transitions = fresh_sequence_state() + local target = domain.TargetValue.character("a", 97) + local landing = domain.Position.new(1, 5) + + transitions:BeginAcquisition("n", "f") + transitions:CommitAcquiredTarget("n", target, 17) + transitions:CommitCommandSuccess("n", landing, true) + + local descriptor = state:get_previous_descriptor("n") + local first_move = state:get_first_move("n") + local moved_forward = state.moved_forward + local before_timestamp = state.repeat_timestamp_ms + + local failed = domain.ActionOutcome.from_search( + domain.SearchOutcome.boundary_before_any(landing), + "f" + ) + local partial_endpoint = domain.Position.new(1, 7) + local partial = domain.ActionOutcome.from_search( + domain.SearchOutcome.boundary_after_partial(partial_endpoint, 2), + "f" + ) + + same(domain.ActionKind.FAILED_SEARCH, failed.kind) + same(nil, failed.diagnostic) + same(0, failed.successful_steps) + same(domain.ActionKind.FAILED_SEARCH, partial.kind) + same(partial_endpoint, partial.position) + same(2, partial.successful_steps) + same(nil, partial.diagnostic) + + same(descriptor, state:get_previous_descriptor("n")) + same(landing, state:get_previous_landing("n")) + same(first_move, state:get_first_move("n")) + same(target, state:get_previous_target("n")) + same(moved_forward, state.moved_forward) + same(before_timestamp, state.repeat_timestamp_ms) +end) + +test("State resource updates retain host locations and timer identity", function() + local state, transitions = fresh_sequence_state() + + transitions:AddTargetOverlay("char-1", "window-1", 4) + transitions:AddTargetOverlay("char-1", "window-2", 8) + transitions:AddTemporaryOverlay("cursor-1", "window-1", "CleverFCursor") + transitions:AddTemporaryOverlay("direct-1", "window-2", "CleverFDirect") + transitions:AddFinalizer("finalizer-1", "buffer-1") + transitions:AddFinalizer("finalizer-1", "buffer-2") + + local resources = state:resources() + same("window-1", resources.target_overlays[1].window) + same(4, resources.target_overlays[1].anchor_line) + same("window-2", resources.target_overlays[2].window) + same("CleverFDirect", resources.temporary_overlays[2].group) + same("buffer-1", resources.finalizers[1].buffer) + same("buffer-2", resources.finalizers[2].buffer) + + resources.target_overlays[1].window = "changed" + same("window-1", state.target_overlays[1].window) + fails(function() + transitions:AddTargetOverlay("char-1", "window-1") + end, "already active") + fails(function() + transitions:AddFinalizer("missing-location") + end, "host location") + + same(nil, transitions:SetHighlightTimer("timer-1")) + same("timer-1", transitions:SetHighlightTimer("timer-2")) + local cleared, current = transitions:ClearHighlightTimer("timer-1") + same(nil, cleared) + falsy(current) + same("timer-2", state.highlight_timer) + cleared, current = transitions:ClearHighlightTimer("timer-2") + same("timer-2", cleared) + truthy(current) + same(nil, state.highlight_timer) + + local removed = transitions:RemoveTargetOverlay("char-1", "window-1") + same(1, #removed) + same("window-1", removed[1].window) + same(1, #state.target_overlays) + same("window-2", state.target_overlays[1].window) +end) + +test("SequenceState exposes copies and rejects direct mutation", function() + local state, transitions = fresh_sequence_state() + transitions:BeginAcquisition("n", "t") + transitions:AddTargetOverlay("char-copy", "window-copy", 2) + + fails(function() + state.moved_forward = true + end, "StateTransitions") + + local descriptor_map = state.previous_descriptor + descriptor_map[domain.ModeContext.from_full_mode("n")] = nil + same(domain.Descriptor.TILL_FORWARD, state:get_previous_descriptor("n")) + + local overlays = state.target_overlays + overlays[1].anchor_line = 99 + overlays[1] = nil + same(2, state.target_overlays[1].anchor_line) +end) + +test("PublicReset applies its exact clear and retain sets", function() + local state, transitions = fresh_sequence_state() + local normal_target = domain.TargetValue.character("h", 104) + local visual_target = domain.TargetValue.character("x", 120) + + transitions:BeginAcquisition("n", "f") + transitions:CommitAcquiredTarget("n", normal_target, 77) + transitions:CommitCommandSuccess("n", domain.Position.new(1, 2), true) + transitions:BeginAcquisition("v", "t") + transitions:CommitAcquiredTarget("v", visual_target) + transitions:CommitVisualSuccess("v", domain.Position.new(2, 3)) + transitions:CacheMigemo("utf-8", { dictionary = "utf-8" }) + transitions:SetHighlightTimer("timer-1") + transitions:AddTargetOverlay("char-1", "window-1", 1) + transitions:AddTargetOverlay("char-2", "window-2", 2) + transitions:AddTemporaryOverlay("cursor-1", "window-1", "CleverFCursor") + transitions:AddFinalizer("finalizer-1", "buffer-1") + + local input_context = state.last_input_context + local cleanup = transitions:PublicReset("window-1") + + same(nil, state:get_previous_descriptor("n")) + same(nil, state:get_previous_descriptor("v")) + same(nil, state:get_previous_landing("n")) + same(nil, state:get_previous_landing("v")) + same(nil, state:get_first_move("n")) + same(nil, state:get_first_move("v")) + same(normal_target, state:get_previous_target("n")) + same(visual_target, state:get_previous_target("v")) + same(input_context, state.last_input_context) + truthy(state.moved_forward) + truthy(state.moved_forward_initialized) + same(0, map_size(state.migemo_cache)) + same(0, state.repeat_timestamp_ms) + same(nil, state.highlight_timer) + same(1, #state.target_overlays) + same("window-2", state.target_overlays[1].window) + same(1, #state.temporary_overlays) + same(1, #state.finalizers) + + same("timer-1", cleanup.highlight_timer) + same(1, #cleanup.target_overlays) + same("char-1", cleanup.target_overlays[1].identity) + same("window-1", cleanup.target_overlays[1].window) + same(0, #cleanup.finalizers) +end) + +test("FullFinalization clears feedback and direction while retaining history", function() + local state, transitions = fresh_sequence_state() + local normal_target = domain.TargetValue.character("h", 104) + local visual_target = domain.TargetValue.character("x", 120) + local dictionary = { dictionary = "utf-8" } + + transitions:BeginAcquisition("n", "f") + transitions:CommitAcquiredTarget("n", normal_target, 91) + transitions:CommitCommandSuccess("n", domain.Position.new(1, 2), true) + transitions:BeginAcquisition("v", "T") + transitions:CommitAcquiredTarget("v", visual_target) + transitions:CommitVisualSuccess("v", domain.Position.new(2, 3)) + transitions:CacheMigemo("utf-8", dictionary) + transitions:SetHighlightTimer("timer-finalize") + transitions:AddTargetOverlay("char-current", "window-1", 1) + transitions:AddTargetOverlay("char-peer", "window-2", 2) + transitions:AddTemporaryOverlay("direct-current", "window-1", "CleverFDirect") + transitions:AddFinalizer("finalizer-a", "buffer-1") + transitions:AddFinalizer("finalizer-b", "buffer-2") + + local input_context = state.last_input_context + local cleanup = transitions:FullFinalization("window-1") + + same(nil, state:get_previous_landing("n")) + same(nil, state:get_previous_landing("v")) + falsy(state.moved_forward) + truthy(state.moved_forward_initialized) + same(domain.Descriptor.FIND_FORWARD, state:get_previous_descriptor("n")) + same(domain.Descriptor.TILL_BACKWARD, state:get_previous_descriptor("v")) + same(normal_target, state:get_previous_target("n")) + same(visual_target, state:get_previous_target("v")) + falsy(state:get_first_move("n")) + falsy(state:get_first_move("v")) + same(input_context, state.last_input_context) + same(dictionary, state:get_migemo("utf-8")) + same(91, state.repeat_timestamp_ms) + same(nil, state.highlight_timer) + same(1, #state.target_overlays) + same("window-2", state.target_overlays[1].window) + same(1, #state.temporary_overlays) + same(0, #state.finalizers) + + same("timer-finalize", cleanup.highlight_timer) + same("window-1", cleanup.target_overlays[1].window) + same(2, #cleanup.finalizers) + same("buffer-1", cleanup.finalizers[1].buffer) +end) + +test("DiagnosticFullReset adds only its diagnostic clear set", function() + local state, transitions = fresh_sequence_state() + local target = domain.TargetValue.character("z", 122) + + transitions:BeginAcquisition("n", "f") + transitions:CommitAcquiredTarget("n", target, 55) + transitions:CommitCommandSuccess("n", domain.Position.new(4, 7), true) + transitions:CacheMigemo("cp932", { dictionary = "cp932" }) + transitions:SetHighlightTimer("timer-diagnostic") + transitions:AddTargetOverlay("char-diagnostic", "window-3", 4) + transitions:AddTemporaryOverlay("cursor-diagnostic", "window-3", "CleverFCursor") + transitions:AddFinalizer("finalizer-diagnostic", "buffer-3") + + local cleanup = transitions:DiagnosticFullReset() + + same(nil, state:get_previous_descriptor("n")) + same(nil, state:get_previous_landing("n")) + same(nil, state:get_first_move("n")) + same(nil, state:get_previous_target("n")) + same(nil, state.last_input_context) + falsy(state.moved_forward) + falsy(state.moved_forward_initialized) + same(0, map_size(state.migemo_cache)) + same(0, state.repeat_timestamp_ms) + same(nil, state.highlight_timer) + same(0, #state.target_overlays) + same(1, #state.temporary_overlays) + same("window-3", state.temporary_overlays[1].window) + same(0, #state.finalizers) + + same("timer-diagnostic", cleanup.highlight_timer) + same("window-3", cleanup.target_overlays[1].window) + same("buffer-3", cleanup.finalizers[1].buffer) + transitions:ClearTemporaryOverlays() +end) + +test("ClearAllLandingsAndDirection retains movement initialization", function() + local state, transitions = fresh_sequence_state() + local target = domain.TargetValue.character("a", 97) + transitions:BeginAcquisition("n", "f") + transitions:CommitAcquiredTarget("n", target) + transitions:CommitCommandSuccess("n", domain.Position.new(1, 9), true) + transitions:BeginAcquisition("v", "t") + transitions:CommitVisualSuccess("v", domain.Position.new(2, 1)) + + transitions:ClearAllLandingsAndDirection() + + same(nil, state:get_previous_landing("n")) + same(nil, state:get_previous_landing("v")) + falsy(state.moved_forward) + truthy(state.moved_forward_initialized) + same(domain.Descriptor.FIND_FORWARD, state:get_previous_descriptor("n")) + same(target, state:get_previous_target("n")) + same(domain.Descriptor.TILL_FORWARD, state:get_previous_descriptor("v")) +end) + for _, item in ipairs(tests) do local ok, failure = xpcall(item.body, debug.traceback) if not ok then @@ -841,4 +1206,4 @@ for _, item in ipairs(tests) do passed = passed + 1 end -io.stdout:write(string.format("Phase 3: %d tests passed\n", passed)) +io.stdout:write(string.format("Phase 4: %d tests passed\n", passed)) -- cgit v1.2.3