create runner with ChatGPT-Codex by using the AGENTS.md
Some checks failed
tests / test (push) Failing after 4s

This commit is contained in:
2026-01-03 14:53:56 +01:00
commit b70d26256c
8 changed files with 1459 additions and 0 deletions

View File

@@ -0,0 +1,660 @@
local runner = {
name = "jest",
framework = "javascript",
}
local RESULT_PREFIX = "TSAMURAI_RESULT "
local STATUS_MAP = {
passed = "passes",
failed = "failures",
skipped = "skips",
pending = "skips",
todo = "skips",
}
runner._last_locations = {}
runner._last_jest_names = {}
local function get_buf_path(bufnr)
return vim.api.nvim_buf_get_name(bufnr)
end
local function get_buf_lines(bufnr)
return vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
end
local function find_root(path, markers)
if not path or path == "" then
return nil
end
local dir = vim.fs.dirname(path)
if not dir or dir == "" then
return nil
end
local found = vim.fs.find(markers, { path = dir, upward = true })
if not found or not found[1] then
return nil
end
return vim.fs.dirname(found[1])
end
local function count_char(line, ch)
local count = 0
for i = 1, #line do
if line:sub(i, i) == ch then
count = count + 1
end
end
return count
end
local function find_block_end(lines, start_idx)
local depth = 0
local started = false
for i = start_idx, #lines do
local line = lines[i]
for j = 1, #line do
local ch = line:sub(j, j)
if ch == "{" then
depth = depth + 1
started = true
elseif ch == "}" then
if started then
depth = depth - 1
if depth == 0 then
return i
end
end
end
end
end
return start_idx
end
local function match_call_name(lines, idx, keywords)
local line = lines[idx] or ""
for _, key in ipairs(keywords) do
local pattern = "%f[%w_]" .. key .. "[%w_%.]*%s*%(%s*['\"]([^'\"]+)['\"]"
local name = line:match(pattern)
if name and name ~= "" then
return name
end
local has_call = line:match("%f[%w_]" .. key .. "[%w_%.]*%s*%(")
if has_call then
local max_idx = math.min(#lines, idx + 3)
for j = idx + 1, max_idx do
local next_line = lines[j] or ""
local next_name = next_line:match("['\"]([^'\"]+)['\"]")
if next_name and next_name ~= "" then
return next_name
end
if next_line:find("%)") then
break
end
end
end
end
return nil
end
local function escape_regex(text)
text = text or ""
return (text:gsub("([\\.^$|()%%[%]{}*+?%-])", "\\\\%1"))
end
local function build_jest_pattern(parts)
local escaped = {}
for _, part in ipairs(parts) do
table.insert(escaped, escape_regex(part))
end
return "^.*" .. escape_regex(parts[#parts] or "") .. "$"
end
local function build_jest_prefix_pattern(parts)
local tokens = {}
for _, part in ipairs(parts or {}) do
for token in part:gmatch("%w+") do
table.insert(tokens, token)
end
end
if #tokens == 0 then
return escape_regex(parts[#parts] or "") .. ".*"
end
return table.concat(tokens, ".*") .. ".*"
end
local function to_jest_full_name(name)
if not name or name == "" then
return name
end
if not name:find("/", 1, true) then
return name
end
local parts = vim.split(name, "/", { plain = true, trimempty = true })
return table.concat(parts, " ")
end
local function find_tests(lines)
local tests = {}
local describes = {}
for i, _line in ipairs(lines) do
local describe_name = match_call_name(lines, i, { "describe", "context" })
if describe_name then
local start_idx = i
local end_idx = find_block_end(lines, start_idx)
table.insert(describes, {
name = describe_name,
start = start_idx - 1,
["end"] = end_idx - 1,
})
end
local test_name = match_call_name(lines, i, { "test", "it" })
if test_name then
local start_idx = i
local end_idx = find_block_end(lines, start_idx)
table.insert(tests, {
name = test_name,
start = start_idx - 1,
["end"] = end_idx - 1,
})
end
end
local function describe_chain(at_start)
local parents = {}
for _, describe in ipairs(describes) do
if at_start >= describe.start and at_start <= describe["end"] then
table.insert(parents, describe)
end
end
table.sort(parents, function(a, b)
if a.start == b.start then
return a["end"] < b["end"]
end
return a.start < b.start
end)
return parents
end
for _, test in ipairs(tests) do
local parents = describe_chain(test.start)
local parts = {}
for _, parent in ipairs(parents) do
table.insert(parts, parent.name)
end
table.insert(parts, test.name)
test.full_name = table.concat(parts, "/")
test.jest_name = table.concat(parts, " ")
test.jest_parts = parts
end
for _, describe in ipairs(describes) do
local parents = describe_chain(describe.start)
local parts = {}
for _, parent in ipairs(parents) do
table.insert(parts, parent.name)
end
describe.full_name = table.concat(parts, "/")
describe.jest_name = table.concat(parts, " ")
describe.jest_parts = parts
end
return tests, describes
end
local function test_file_path(path)
if not path or path == "" then
return false
end
return path:match("%.test%.[jt]sx?$") ~= nil
or path:match("%.spec%.[jt]sx?$") ~= nil
or path:match("%.test%.mjs$") ~= nil
or path:match("%.spec%.mjs$") ~= nil
or path:match("%.test%.cjs$") ~= nil
or path:match("%.spec%.cjs$") ~= nil
end
local function collect_unique(list)
local out = {}
local seen = {}
for _, item in ipairs(list) do
if item and item ~= "" and not seen[item] then
seen[item] = true
table.insert(out, item)
end
end
return out
end
local function order_by_root(names)
local roots = {}
local seen_root = {}
local buckets = {}
for _, name in ipairs(names) do
local root = name:match("^[^/]+") or name
if not seen_root[root] then
seen_root[root] = true
table.insert(roots, root)
end
buckets[root] = buckets[root] or { main = nil, subs = {} }
if name == root then
buckets[root].main = name
else
table.insert(buckets[root].subs, name)
end
end
local ordered = {}
for _, root in ipairs(roots) do
local bucket = buckets[root]
if bucket.main then
table.insert(ordered, bucket.main)
end
for _, sub in ipairs(bucket.subs) do
table.insert(ordered, sub)
end
end
return ordered
end
local function order_with_display(names, display_map)
local ordered = order_by_root(names)
local display = {}
for _, name in ipairs(ordered) do
display[#display + 1] = display_map[name] or name
end
return ordered, display
end
local function split_output_lines(text)
if not text or text == "" then
return {}
end
local lines = vim.split(text, "\n", { plain = true })
if #lines > 0 and lines[#lines] == "" then
table.remove(lines, #lines)
end
return lines
end
local function reporter_path()
local source = debug.getinfo(1, "S").source
if source:sub(1, 1) == "@" then
source = source:sub(2)
end
local dir = vim.fs.dirname(source)
return vim.fs.normalize(dir .. "/../../reporter/test_samurai_jest_reporter.js")
end
local function base_cmd()
return {
"npx",
"jest",
"--testLocationInResults",
"--reporters",
reporter_path(),
}
end
local function parse_result_line(line)
if not line or line == "" then
return nil
end
if line:sub(1, #RESULT_PREFIX) ~= RESULT_PREFIX then
return nil
end
local payload = line:sub(#RESULT_PREFIX + 1)
local ok, data = pcall(vim.json.decode, payload)
if not ok or type(data) ~= "table" then
return nil
end
return data
end
local function update_location_cache(name, data)
if not name or name == "" then
return
end
local location = data.location
if type(location) ~= "table" or not data.file then
return
end
runner._last_locations[name] = {
filename = data.file,
lnum = location.line or 1,
col = location.column or 1,
text = name,
}
if data.jestName and data.jestName ~= "" then
runner._last_jest_names[name] = data.jestName
end
end
function runner.is_test_file(bufnr)
local path = get_buf_path(bufnr)
return test_file_path(path)
end
function runner.find_nearest(bufnr, row, _col)
local path = get_buf_path(bufnr)
if not path or path == "" then
return nil, "no file name"
end
if not test_file_path(path) then
return nil, "not a jest test file"
end
local lines = get_buf_lines(bufnr)
local tests, describes = find_tests(lines)
local nearest = nil
for _, test in ipairs(tests) do
if row >= test.start and row <= test["end"] then
nearest = test
end
end
if not nearest then
local describe_match = nil
for _, describe in ipairs(describes) do
if row >= describe.start and row <= describe["end"] then
if not describe_match or describe.start >= describe_match.start then
describe_match = describe
end
end
end
if describe_match then
local cwd = find_root(path, {
"package.json",
"jest.config.js",
"jest.config.ts",
"jest.config.cjs",
"jest.config.mjs",
"jest.config.json",
})
return {
file = path,
cwd = cwd,
test_name = describe_match.name,
full_name = describe_match.full_name,
jest_name = describe_match.jest_name,
jest_parts = describe_match.jest_parts,
kind = "describe",
}
end
local cwd = find_root(path, {
"package.json",
"jest.config.js",
"jest.config.ts",
"jest.config.cjs",
"jest.config.mjs",
"jest.config.json",
})
return {
file = path,
cwd = cwd,
kind = "file",
}
end
local cwd = find_root(path, {
"package.json",
"jest.config.js",
"jest.config.ts",
"jest.config.cjs",
"jest.config.mjs",
"jest.config.json",
})
return {
file = path,
cwd = cwd,
test_name = nearest.name,
full_name = nearest.full_name,
jest_name = nearest.jest_name,
jest_parts = nearest.jest_parts,
kind = "test",
}
end
function runner.build_command(spec)
local file = spec.file
if not file or file == "" then
return { cmd = base_cmd(), cwd = spec.cwd }
end
if spec.kind == "file" then
local cmd = base_cmd()
table.insert(cmd, "--runTestsByPath")
table.insert(cmd, file)
return { cmd = cmd, cwd = spec.cwd }
end
local cmd = base_cmd()
table.insert(cmd, "--runTestsByPath")
table.insert(cmd, file)
local ok, pattern = pcall(function()
if type(spec.jest_parts) == "table" and #spec.jest_parts > 0 then
if spec.kind == "describe" then
return build_jest_prefix_pattern(spec.jest_parts)
end
return build_jest_pattern(spec.jest_parts)
end
local name = spec.jest_name or spec.test_name
if name and name ~= "" then
return "^" .. escape_regex(name) .. "$"
end
return nil
end)
if not ok then
pattern = nil
end
if pattern then
table.insert(cmd, "--testNamePattern")
table.insert(cmd, pattern)
end
return { cmd = cmd, cwd = spec.cwd }
end
function runner.build_file_command(bufnr)
local path = get_buf_path(bufnr)
local cwd = find_root(path, {
"package.json",
"jest.config.js",
"jest.config.ts",
"jest.config.cjs",
"jest.config.mjs",
"jest.config.json",
})
local cmd = base_cmd()
table.insert(cmd, "--runTestsByPath")
table.insert(cmd, path)
return { cmd = cmd, cwd = cwd }
end
function runner.build_all_command(bufnr)
local path = get_buf_path(bufnr)
local cwd = find_root(path, {
"package.json",
"jest.config.js",
"jest.config.ts",
"jest.config.cjs",
"jest.config.mjs",
"jest.config.json",
})
local cmd = base_cmd()
return { cmd = cmd, cwd = cwd }
end
function runner.build_failed_command(last_command, failures, _scope_kind)
if not failures or #failures == 0 then
if last_command and last_command.cmd then
return { cmd = last_command.cmd, cwd = last_command.cwd }
end
return { cmd = base_cmd() }
end
local pattern_parts = {}
for _, name in ipairs(failures or {}) do
if name and name ~= "" then
local jest_name = runner._last_jest_names[name] or to_jest_full_name(name)
table.insert(pattern_parts, "^" .. escape_regex(jest_name) .. "$")
end
end
local pattern = "(" .. table.concat(pattern_parts, "|") .. ")"
local cmd = {}
local skip_next = false
for _, arg in ipairs(last_command and last_command.cmd or {}) do
if skip_next then
skip_next = false
elseif arg == "--testNamePattern" then
skip_next = true
else
table.insert(cmd, arg)
end
end
if #cmd == 0 then
cmd = base_cmd()
end
table.insert(cmd, "--testNamePattern")
table.insert(cmd, pattern)
return {
cmd = cmd,
cwd = last_command and last_command.cwd or nil,
}
end
function runner.parse_results(output)
runner._last_locations = {}
runner._last_jest_names = {}
if not output or output == "" then
return { passes = {}, failures = {}, skips = {}, display = { passes = {}, failures = {}, skips = {} } }
end
local passes = {}
local failures = {}
local skips = {}
local display = { passes = {}, failures = {}, skips = {} }
local pass_display = {}
local fail_display = {}
local skip_display = {}
local seen = {
passes = {},
failures = {},
skips = {},
}
for line in output:gmatch("[^\n]+") do
local data = parse_result_line(line)
if data and data.name and data.status then
local kind = STATUS_MAP[data.status]
if kind and not seen[kind][data.name] then
seen[kind][data.name] = true
table.insert(kind == "passes" and passes or kind == "failures" and failures or skips, data.name)
if kind == "passes" then
pass_display[data.name] = data.display or data.name
elseif kind == "failures" then
fail_display[data.name] = data.display or data.name
else
skip_display[data.name] = data.display or data.name
end
update_location_cache(data.name, data)
end
end
end
passes = collect_unique(passes)
failures = collect_unique(failures)
skips = collect_unique(skips)
passes, display.passes = order_with_display(passes, pass_display)
failures, display.failures = order_with_display(failures, fail_display)
skips, display.skips = order_with_display(skips, skip_display)
return { passes = passes, failures = failures, skips = skips, display = display }
end
function runner.output_parser()
runner._last_locations = {}
runner._last_jest_names = {}
return {
on_line = function(line, state)
local data = parse_result_line(line)
if not data or not data.name or not data.status then
return nil
end
local kind = STATUS_MAP[data.status]
if not kind then
return nil
end
state.jest = state.jest or { failures_all = {}, failures_seen = {} }
local results = {
passes = {},
failures = {},
skips = {},
display = { passes = {}, failures = {}, skips = {} },
}
if kind == "passes" then
results.passes = { data.name }
results.display.passes = { data.display or data.name }
elseif kind == "failures" then
results.failures = { data.name }
results.display.failures = { data.display or data.name }
if not state.jest.failures_seen[data.name] then
state.jest.failures_seen[data.name] = true
table.insert(state.jest.failures_all, data.name)
end
results.failures_all = vim.deepcopy(state.jest.failures_all)
else
results.skips = { data.name }
results.display.skips = { data.display or data.name }
end
update_location_cache(data.name, data)
return results
end,
on_complete = function(output, state)
return nil
end,
}
end
function runner.parse_test_output(output)
local out = {}
if not output or output == "" then
return out
end
for line in output:gmatch("[^\n]+") do
local data = parse_result_line(line)
if data and data.name and data.output then
out[data.name] = out[data.name] or {}
if type(data.output) == "string" then
for _, item in ipairs(split_output_lines(data.output)) do
table.insert(out[data.name], item)
end
elseif type(data.output) == "table" then
for _, item in ipairs(data.output) do
if item and item ~= "" then
table.insert(out[data.name], item)
end
end
end
end
end
return out
end
function runner.collect_failed_locations(failures, _command, _scope_kind)
if type(failures) ~= "table" or #failures == 0 then
return {}
end
local items = {}
local seen = {}
for _, name in ipairs(failures) do
local loc = runner._last_locations[name]
if loc then
local key = string.format("%s:%d:%d:%s", loc.filename or "", loc.lnum or 0, loc.col or 0, name)
if not seen[key] then
seen[key] = true
table.insert(items, loc)
end
end
end
return items
end
return runner