local script = debug.getinfo(1, "S").source:sub(2) local root = script:match("^(.*)/tests/run%.lua$") or "." package.path = table.concat({ root .. "/lua/?.lua", root .. "/lua/?/init.lua", package.path, }, ";") 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 = {} local passed = 0 local function test(name, body) tests[#tests + 1] = { name = name, body = body } end local function same(expected, actual, message) if expected ~= actual then error((message or "values differ") .. ": expected " .. tostring(expected) .. ", got " .. tostring(actual), 2) end end local function truthy(value, message) if not value then error(message or "value must be true", 2) end end local function falsy(value, message) if value then error(message or "value must be false", 2) end end local function fails(body, expected_text) local ok, failure = pcall(body) if ok then error("operation must fail", 2) end if expected_text and not tostring(failure):find(expected_text, 1, true) then error("failure does not contain '" .. expected_text .. "': " .. tostring(failure), 2) end end local function list_same(expected, actual) same(#expected, #actual, "list lengths differ") for index = 1, #expected do same(expected[index], actual[index], "list item " .. tostring(index) .. " differs") end end test("Position validates one-based byte coordinates", function() local position = domain.Position.new(1, 1) same(1, position.line) same(1, position.byte_column) same("Position", domain.type_of(position)) fails(function() domain.Position.new(0, 1) end, "line") fails(function() domain.Position.new(1, 0) end, "byte_column") fails(function() domain.Position.new(1.5, 1) end, "line") fails(function() position.line = 2 end, "immutable") end) test("Position comparison is lexicographic and detects stationarity", function() local first = domain.Position.new(1, 9) local later_column = domain.Position.new(1, 10) local later_line = domain.Position.new(2, 1) local same_place = domain.Position.new(1, 9) same(-1, domain.Position.compare(first, later_column)) same(-1, domain.Position.compare(later_column, later_line)) same(1, domain.Position.compare(later_line, first)) truthy(domain.Position.is_forward(later_line, first)) truthy(domain.Position.is_backward(first, later_line)) truthy(domain.Position.stationary(first, same_place)) falsy(domain.Position.is_forward(first, same_place)) falsy(domain.Position.is_backward(first, same_place)) truthy(first == same_place) local serialized = first:to_table() same(2, (function() local count = 0 for _ in pairs(serialized) do count = count + 1 end return count end)()) end) test("Descriptor conversion defines all families and directions", function() local cases = { { "f", domain.Family.FIND, domain.Direction.FORWARD, false }, { "F", domain.Family.FIND, domain.Direction.BACKWARD, true }, { "t", domain.Family.TILL, domain.Direction.FORWARD, false }, { "T", domain.Family.TILL, domain.Direction.BACKWARD, true }, } for _, case in ipairs(cases) do local descriptor = domain.Descriptor.from_string(case[1]) same(case[1], domain.Descriptor.to_string(descriptor)) same(case[2], descriptor.family) same(case[3], descriptor.direction) same(case[4], domain.Descriptor.is_uppercase(descriptor)) same(descriptor, domain.Descriptor.from_parts(case[2], case[3])) same(descriptor, domain.Descriptor.swap(domain.Descriptor.swap(descriptor))) end same(domain.Descriptor.FIND_BACKWARD, domain.Descriptor.swap("f")) same(domain.Descriptor.TILL_FORWARD, domain.Descriptor.swap("T")) same(domain.Descriptor.FIND_FORWARD, domain.Descriptor.lowercase("F")) same(domain.Descriptor.TILL_BACKWARD, domain.Descriptor.uppercase("t")) falsy(domain.Descriptor.is_valid("x")) fails(function() domain.Descriptor.from_string("x") end, "f, F, t, or T") end) test("Count normalizes absent input and rejects nonpositive values", function() same(domain.Count.ONE, domain.Count.new(nil)) same(1, domain.Count.to_number(nil)) same(3, domain.Count.to_number(domain.Count.new(3))) fails(function() domain.Count.new(0) end, "positive") fails(function() domain.Count.new(-1) end, "positive") fails(function() domain.Count.new(1.5) end, "positive") end) test("ModeContext normalizes every operator-pending variant", function() local operator = domain.ModeContext.from_full_mode("no") local variants = { "no", "nov", "noV", "no" .. string.char(0x16), } for _, mode in ipairs(variants) do local context = domain.ModeContext.from_full_mode(mode) same(operator, context) same("no", context.key) truthy(context.operator) truthy(context.command_path) end end) test("ModeContext preserves other full strings and exposes mode traits", function() local normal = domain.ModeContext.from_full_mode("n") local insert_normal = domain.ModeContext.from_full_mode("niI") local other_full_mode = domain.ModeContext.from_full_mode("normal-extension") local visual_character = domain.ModeContext.from_full_mode("v") local visual_line = domain.ModeContext.from_full_mode("V") local visual_block = domain.ModeContext.from_full_mode(string.char(0x16)) local visual_from_select = domain.ModeContext.from_full_mode("vs") local select_character = domain.ModeContext.from_full_mode("s") local select_line = domain.ModeContext.from_full_mode("S") local select_block = domain.ModeContext.from_full_mode(string.char(0x13)) same("n", normal.key) same("niI", insert_normal.key) same("normal-extension", other_full_mode.key) falsy(other_full_mode.operator) falsy(normal == insert_normal) same(domain.SelectionKind.CHARACTER, visual_character.visual_kind) same(domain.SelectionKind.LINE, visual_line.visual_kind) same(domain.SelectionKind.BLOCK, visual_block.visual_kind) same(domain.SelectionKind.CHARACTER, visual_from_select.visual_kind) falsy(visual_character.command_path) same(domain.SelectionKind.CHARACTER, select_character.select_kind) same(domain.SelectionKind.LINE, select_line.select_kind) same(domain.SelectionKind.BLOCK, select_block.select_kind) truthy(select_character.command_path) falsy(select_character.visual) truthy(select_character.select) end) test("Selection and text snapshots preserve semantic input values", function() local anchor = domain.Position.new(1, 1) local focus = domain.Position.new(1, 3) local selection = domain.Selection.active( domain.SelectionKind.CHARACTER, anchor, focus, domain.SelectionOption.EXCLUSIVE ) truthy(selection.active) same(domain.SelectionKind.CHARACTER, selection.kind) same(domain.SelectionOption.EXCLUSIVE, selection.option) same(domain.Position.new(1, 4), selection:with_focus(domain.Position.new(1, 4)).focus) local inactive = domain.Selection.inactive() falsy(inactive.active) same(domain.SelectionKind.NONE, inactive.kind) local text = domain.TextSnapshot.new({ "alpha", "beta" }) same(2, text.line_count) same("alpha", text:line(1)) local lines = text:lines() lines[1] = "changed" same("alpha", text:line(1)) end) test("TargetValue represents characters, special keys, and code fallback", function() local character = domain.TargetValue.character("a", 97) local special = domain.TargetValue.special_key(string.char(0x80, 0xfd, 0x01)) local fallback = domain.TargetValue.code_fallback() same(domain.TargetKind.CHARACTER, character.kind) same("a", character.value) same(97, character.first_code) same(domain.TargetKind.SPECIAL_KEY, special.kind) same(0x80, special.first_code) same(domain.TargetKind.CODE_FALLBACK, fallback.kind) same(0, fallback.first_code) same("", fallback.value) fails(function() domain.TargetValue.special_key("x") end, "hexadecimal 80") fails(function() character.value = "b" end, "immutable") end) test("InputPacket carries every input capability variant", function() local text = domain.InputPacket.text("a") local bytes = domain.InputPacket.raw_bytes({ 0x80, 0xfd, 0x60 }) local special = domain.InputPacket.special_key("Left", string.char(0x80, 1)) local failure = domain.InputPacket.error("input failed") same(domain.InputPacketKind.TEXT, text.kind) list_same({ 0x80, 0xfd, 0x60 }, bytes:bytes()) same("Left", special.name) same("input failed", failure.message) end) test("Plans and requests carry typed immutable motion data", function() local target = domain.TargetValue.character("h", 104) local target_plan = domain.TargetPlan.new({ target = target, kind = domain.TargetPlanKind.LITERAL, case_mode = domain.CaseMode.SENSITIVE, matcher = function(character) return character == "h" end, }) truthy(target_plan:matches("h")) falsy(target_plan:matches("H")) local motion_plan = domain.ResolvedMotionPlan.new({ target_plan = target_plan, descriptor = "f", search_scope = domain.SearchScope.BUFFER, endpoint_policy = domain.EndpointPolicy.REGULAR, }) same(domain.Descriptor.FIND_FORWARD, motion_plan.descriptor) same(target_plan, motion_plan.target_plan) local policy = { search_current_line_only = false } local request = domain.MotionRequest.new({ context = domain.ModeContext.from_full_mode("n"), origin = domain.Position.new(1, 1), descriptor = "f", target = target, count = nil, policy = policy, first_move = true, }) same(1, request.count.value) truthy(request.first_move) same(policy, request.policy) fails(function() motion_plan.descriptor = domain.Descriptor.FIND_BACKWARD end, "immutable") end) test("SearchOutcome distinguishes complete, partial, and first-step failure", function() local origin = domain.Position.new(1, 1) local reached = domain.Position.new(1, 5) local complete = domain.SearchOutcome.complete(reached, 3) local partial = domain.SearchOutcome.boundary_after_partial(reached, 2) local before = domain.SearchOutcome.boundary_before_any(origin) truthy(complete.complete) same(domain.SearchStatus.COMPLETE, complete.status) falsy(partial.complete) same(domain.SearchStatus.BOUNDARY_AFTER_PARTIAL, partial.status) same(2, partial.successful_steps) same(origin, before.endpoint) same(0, before.successful_steps) fails(function() domain.SearchOutcome.complete(reached, 0) end, "successful step") end) test("ActionOutcome and DotPayload retain resolved result data", function() local target = domain.TargetValue.character("e", 101) local payload = domain.DotPayload.new("t", target) local destination = domain.Position.new(1, 4) local complete = domain.SearchOutcome.complete(destination, 1) local movement = domain.ActionOutcome.from_search(complete, "t", payload) same(domain.ActionKind.MOVEMENT, movement.kind) same(destination, movement.position) truthy(movement.complete) same(1, movement.successful_steps) same(domain.Descriptor.TILL_FORWARD, movement.effective_descriptor) same(payload, movement.dot_payload) same("t", payload:to_table().descriptor) local failed = domain.ActionOutcome.from_search( domain.SearchOutcome.boundary_before_any(destination), "T" ) same(domain.ActionKind.FAILED_SEARCH, failed.kind) same(false, failed.complete) same(domain.ActionKind.NEUTRAL, domain.ActionOutcome.neutral(destination).kind) same(domain.ActionKind.ESCAPE, domain.ActionOutcome.escape(destination).kind) same(domain.ActionKind.EMPTY, domain.ActionOutcome.empty(destination).kind) same("problem", domain.ActionOutcome.error(destination, "problem").diagnostic) end) test("Capability contract reports every semantic method", function() local host = MemoryHost.new() same(host, capabilities.assert_implements(host)) same(0, #capabilities.missing_methods(host)) local required = capabilities.required_methods() truthy(#required >= 30) local incomplete = {} local missing = capabilities.missing_methods(incomplete) truthy(#missing == #required) fails(function() capabilities.assert_implements(incomplete) end, "semantic capabilities") end) test("MemoryHost supplies all semantic reads with domain values", function() local host = MemoryHost.new({ buffer_lines = { "alpha", "beta" }, cursor = { line = 2, byte_column = 2 }, mode = "nov", selection = { active = false, kind = "none", anchor = nil, focus = nil, option = "exclusive", }, count = 3, configuration = { enabled = false, triggers = { "x", "y" }, }, effective_encoding = "cp932", macro_register = "q", fold_open_policy = { "horizontal" }, closed_fold_levels = 2, pending_operator = "delete", time_values_ms = { 10.5, 12 }, }) same("alpha", host:read_text():line(1)) same(domain.Position.new(2, 2), host:read_cursor()) same("nov", host:read_mode()) same("no", host:read_mode_context().key) same("delete", host:read_pending_operator()) same(domain.SelectionOption.EXCLUSIVE, host:read_selection().option) same(3, host:read_count().value) truthy(host:configuration_present("enabled")) same(false, host:read_configuration("enabled")) local triggers = host:read_configuration("triggers") triggers[1] = "changed" same("x", host:read_configuration("triggers")[1]) same("cp932", host:read_encoding()) truthy(host:read_macro_state().executing) truthy(host:read_fold_state():opens("horizontal")) same(2, host:read_fold_state().closed_levels) same(10.5, host:read_time_ms()) same(12, host:read_time_ms()) end) test("MemoryHost queues movement events until action state commits", function() local host = MemoryHost.new({ buffer_lines = { "abc" }, cursor = { line = 1, byte_column = 1 }, }) local state = { committed = false } local observations = {} host:register_events("CursorMoved", function(name, payload) observations[#observations + 1] = { name = name, position = payload.cursor, committed = state.committed, } end) local token = host:begin_action_transition() host:apply_cursor(domain.Position.new(1, 2)) same(1, host:pending_event_count()) same(0, #observations) state.committed = true host:commit_action_transition(token) same(0, host:pending_event_count()) same(1, #observations) same("CursorMoved", observations[1].name) same(domain.Position.new(1, 2), observations[1].position) truthy(observations[1].committed) end) test("MemoryHost action invocation flushes events after the action callback", function() local host = MemoryHost.new({ buffer_lines = { "abc" } }) local state = { landing = nil } local observed_landing host:register_events("CursorMoved", function() observed_landing = state.landing end) host:register_action("Move", function() local destination = domain.Position.new(1, 3) host:apply_cursor(destination) state.landing = destination return "moved" end) same("moved", host:invoke_action("Move")) same(domain.Position.new(1, 3), observed_landing) end) test("MemoryHost models movement, input, folds, and visible effects", function() local host = MemoryHost.new({ buffer_lines = { "abcd" }, cursor = { line = 1, byte_column = 1 }, selection = { active = true, kind = "character", anchor = { line = 1, byte_column = 1 }, focus = { line = 1, byte_column = 1 }, option = "inclusive", }, input_packets = { { kind = "text", text = "d" }, { kind = "raw_bytes", bytes = { 0x80, 0xfd, 0x60 } }, { kind = "error", message = "read failed" }, }, fold_open_policy = { "all" }, closed_fold_levels = 2, }) host:apply_selection(domain.Position.new(1, 2), domain.SelectionKind.CHARACTER) same(domain.Position.new(1, 2), host:read_cursor()) same(domain.Position.new(1, 2), host:read_selection().focus) host:set_operator_inclusive(true) truthy(host:operator_inclusive()) same("d", host:read_input().text) list_same({ 0x80, 0xfd, 0x60 }, host:read_input():bytes()) fails(function() host:read_input() end, "read failed") truthy(host:open_fold()) truthy(host:open_fold()) falsy(host:open_fold()) same(0, host:read_fold_state().closed_levels) host:show_prompt("clever-f: ") host:redraw("screen") host:redraw("full") host:emit_diagnostic("error", "problem") same("clever-f: ", host:prompts()[1]) list_same({ "screen", "full" }, host:redraws()) same("problem", host:diagnostics()[1].text) end) test("MemoryHost manages highlight and cursor-presentation resources", function() local presentation = { guicursor = "n-v:block", terminal_cursor_visible = true, hidden = false, } local host = MemoryHost.new({ cursor_presentation = presentation }) local highlight = host:create_highlight({ group = "CleverFChar", identity = "char-1", position = domain.Position.new(1, 1), priority = "high", }) same("char-1", highlight) truthy(host:highlights()[highlight] ~= nil) truthy(host:remove_highlight(highlight)) falsy(host:remove_highlight(highlight)) local lease = host:suppress_cursor_presentation() truthy(host:cursor_presentation().hidden) truthy(host:restore_cursor_presentation(lease)) local restored = host:cursor_presentation() same(presentation.guicursor, restored.guicursor) same(presentation.terminal_cursor_visible, restored.terminal_cursor_visible) same(presentation.hidden, restored.hidden) end) test("MemoryHost manages timers, events, mappings, and dot repeat", function() local host = MemoryHost.new() local fired truthy(host:supports_timers()) local timer = host:start_timer(25, function(identity) fired = identity end) truthy(host:timers()[timer].active) truthy(host:fire_timer(timer)) same(timer, fired) falsy(host:timers()[timer].active) falsy(host:stop_timer(timer)) local delivered local registration = host:register_events({ "InsertEnter", "TextChanged" }, function(name) delivered = name end, { buffer = "buffer-1" }) host:deliver_event("InsertEnter", {}) same("InsertEnter", delivered) truthy(host:remove_event_registration(registration)) delivered = nil host:deliver_event("TextChanged", {}) same(nil, delivered) host:register_action("Neutral", function() return domain.ActionOutcome.neutral(domain.Position.new(1, 1)) end) local mapping = host:register_mapping( { "n", "x", "o" }, "f", "Neutral", { silent = true, remap = false } ) same("f", host:mappings()[mapping].lhs) truthy(host:mappings()[mapping].options.silent) local target = domain.TargetValue.character("a", 97) local payload = domain.DotPayload.new("f", target) host:register_dot_repeat(payload, function(replayed, count) return replayed, count end) same(payload, host:dot_repeat_payload()) local replayed, count = host:replay_dot(2) same(payload, replayed) same(2, count.value) end) test("Policy schema contains every default and color target", function() local defaults = policy.defaults() local false_settings = { "search_current_line_only", "ignore_case", "smart_case", "use_migemo", "fix_key_direction", "show_prompt", "mark_direct", } local true_settings = { "mark_cursor", "hide_cursor_on_cmdline", "mark_char", "clean_labels_eagerly", } for _, name in ipairs(false_settings) do same(false, defaults[name], name) end for _, name in ipairs(true_settings) do same(true, defaults[name], name) end same("", defaults.chars_match_any_signs) same(0, defaults.repeat_timeout_ms) same(0, defaults.highlight_timeout_ms) list_same({ "\r" }, defaults.repeat_last_char_inputs) same(nil, policy.default("mark_cursor_color")) same(nil, policy.default("mark_char_color")) same(nil, policy.default("mark_direct_color")) local schema = policy.schema() same("Cursor", schema.mark_cursor_color.default_target) same("CleverFDefaultLabel", schema.mark_char_color.default_target) same("CleverFDefaultLabel", schema.mark_direct_color.default_target) same(policy.ValueType.OPTIONAL_GROUP_NAME, schema.mark_cursor_color.value_type) same(policy.ValueType.PRESENCE, schema[policy.DEFAULT_MAP_SUPPRESSION_SENTINEL].value_type) defaults.repeat_last_char_inputs[1] = "changed" schema.repeat_last_char_inputs.default[1] = "changed" list_same({ "\r" }, policy.default("repeat_last_char_inputs")) end) test("Policy typed accessors validate semantic values", function() local host = MemoryHost.new({ configuration = { search_current_line_only = true, chars_match_any_signs = ";:", repeat_last_char_inputs = { "x", "" }, mark_cursor_color = "IncSearch", repeat_timeout_ms = 25, }, }) local service = policy.new(host) truthy(service:get_boolean("search_current_line_only")) same(";:", service:get_string("chars_match_any_signs")) list_same({ "x", "" }, service:get_string_list("repeat_last_char_inputs")) same("IncSearch", service:get_optional_group_name("mark_cursor_color")) same(25, service:get_nonnegative_integer("repeat_timeout_ms")) same(nil, service:get_optional_group_name("mark_char_color")) local inputs = service:get_string_list("repeat_last_char_inputs") inputs[1] = "changed" same("x", service:get_string_list("repeat_last_char_inputs")[1]) host:set_configuration("ignore_case", 1) fails(function() service:get_boolean("ignore_case") end, "must be a Boolean") host:set_configuration("chars_match_any_signs", {}) fails(function() service:get_string("chars_match_any_signs") end, "must be a string") host:set_configuration("repeat_last_char_inputs", { "x", 2 }) fails(function() service:get_string_list("repeat_last_char_inputs") end, "list of strings") host:set_configuration("mark_cursor_color", "") fails(function() service:get_optional_group_name("mark_cursor_color") end, "optional group name") host:set_configuration("repeat_timeout_ms", -1) fails(function() service:get_nonnegative_integer("repeat_timeout_ms") end, "nonnegative integer") host:set_configuration("repeat_timeout_ms", 1.5) fails(function() service:get_nonnegative_integer("repeat_timeout_ms") end, "nonnegative integer") fails(function() service:get_string("ignore_case") end, "does not have type string") fails(function() service:get("unknown") end, "unknown policy setting") end) test("Default-map suppression is presence-based and activation values are retained", function() local absent_host = MemoryHost.new() local absent_policy = policy.new(absent_host) falsy(absent_policy:default_maps_suppressed()) truthy(absent_policy:capture_activation().install_default_mappings) for _, sentinel_value in ipairs({ false, 0 }) do local configuration = { clean_labels_eagerly = false, } configuration[policy.DEFAULT_MAP_SUPPRESSION_SENTINEL] = sentinel_value local host = MemoryHost.new({ configuration = configuration }) local service = policy.new(host) host:clear_operations() truthy(service:default_maps_suppressed()) local operations = host:operations() same(1, #operations) same("configuration_present", operations[1].operation) local activation = service:capture_activation() falsy(activation.install_default_mappings) falsy(activation.clean_labels_eagerly) host:unset_configuration(policy.DEFAULT_MAP_SUPPRESSION_SENTINEL) host:set_configuration("clean_labels_eagerly", true) local retained = service:capture_activation() falsy(retained.install_default_mappings) falsy(retained.clean_labels_eagerly) local next_activation = policy.new(host):capture_activation() truthy(next_activation.install_default_mappings) truthy(next_activation.clean_labels_eagerly) end end) test("Runtime policy samples read current behavior values", function() local host = MemoryHost.new() local service = policy.new(host) local target = domain.TargetValue.character("a", 97) same(domain.SearchScope.BUFFER, service:sample_search().search_scope) same(domain.CaseMode.SENSITIVE, service:sample_match(target).case_mode) falsy(service:sample_direction().fix_key_direction) falsy(service:sample_acquisition().show_prompt) truthy(service:sample_markers().mark_char) same(0, service:sample_timeouts().repeat_timeout_ms) list_same({ "\r" }, service:sample_previous_input().repeat_last_char_inputs) host:set_configuration("search_current_line_only", true) host:set_configuration("ignore_case", true) host:set_configuration("smart_case", true) host:set_configuration("use_migemo", true) host:set_configuration("chars_match_any_signs", ";") host:set_configuration("fix_key_direction", true) host:set_configuration("show_prompt", true) host:set_configuration("mark_cursor", false) host:set_configuration("hide_cursor_on_cmdline", false) host:set_configuration("mark_char", false) host:set_configuration("mark_direct", true) host:set_configuration("repeat_timeout_ms", 75) host:set_configuration("highlight_timeout_ms", 125) host:set_configuration("repeat_last_char_inputs", { "x", "yz" }) local search = service:sample_search() truthy(search.search_current_line_only) same(domain.SearchScope.CURRENT_LINE, search.search_scope) local match = service:sample_match(target) truthy(match.ignore_case) truthy(match.smart_case) truthy(match.use_migemo) same(";", match.chars_match_any_signs) same(domain.CaseMode.INSENSITIVE, match.case_mode) truthy(service:sample_direction().fix_key_direction) local acquisition = service:sample_acquisition() truthy(acquisition.show_prompt) falsy(acquisition.mark_cursor) falsy(acquisition.hide_cursor_on_cmdline) truthy(acquisition.mark_direct) local markers = service:sample_markers() falsy(markers.mark_cursor) falsy(markers.mark_char) truthy(markers.mark_direct) local timeouts = service:sample_timeouts() same(75, timeouts.repeat_timeout_ms) same(125, timeouts.highlight_timeout_ms) list_same({ "x", "yz" }, service:sample_previous_input().repeat_last_char_inputs) end) test("Highlight policy is reevaluated with live feature and color values", function() local host = MemoryHost.new() local service = policy.new(host) service:capture_activation() local initial = service:evaluate_highlight_links() truthy(initial.CleverFCursor.enabled) same(nil, initial.CleverFCursor.configured_target) same("Cursor", initial.CleverFCursor.target) truthy(initial.CleverFChar.enabled) same("CleverFDefaultLabel", initial.CleverFChar.target) falsy(initial.CleverFDirect.enabled) same("CleverFDefaultLabel", initial.CleverFDirect.target) host:set_configuration("mark_cursor", false) host:set_configuration("mark_cursor_color", "Search") host:set_configuration("mark_char", false) host:set_configuration("mark_char_color", "ErrorMsg") host:set_configuration("mark_direct", true) host:set_configuration("mark_direct_color", "IncSearch") local refreshed = service:evaluate_highlight_links() falsy(refreshed.CleverFCursor.enabled) same("Search", refreshed.CleverFCursor.target) falsy(refreshed.CleverFChar.enabled) same("ErrorMsg", refreshed.CleverFChar.target) truthy(refreshed.CleverFDirect.enabled) same("IncSearch", refreshed.CleverFDirect.target) end) test("Case policy resolves an explicit mode for each target", function() local host = MemoryHost.new() local service = policy.new(host) local lower = domain.TargetValue.character("a", 97) local upper = domain.TargetValue.character("A", 65) local multibyte = domain.TargetValue.character("\227\129\130", 0x3042) same(domain.CaseMode.SENSITIVE, service:case_mode(lower)) host:set_configuration("smart_case", true) same(domain.CaseMode.INSENSITIVE, service:case_mode(lower)) same(domain.CaseMode.SENSITIVE, service:case_mode(upper)) same(domain.CaseMode.SENSITIVE, service:case_mode(multibyte)) host:set_configuration("ignore_case", true) same(domain.CaseMode.INSENSITIVE, service:case_mode(upper)) local match = service:sample_match(upper) local plan = domain.TargetPlan.new({ target = upper, kind = domain.TargetPlanKind.LITERAL, case_mode = match.case_mode, matcher = function() return true end, }) same(domain.CaseMode.INSENSITIVE, plan.case_mode) end) test("Unsupported Migemo policy mutation updates live configuration", function() local host = MemoryHost.new({ configuration = { use_migemo = true }, }) local service = policy.new(host) truthy(service:get_boolean("use_migemo")) host:clear_operations() service:disable_migemo_for_unsupported_encoding() local operations = host:operations() same(1, #operations) same("write_configuration", operations[1].operation) same("use_migemo", operations[1].name) same(false, operations[1].value) falsy(service:get_boolean("use_migemo")) host:set_configuration("use_migemo", true) 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 io.stderr:write("FAIL: " .. item.name .. "\n" .. tostring(failure) .. "\n") os.exit(1) end passed = passed + 1 end io.stdout:write(string.format("Phase 4: %d tests passed\n", passed))