summaryrefslogtreecommitdiff
path: root/_selfcheck.lua
diff options
context:
space:
mode:
Diffstat (limited to '_selfcheck.lua')
-rw-r--r--_selfcheck.lua161
1 files changed, 161 insertions, 0 deletions
diff --git a/_selfcheck.lua b/_selfcheck.lua
new file mode 100644
index 0000000..67a84c7
--- /dev/null
+++ b/_selfcheck.lua
@@ -0,0 +1,161 @@
+-- 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")