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 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) 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 2: %d tests passed\n", passed))