-- Run with `lua _selfcheck.lua` or `panto lua _selfcheck.lua`; no live terminal writes. local events, writes, env = {}, {}, {} local handle, inode, pid = "tty", 10, 100.0 local write_error, partial_limit os.getenv = function(name) return env[name] end package.loaded.panto = nil package.preload.panto = function() return { ext = { on = function(name, handler) events[name] = handler end } } end package.loaded.luv = nil package.preload.luv = function() return { guess_handle = function(fd) assert(fd == 1); return handle end, fs_fstat = function(fd) assert(fd == 1); return { dev = 1, ino = inode, rdev = 2 } end, os_getpid = function() return pid end, os_setenv = function(name, value) env[name] = value end, fs_write = function(fd, data, offset) assert(fd == 1 and offset == -1) if write_error then return nil, "EIO" end local chunk = data:sub(1, partial_limit or #data) writes[#writes + 1] = chunk return #chunk end, } end local entry = assert(loadfile("panto-report-status/init.lua"))() local function activate() events = {} entry.activate() end local function latest(state, message) local last for body in table.concat(writes):gmatch("\27%]7501;([^\27]*)\27\\") do last = body end assert(last, "missing report") assert(last == "state=" .. state .. ":app=panto" .. (message and ":msg=" .. message or ""), last) end local function unchanged(fn) local count = #writes fn() assert(#writes == count) end for _, kind in ipairs({ "pipe", "file", "unknown" }) do handle = kind activate() assert(next(events) == nil and #writes == 0) end handle = "tty" activate() assert(env.PANTO_REPORT_STATUS_OWNER == "1:10:2@100") local registered = 0 for _ in pairs(events) do registered = registered + 1 end assert(registered == 13) events.session_ready() latest("idle") unchanged(events.session_ready) unchanged(events.render_compaction) unchanged(events.background_work_end) unchanged(function() events.turn_end({ reason = "completed" }) end) for _, name in ipairs({ "turn_start", "agent_submission", "render_thinking" }) do events[name]() latest("working", "dGhpbmtpbmc=") -- thinking unchanged(events[name]) events.turn_end({ reason = "completed" }) latest("done") end for _, item in ipairs({ { "a", "dG9vbDph" }, { "ab", "dG9vbDphYg==" }, { "abc", "dG9vbDphYmM=" }, { "a\27\n\t\127\194\128", "dG9vbDphPz8/Pz8/" }, }) do events.render_tool_details({ tool_name = item[1] }) latest("working", item[2]) end events.render_assistant_text() latest("working", "cmVzcG9uZGluZw==") -- responding events.render_compaction() latest("working", "Y29tcGFjdGluZw==") -- compacting events.turn_end({ reason = "completed" }) latest("done") unchanged(events.render_compaction) for _, item in ipairs({ { "compact", "Y29tcGFjdGluZw==" }, { "!", "c2hlbGw=" }, { "!!", "c2hlbGw=" }, { "status", "Y21kOnN0YXR1cw==" }, }) do events.command_start({ command_name = item[1] }) latest("working", item[2]) events.command_end({ command_name = item[1] }) latest("done") end -- Foreground and background work must settle together, including failures. events.turn_start() events.background_work_start() events.background_work_start() events.turn_end({ reason = "failed" }) latest("working", "YmFja2dyb3VuZA==") -- background unchanged(events.background_work_end) events.background_work_end() latest("error") unchanged(events.background_work_end) events.turn_start() events.turn_end({ reason = "completed" }) latest("done") for _, reason in ipairs({ "interrupted", "exit_requested" }) do events.turn_start() events.turn_end({ reason = reason }) latest("idle") end events.background_work_start() latest("working", "YmFja2dyb3VuZA==") events.background_work_end() latest("done") events.background_work_start() events.turn_start() unchanged(events.background_work_end) latest("working", "dGhpbmtpbmc=") events.turn_end() latest("done") -- An output redirected after activation must remain clean. handle = "pipe" unchanged(events.turn_start) handle = "tty" events.turn_start() latest("working", "dGhpbmtpbmc=") -- A failed write must not mark the new state as successfully sent. write_error = true local ok, err = pcall(events.render_assistant_text) assert(not ok and tostring(err):find("panto-report-status:", 1, true)) write_error = nil events.render_assistant_text() latest("working", "cmVzcG9uZGluZw==") -- Synchronous writes may be short; finish the entire sequence in order. partial_limit = 3 events.turn_end() latest("done") partial_limit = nil events.session_end() latest("clear") unchanged(events.session_end) unchanged(function() events.turn_start(); events.background_work_start(); events.background_work_end() end) -- /new in the owning process reports; children in the same terminal do not. activate() events.session_ready() latest("idle") pid = 200.0 unchanged(activate) assert(next(events) == nil) -- A newly created terminal may report, even with an inherited owner marker. inode = 20 activate() assert(env.PANTO_REPORT_STATUS_OWNER == "1:20:2@200") events.session_ready() latest("idle") -- Changing terminals after activation cannot redirect an existing reporter. inode = 30 unchanged(events.turn_start) print("panto-report-status self-check: ok")