local testimony = require("testimony") local std = require("std") local mneme = require("mneme") local testify = testimony.new("== learned completions: stale handle, chains, frecency, flags ==") local tmp_path = function() return "/tmp/mneme_learned_" .. std.nanoid() .. ".mneme" end testify:that("read handle sees entries after same-handle write", function() local path = tmp_path() local db = mneme.open(path, { sync = "none" }) local ks = db:keyspace("completions") local zs = ks:sorted_set("git") zs:add(1, "status") zs:add(1, "diff") local found = {} for score, member in ks:sorted_set("git"):iter({ reverse = true }) do found[member] = true end testimony.assert_true(found["status"]) testimony.assert_true(found["diff"]) db:close() std.fs.remove(path) end) testify:that("second handle goes stale after first handle writes to same file", function() local path = tmp_path() -- handle_A: like the shell store (writes history) local handle_A = mneme.open(path, { sync = "none" }) -- Seed completions data via handle_A local comp_ks = handle_A:keyspace("completions") local zs = comp_ks:sorted_set("git") zs:add(1, "status") zs:add(1, "diff") -- handle_B: like the learned source (reads completions) local handle_B = mneme.open(path, { sync = "none" }) -- First read via handle_B: should work local found1 = {} local ks_b = handle_B:keyspace("completions") for score, member in ks_b:sorted_set("git"):iter({ reverse = true }) do found1[member] = true end testimony.assert_true(found1["status"]) testimony.assert_true(found1["diff"]) -- Now handle_A writes to a DIFFERENT keyspace (simulating history save) -- This is what the shell store does after every command. for i = 1, 5 do local hist_ks = handle_A:keyspace("shell") local hist_zs = hist_ks:sorted_set("entries") hist_zs:add(1000 + i, "cmd_" .. i) end -- Read via handle_B again WITHOUT reopening. -- This may fail because handle_B's mmap is stale. local found2 = {} for score, member in handle_B:keyspace("completions"):sorted_set("git"):iter({ reverse = true }) do found2[member] = true end -- This assertion documents the bug: handle_B may see corrupted data. -- On some runs it may pass (kernel page cache), on others it fails. -- The fix is to close and reopen handle_B before reading. -- Now verify the fix: close handle_B, reopen, and read again handle_B:close() handle_B = mneme.open(path, { sync = "none" }) local found3 = {} for score, member in handle_B:keyspace("completions"):sorted_set("git"):iter({ reverse = true }) do found3[member] = true end testimony.assert_true(found3["status"]) testimony.assert_true(found3["diff"]) handle_A:close() handle_B:close() std.fs.remove(path) end) -- ── Chain-aware / frecency / flag completions ──────────────────────── local learned = require("shell.completion.source.learned") local has = function(candidates, suffix) for _, c in ipairs(candidates) do if c == suffix then return true end end return false end local new_source = function(now) local src = learned.new({ db_path = tmp_path() }) if now then src.__state.now_fn = now end return src end testify:that("depth-1 chain is learned and completed", function() local src = new_source() src:record("git remote add origin ssh://x", 0) src:record("git status", 0) -- depth-1: git remote -> add (search returns the suffix after "a") local d1 = src:search("git", { "remote", "a" }) testimony.assert_true(has(d1, "dd ")) -- depth-0 still works: git -> remote, git -> status testimony.assert_true(has(src:search("git", { "r" }), "emote ")) testimony.assert_true(has(src:search("git", { "s" }), "tatus ")) src:close() end) testify:that("depth-2 chain is learned and completed", function() local src = new_source() src:record("docker container ls", 0) testimony.assert_true(has(src:search("docker", { "container", "l" }), "s ")) testimony.assert_true(has(src:search("docker", { "c" }), "ontainer ")) src:close() end) testify:that("more recent use ranks first (frecency)", function() local clock = { t = 1735689600 + 30 * 86400 } local src = new_source(function() return clock.t end) src:record("git pull", 0) -- older clock.t = clock.t + 90 * 86400 src:record("git push", 0) -- newer local res = src:search("git", { "p" }) testimony.assert_true(#res >= 2) testimony.assert_equal(res[1], "ush ") -- push before pull src:close() end) testify:that("flags are learned per context, values are not", function() local src = new_source() src:record('git commit -m "x"', 0) src:record("git commit --amend", 0) local flags = src:search("git", { "commit", "-" }) testimony.assert_true(has(flags, "m ")) -- -m (suffix after "-") testimony.assert_true(has(flags, "-amend ")) -- --amend (suffix after "-") -- the flag value "x" must not be recorded as a subcommand testimony.assert_true(#src:search("git", { "x" }) == 0) src:close() end) testify:that("path-like tokens stop the chain", function() local src = new_source() src:record("git add ./file.lua", 0) -- "add" is learned under git, but "./file.lua" is not under git\x1fadd testimony.assert_true(has(src:search("git", { "a" }), "dd ")) testimony.assert_true(#src:search("git", { "add", "f" }) == 0) src:close() end) testify:that("context deeper than MAX_CHAIN_DEPTH yields nothing", function() local src = new_source() src:record("a b c d e f", 0) -- chain caps at {b,c,d,e} -- depth-3 prefix (a b c d -> e) is the deepest recorded context testimony.assert_true(#src:search("a", { "b", "c", "d", "e" }) > 0) -- depth-4 prefix exceeds MAX_CHAIN_DEPTH and returns nothing testimony.assert_true(#src:search("a", { "b", "c", "d", "e", "x" }) == 0) src:close() end) testify:that("separator-bearing tokens are rejected without crashing", function() local src = new_source() src:record("git \x1fweird sub", 0) -- must not crash testimony.assert_true(#src:search("git", { "\x1fweird", "s" }) == 0) src:close() end) testify:conclude()