-- 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. Load more
-- 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_HOME local 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 end end -- A condition the rest of the test cannot run without. local function must(cond, msg) if not cond then error(msg, 0) end end local function has(s, sub) return s:find(sub, 1, true) ~= nil end 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 end end local function words(s) local out = {} for w in s:gmatch("%S+") do out[#out + 1] = w end return out end 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 out end -- 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 out end 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 s end local function exists(path) return sys.stat(path) ~= nil end 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 out end -- 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 out end local function nonblank(lines) local out = {} for _, ln in ipairs(lines) do if trim(ln) ~= "" then out[#out + 1] = ln end end return out end 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()