local describe, it, expect = lust.describe, lust.it, lust.expect
if not log then
log = { warn = function() end, info = function() end, debug = function() end }
end
if not std then
std = {
env = { get = function() return nil end, get_or = function(_n, d) return d end },
json = { encode = function(v) return tostring(v) end },
}
end
local CL = require("compile_loop")
local H = CL._test_helpers()
local function contains(haystack, needle)
return haystack:find(needle, 1, true) ~= nil
end
local function sr_block(search, replace)
return "<<<<<<< SEARCH\n" .. search .. "\n=======\n" .. replace .. "\n>>>>>>> REPLACE"
end
describe("compile_loop.parse_search_replace (single-file)", function()
local parse = H.parse_search_replace
it("parses one well-formed block with nil path", function()
local blocks, err = parse(sr_block("old line", "new line"), false, {})
expect(err).to.equal(nil)
expect(#blocks).to.equal(1)
expect(blocks[1].path).to.equal(nil)
expect(blocks[1].search).to.equal("old line")
expect(blocks[1].replace).to.equal("new line")
end)
it("parses two consecutive blocks", function()
local text = sr_block("a", "A") .. "\n" .. sr_block("b", "B")
local blocks, err = parse(text, false, {})
expect(err).to.equal(nil)
expect(#blocks).to.equal(2)
expect(blocks[1].replace).to.equal("A")
expect(blocks[2].replace).to.equal("B")
end)
it("errors with no blocks found on plain text", function()
local blocks, err = parse("just some prose, no markers", false, {})
expect(blocks).to.equal(nil)
expect(contains(err, "no SEARCH/REPLACE blocks found")).to.equal(true)
end)
it("errors on a missing ======= separator", function()
local malformed = "<<<<<<< SEARCH\nold\n>>>>>>> REPLACE"
local blocks, err = parse(malformed, false, {})
expect(blocks).to.equal(nil)
expect(contains(err, "missing ======= separator")).to.equal(true)
end)
it("errors on a missing >>>>>>> REPLACE marker", function()
local malformed = "<<<<<<< SEARCH\nold\n=======\nnew\n"
local blocks, err = parse(malformed, false, {})
expect(blocks).to.equal(nil)
expect(contains(err, "missing >>>>>>> REPLACE marker")).to.equal(true)
end)
end)
describe("compile_loop.parse_search_replace (multi-file)", function()
local parse = H.parse_search_replace
it("attaches the preceding path header to the block", function()
local text = "<<< path=foo.lua >>>\n" .. sr_block("x", "y")
local blocks, err = parse(text, true, { ["foo.lua"] = true })
expect(err).to.equal(nil)
expect(#blocks).to.equal(1)
expect(blocks[1].path).to.equal("foo.lua")
end)
it("errors when a path is not in the allowlist", function()
local text = "<<< path=evil.lua >>>\n" .. sr_block("x", "y")
local blocks, err = parse(text, true, { ["foo.lua"] = true })
expect(blocks).to.equal(nil)
expect(contains(err, "not in target_files allowlist")).to.equal(true)
end)
it("errors on a SEARCH block with no preceding path header", function()
local blocks, err = parse(sr_block("x", "y"), true, { ["foo.lua"] = true })
expect(blocks).to.equal(nil)
expect(contains(err, "missing path header for multi-file mode")).to.equal(true)
end)
end)
describe("compile_loop.ws_normalize", function()
local norm = H.ws_normalize
it("collapses internal whitespace runs to a single space", function()
expect(norm("a b\t c")).to.equal("a b c")
end)
it("strips leading and trailing whitespace", function()
expect(norm(" hi ")).to.equal("hi")
end)
end)
describe("compile_loop.apply_blocks", function()
local apply = H.apply_blocks
it("applies an exact-match block", function()
local content = "foo\nbar\nbaz"
local new, failed = apply(content, { { search = "bar", replace = "BAR" } })
expect(new).to.equal("foo\nBAR\nbaz")
expect(#failed).to.equal(0)
end)
it("records a failed index when the SEARCH text is absent", function()
local content = "foo\nbar"
local new, failed = apply(content, { { search = "missing", replace = "X" } })
expect(new).to.equal(content) expect(#failed).to.equal(1)
expect(failed[1]).to.equal(1)
end)
it("falls back to a whitespace-normalized match when exact fails", function()
local new, failed = apply("hello world", { { search = "hello world", replace = "ok" } })
expect(#failed).to.equal(0)
expect(new).to.equal("ok")
end)
it("applies multiple blocks in order, updating content between each", function()
local content = "one\ntwo\nthree"
local new, failed = apply(content, {
{ search = "one", replace = "1" },
{ search = "three", replace = "3" },
})
expect(#failed).to.equal(0)
expect(new).to.equal("1\ntwo\n3")
end)
end)
describe("compile_loop.extract_code", function()
local extract = H.extract_code
it("extracts a language-specific fenced block", function()
expect(extract("```lua\nprint('x')\n```", "lua")).to.equal("print('x')")
end)
it("falls back to any fence when the lang fence is absent", function()
expect(extract("```python\nx = 1\n```", "lua")).to.equal("x = 1")
end)
it("returns raw text when no fence is present", function()
expect(extract("no fences here", "lua")).to.equal("no fences here")
end)
end)
describe("compile_loop.make_summary", function()
local summary = H.make_summary
it("reports PASS with iter count on success", function()
expect(summary(true, 3, 10, nil)).to.equal("PASS in 3 iters")
end)
it("reports stagnation give-up", function()
local s = summary(false, 5, 10, "stagnation")
expect(contains(s, "give-up: stagnation at iter 5/10")).to.equal(true)
end)
it("reports max_iters give-up", function()
expect(summary(false, 10, 10, "max_iters")).to.equal("give-up: max_iters reached (10)")
end)
it("reports llm_call give-up", function()
expect(summary(false, 2, 10, "llm_call")).to.equal("give-up: llm_call failed at iter 2/10")
end)
it("reports an unknown reason verbatim", function()
expect(summary(false, 1, 10, "weird_reason")).to.equal("give-up: weird_reason")
end)
end)
describe("compile_loop.fnv1a_hash / compute_sr_hash", function()
local hash = H.fnv1a_hash
local sr_hash = H.compute_sr_hash
it("returns the FNV offset basis for the empty string", function()
expect(hash("")).to.equal("2166136261")
end)
it("is deterministic for identical inputs", function()
expect(hash("abc")).to.equal(hash("abc"))
end)
it("differs for different inputs", function()
expect(hash("abc") ~= hash("abd")).to.equal(true)
end)
it("compute_sr_hash is whitespace-insensitive", function()
expect(sr_hash("a b")).to.equal(sr_hash("a b"))
expect(sr_hash(" a b ")).to.equal(sr_hash("a b"))
end)
end)
describe("compile_loop.group_blocks_by_path", function()
local group = H.group_blocks_by_path
it("groups blocks by their path key", function()
local grouped = group({
{ path = "a.lua", search = "s1" },
{ path = "a.lua", search = "s2" },
{ path = "b.lua", search = "s3" },
})
expect(#grouped["a.lua"]).to.equal(2)
expect(#grouped["b.lua"]).to.equal(1)
end)
it("uses the boolean false key for nil-path blocks", function()
local grouped = group({ { path = nil, search = "s" } })
expect(#grouped[false]).to.equal(1)
end)
end)
describe("compile_loop.build_edit_failure_msg", function()
local build = H.build_edit_failure_msg
it("names the failing block, echoes its SEARCH, and asks for a re-emit", function()
local blocks = { { search = "target_text", replace = "r" } }
local msg = build({ 1 }, blocks, "current file body")
expect(contains(msg, "block 1")).to.equal(true)
expect(contains(msg, "target_text")).to.equal(true)
expect(contains(msg, "Current file content")).to.equal(true)
expect(contains(msg, "current file body")).to.equal(true)
expect(contains(msg, "Re-emit ALL blocks")).to.equal(true)
end)
end)
describe("compile_loop.build_multifile_edit_failure_msg", function()
local build = H.build_multifile_edit_failure_msg
it("scopes the failure message to the file path", function()
local all_failed = {
{ path = "mod.lua", indices = { 1 }, blocks = { { search = "needle" } } },
}
local msg = build(all_failed, { ["mod.lua"] = "file contents here" })
expect(contains(msg, "Edit FAILED in mod.lua")).to.equal(true)
expect(contains(msg, "needle")).to.equal(true)
expect(contains(msg, "file contents here")).to.equal(true)
end)
end)
describe("compile_loop.build_failure_msg", function()
local build = H.build_failure_msg
it("embeds lang fence hint, stdout, stderr and exit_code", function()
local msg = build("lua", { stdout = "out-text", stderr = "err-text", exit_code = 2 })
expect(contains(msg, "```lua")).to.equal(true)
expect(contains(msg, "out-text")).to.equal(true)
expect(contains(msg, "err-text")).to.equal(true)
expect(contains(msg, "2")).to.equal(true)
end)
end)
describe("compile_loop.filter_for_tool_output", function()
local filter = H.filter_for_tool_output
it("preserves the reporting fields", function()
local out = filter({
ok = true,
artifact_path = "/abs/x.lua",
modified_files = nil,
iters = 4,
summary = "PASS in 4 iters",
failure_reason = nil,
last_error = nil,
})
expect(out.ok).to.equal(true)
expect(out.artifact_path).to.equal("/abs/x.lua")
expect(out.iters).to.equal(4)
expect(out.summary).to.equal("PASS in 4 iters")
end)
it("strips code and history to prevent context contamination", function()
local out = filter({
ok = false,
iters = 1,
code = "print('leaked source')",
history = { { anything = true } },
})
expect(out.code).to.equal(nil)
expect(out.history).to.equal(nil)
end)
end)
describe("compile_loop.cl_oai_map_finish_reason", function()
local map = H.cl_oai_map_finish_reason
it("maps standard OpenAI finish reasons", function()
expect(map("stop")).to.equal("end_turn")
expect(map("tool_calls")).to.equal("tool_use")
expect(map("length")).to.equal("max_tokens")
end)
it("defaults nil to end_turn", function()
expect(map(nil)).to.equal("end_turn")
end)
end)