1202 lines · 34.9 KB · Lua
RawDownload
-- SPDX-License-Identifier: ISC---- Deterministic TUI regression tests. They drive the real clm binary on a pty-- against the built-in mock server and assert on the rendered terminal grid.-- The binary under test comes from the CLM_BIN environment variable. local here = arg[0]:match("^(.*)/[^/]*$") or "."package.path = here .. "/?.lua;" .. package.path local sys = require("sys")local mock = require("mock")local driver = require("driver") driver.TEST_DIR = here:match("^(.*)/[^/]*$") or ".." local BIN = os.getenv("CLM_BIN") or "clm"local STATE_HOME = driver.STATE_HOMElocal SESSIONS = STATE_HOME .. "/clm" -- Blank columns the tui keeps between the transcript and each screen edge.local MARGIN = 2 local failures = {} local function check(cond, msg)	print((cond and "ok  " or "FAIL") .. "  " .. msg)	if not cond then		failures[#failures + 1] = msg	endend -- A condition the rest of the test cannot run without.local function must(cond, msg)	if not cond then		error(msg, 0)	endend local function has(s, sub)	return s:find(sub, 1, true) ~= nilend local function at(s, sub)	return s:find(sub, 1, true)end local function count(s, sub)	local n = 0	local i = 1 	while true do		local a = s:find(sub, i, true)		if a == nil then			return n		end		n = n + 1		i = a + #sub	endend local function words(s)	local out = {} 	for w in s:gmatch("%S+") do		out[#out + 1] = w	end	return outend local function trim(s)	return (s:gsub("^%s+", ""):gsub("%s+$", ""))end -- Transcript lines with the left margin removed.local function body(t)	local out = {} 	for i, ln in ipairs(t:lines()) do		out[i] = ln:sub(1, MARGIN) == string.rep(" ", MARGIN) and		    ln:sub(MARGIN + 1) or ln	end	return outend -- Every line but the status bar and the input box.local function transcript_lines(t)	local l = t:lines()	local out = {} 	for i = 1, #l - 2 do		out[i] = l[i]	end	return outend local function transcript(t)	return table.concat(transcript_lines(t), "\n")end -- The status bar: the lowest line naming the binary and its state.local function status_line(t)	local l = t:lines() 	for i = #l, 1, -1 do		if has(l[i], " clm ") and has(l[i], "[") then			return l[i]		end	end	return ""end local function last_line(t)	local l = t:lines() 	return l[#l]end local function read_file(path)	local f = io.open(path, "r") 	if f == nil then		return nil	end	local s = f:read("a")	f:close()	return send local function exists(path)	return sys.stat(path) ~= nilend local function jsonl_set(dir)	local out = {} 	for _, f in ipairs(sys.listdir(dir)) do		if f:sub(-6) == ".jsonl" then			out[f] = true		end	end	return outend -- The .jsonl session logs that appeared since `before` was taken.local function new_sessions(before)	local out = {} 	for f in pairs(jsonl_set(SESSIONS)) do		if not before[f] then			out[#out + 1] = f		end	end	table.sort(out)	return outend local function nonblank(lines)	local out = {} 	for _, ln in ipairs(lines) do		if trim(ln) ~= "" then			out[#out + 1] = ln		end	end	return outend local tests = {} tests.connection = function(url)	local t = driver.new(BIN, url, { rows = 12, cols = 60 }) 	check(t:wait_for("[online]", 10),	    "connection: status bar shows [online] against a live server")	t:close()end tests.offline = function()	-- Point at a port nothing is listening on; expect an offline marker.	local t = driver.new(BIN, "http://127.0.0.1:1/v1/chat/completions",	    { rows = 12, cols = 60 }) 	check(t:wait_for("offline", 10),	    "connection: status bar shows offline when unreachable")	t:close()end tests.agent = function(url)	local t = driver.new(BIN, url, { rows = 12, cols = 60 }) 	if not t:wait_for("[online]", 10) then		t:pump(1.0)	end	-- test/config/clm/config.lua resolves agent "test" to the model	-- "mock/mock-model".	check(has(t:text(), "[mock/mock-model:test]"),	    "agent: status bar shows provider/model:agent from config")	t:close()end tests.markdown = function(url)	local t = driver.new(BIN, url, { rows = 24, cols = 70 }) 	t:send("show me fruit\r")	-- "Yellow" is the last table cell, so waiting for it means the whole	-- streamed reply has arrived.	must(t:wait_for("Yellow", 15), "no response rendered")	local txt = t:text()	check(has(txt, "Fruit"), "markdown: heading text present")	check(has(txt, "Apple") and has(txt, "Banana"),	    "markdown: list/table items")	check(has(txt, "│") or has(txt, "─"),	    "markdown: table drawn with box characters")	check(t:any_bold("bold"), "markdown: **bold** rendered bold")	t:close()end tests.scrollback = function(url)	-- A short terminal, so the canned reply overflows and can be paged.	local t = driver.new(BIN, url, { rows = 10, cols = 40 }) 	t:send("show me fruit\r")	must(t:wait_for("Colour", 15) or t:wait_for("Fruit", 5),	    "no response to scroll")	local bottom = t:text()	t:send(driver.PGUP)	t:send(driver.PGUP)	t:pump(0.4)	local scrolled = t:text()	check(scrolled ~= bottom, "scrollback: PgUp changes the viewport")	check(has(scrolled, "Fruit"),	    "scrollback: PgUp reveals earlier content")	t:send(driver.PGDN)	t:send(driver.PGDN)	t:send(driver.PGDN)	t:pump(0.4)	check(t:text() ~= scrolled, "scrollback: PgDn returns toward bottom")	t:close()end tests.scroll_stable = function(url)	-- Scroll is stored as a distance from the bottom, so a growing	-- transcript must not drag the viewport forward under a reader.	-- Only the transcript rows are compared: the status bar's spinner	-- animates independent of scroll.	local t = driver.new(BIN, url, { rows = 10, cols = 40 }) 	t:send("show me fruit\r")	must(t:wait_for("Colour", 15), "no first response")	t:send(driver.PGUP)	t:send(driver.PGUP)	t:pump(0.4)	local scrolled = transcript(t)	check(has(scrolled, "Fruit"), "scroll-stable: scrolled up into history") 	-- Submit a second prompt while scrolled up and sample the viewport	-- partway through its reply.	t:send("slowtest show me fruit again\r")	t:pump(0.3)	check(transcript(t) == scrolled,	    "scroll-stable: viewport unchanged partway through a new " ..	    "streamed reply while scrolled up") 	t:pump(1.5)	check(transcript(t) == scrolled,	    "scroll-stable: viewport still unchanged once the new reply " ..	    "finishes streaming") 	-- Following the bottom must still work once the user scrolls back.	t:send(driver.PGDN)	t:send(driver.PGDN)	t:send(driver.PGDN)	t:pump(0.4)	check(transcript(t) ~= scrolled,	    "scroll-stable: PgDn still returns toward the bottom")	t:close()end tests.end_key = function(url)	-- End on an empty input line jumps the transcript to the bottom; End	-- while editing a line still only moves the cursor.	local t = driver.new(BIN, url, { rows = 10, cols = 40 }) 	t:send("show me fruit\r")	must(t:wait_for("Colour", 15), "no first response")	t:send(driver.PGUP)	t:send(driver.PGUP)	t:pump(0.4)	local before = transcript(t)	check(has(before, "Fruit"), "end: scrolled up into history") 	t:send(driver.END) -- input is empty here	t:pump(0.4)	local after = transcript(t)	check(after ~= before,	    "end: jumps the transcript to the bottom from an empty input")	check(has(after, "Colour"),	    "end: bottom of the transcript is now visible") 	t:send(driver.PGUP)	t:send(driver.PGUP)	t:pump(0.4)	t:send("hello")	t:pump(0.2)	local typed = transcript(t)	t:send(driver.END)	t:pump(0.3)	check(transcript(t) == typed,	    "end: with text typed, only moves the input cursor, " ..	    "doesn't scroll")	t:close()end tests.resize = function(url)	local t = driver.new(BIN, url, { rows = 24, cols = 70 }) 	t:send("show me fruit\r")	must(t:wait_for("Apple", 15), "no response before resize")	t:resize(20, 40)	check(has(t:text(), "Apple"), "resize: content survives narrow reflow")	t:resize(28, 100)	check(has(t:text(), "Apple"), "resize: content survives wide reflow")	t:close()end tests.margin = function(url)	local cols = 60	local t = driver.new(BIN, url, { rows = 24, cols = cols }) 	t:send("show me fruit\r")	must(t:wait_for("Apple", 15), "no response to measure")	local rows = nonblank(transcript_lines(t))	local all_indented = #rows > 0	local widest = 0	for _, ln in ipairs(rows) do		if ln:sub(1, MARGIN) ~= string.rep(" ", MARGIN) then			all_indented = false		end		local w = utf8.len(ln) or #ln		if w > widest then			widest = w		end	end	check(all_indented,	    "margin: no transcript line starts at the left edge")	check(widest > cols - 2 * MARGIN - 8,	    "margin: some line is long enough to reach the right edge")	check(widest <= cols - MARGIN,	    "margin: no transcript line runs into the right edge")	t:close() 	-- The gutters belong to no window. Whatever a full-width window drew	-- there must not survive it moving or shrinking.	t = driver.new(BIN, url, { rows = 14, cols = cols })	t:wait_for("online", 8)	t:send("show me fruit\r")	must(t:wait_for("Apple", 15), "no response before resize")	t:send(string.rep("x", 200)) -- grow the input box	t:pump(0.6)	t:send(driver.CTRL_U) -- shrink it back, vacating those rows	t:pump(0.6)	local left_clear = true	local right_clear = true	local lines = t:lines()	for i = 1, #lines - 2 do		if trim(lines[i]:sub(1, MARGIN)) ~= "" then			left_clear = false		end		if (utf8.len(lines[i]) or #lines[i]) > cols - MARGIN then			right_clear = false		end	end	check(left_clear, "margin: the left gutter is clear after the box shrinks")	check(right_clear,	    "margin: the right gutter is clear after the box shrinks")	t:close() 	-- A screen too narrow to spare the columns drops the margin rather	-- than squeezing the text.	t = driver.new(BIN, url, { rows = 10, cols = 7 })	t:send("show me fruit\r")	t:pump(3.0)	rows = nonblank(transcript_lines(t))	local any_flush = false	for _, ln in ipairs(rows) do		if trim(ln:sub(1, 1)) ~= "" then			any_flush = true		end	end	check(#rows > 0 and any_flush,	    "margin: dropped when the screen cannot spare it")	t:close()end tests.editing = function(url)	local t = driver.new(BIN, url, { rows = 12, cols = 60 }) 	t:send("hello world")	t:pump(0.3)	check(has(t:text(), "hello world"), "edit: typed text shows in input")	t:send(driver.CTRL_A) -- cursor to start	t:send(driver.CTRL_K) -- kill to end (the whole line)	t:pump(0.3)	local bare = trim(last_line(t))	check(bare == ">" or bare == "",	    "edit: ^A then ^K clears the input line")	t:send(driver.CTRL_Y) -- yank it back	t:pump(0.3)	check(has(t:text(), "hello world"),	    "edit: ^Y yanks the killed text back")	t:send(driver.CTRL_U) -- kill to start, clearing it again	t:pump(0.3)	check(not has(last_line(t), "hello world"),	    "edit: ^U clears the restored line")	t:close()end tests.history = function(url)	local t = driver.new(BIN, url, { rows = 12, cols = 60 }) 	t:wait_for("online", 8)	t:send("alpha one\r")	t:pump(0.3)	t:send("bravo two\r")	t:pump(0.3)	t:send(driver.UP) -- most recent	t:pump(0.3)	check(has(last_line(t), "bravo two"),	    "history: Up recalls the most recent prompt")	t:send(driver.UP) -- older	t:pump(0.3)	check(has(last_line(t), "alpha one"),	    "history: Up again recalls the older prompt")	t:send(driver.DOWN)	t:pump(0.3)	check(has(last_line(t), "bravo two"),	    "history: Down returns toward the newer prompt")	t:close()end tests.commands = function(url)	local t = driver.new(BIN, url, { rows = 20, cols = 80 }) 	t:wait_for("online", 8)	t:send("/help\r")	t:pump(0.4)	check(has(t:text(), "/clear") and has(t:text(), "/reasoning"),	    "commands: /help lists commands")	t:send("show me fruit\r")	must(t:wait_for("Yellow", 15), "no reply before clear")	t:pump(1.0) -- let the stream finish so nothing re-appends after clear	t:send("/clear\r")	t:pump(0.5)	check(not has(t:text(), "Apple"),	    "commands: /clear wipes the transcript")	t:close()end tests.effort = function(url)	local t = driver.new(BIN, url, { rows = 20, cols = 70 }) 	t:wait_for("online", 8)	t:send("/effort\r")	t:pump(0.4)	check(has(t:text(), "effort:"),	    "effort: bare command reports the level")	t:send("/effort low\r")	t:pump(0.4)	check(has(t:text(), "effort: low"), "effort: level accepted")	t:send("/effort \t")	t:pump(0.5)	local txt = t:text()	check(has(txt, "xhigh") and has(txt, "medium"),	    "effort: TAB lists the levels")	t:send(driver.CTRL_U)	t:pump(0.2)	t:send("/effo\t")	t:pump(0.5)	check(has(t:text(), "/effort"), "effort: command name completes")	t:send(driver.CTRL_U)	t:close()end tests.queueing = function(url)	local t = driver.new(BIN, url, { rows = 20, cols = 70 }) 	t:wait_for("online", 8)	-- The first prompt starts a slow turn; the second queues mid-turn.	t:send("slowtest first\r")	t:pump(0.15)	t:send("second\r")	check(t:wait_for("(queued)", 3),	    "queueing: a prompt sent while busy shows the queue marker")	t:pump(6.0)	check(not has(t:text(), "turn already in progress"),	    "queueing: no in-progress rejection reaches the transcript")	t:send("/quit\r")	t:pump(0.8)	t:close() 

Lines 1–500 of 1202.