summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/run.lua367
1 files changed, 366 insertions, 1 deletions
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))