summaryrefslogtreecommitdiff
path: root/spec/test_progress.lua
diff options
context:
space:
mode:
Diffstat (limited to 'spec/test_progress.lua')
-rw-r--r--spec/test_progress.lua322
1 files changed, 322 insertions, 0 deletions
diff --git a/spec/test_progress.lua b/spec/test_progress.lua
new file mode 100644
index 0000000..06509bf
--- /dev/null
+++ b/spec/test_progress.lua
@@ -0,0 +1,322 @@
+local progress = require("subagents.progress")
+
+local function board(id, collapsed)
+ local component
+ progress.claim({
+ id = id,
+ tool_name = "subagents.run",
+ collapsed = collapsed,
+ set_component = function(_, value)
+ component = value
+ return { invalidate = function() end, alive = function() return true end }
+ end,
+ })
+ progress.bind({ tool_call_id = id })
+ return component
+end
+
+local function plain(lines)
+ return table.concat(lines, "\n"):gsub("\27%[[%d;]*m", "")
+end
+
+return {
+ { "concurrent boards pin independently until their matching result", function()
+ progress.reset()
+ local pins = { a = {}, b = {} }
+ local function claim(id)
+ progress.claim({
+ id = id,
+ tool_name = "subagents.workflow",
+ set_component = function()
+ return {
+ alive = function() return true end,
+ invalidate = function() end,
+ set_pinned = function(_, value)
+ pins[id][#pins[id] + 1] = value
+ end,
+ }
+ end,
+ })
+ end
+ claim("a")
+ claim("b")
+ assert(pins.a[1] == true and pins.b[1] == true, "each invocation pins its own handle")
+ progress.settle({ id = "a" })
+ assert(pins.a[2] == false, "the matching result restores transcript order")
+ assert(#pins.b == 1, "another in-flight workflow remains pinned")
+ progress.settle({ id = "missing" })
+ assert(#pins.b == 1, "an unrelated or stale result is inert")
+ progress.reset()
+ assert(pins.b[2] == false, "reset safely releases unresolved boards")
+ end },
+
+ { "a pinned board leaves one blank line before the waiting indicator", function()
+ progress.reset()
+ local component, pins
+ progress.claim({
+ id = "spacing-call",
+ tool_name = "subagents.workflow",
+ collapsed = true,
+ set_component = function(_, value)
+ component = value
+ pins = {}
+ return {
+ alive = function() return true end,
+ invalidate = function() end,
+ set_pinned = function(_, value) pins[#pins + 1] = value end,
+ }
+ end,
+ })
+ progress.bind({ tool_call_id = "spacing-call" })
+ local card = progress.card("worker", "child")
+ card:event({ type = "block_start", block_type = "text", index = 1 })
+ card:event({ type = "content_delta", index = 1, delta = "final line" })
+
+ local compact = component:render(80)
+ assert(pins[1] == true, "the board pins before it renders")
+ assert(compact[#compact] == "", "pinned compact boards end with a blank line")
+ assert(compact[#compact - 1]:find("final line", 1, true), "the gap follows the final compact line")
+
+ progress.collapse({ collapsed = false })
+ local expanded = component:render(80)
+ assert(expanded[#expanded] == "", "pinned expanded boards end with a blank line")
+ assert(expanded[#expanded - 1]:find("final line", 1, true), "the gap follows the final expanded line")
+
+ progress.settle({ id = "spacing-call" })
+ local historical = component:render(80)
+ assert(historical[#historical] ~= "", "settled boards do not keep the spacing line")
+ assert(pins[2] == false, "settling unpins the board")
+ progress.reset()
+ end },
+
+ { "failed pin transitions do not change rendered spacing state", function()
+ progress.reset()
+ local component, pins, fail_unpin = nil, {}, true
+ progress.claim({
+ id = "spacing-lifecycle",
+ tool_name = "subagents.run",
+ set_component = function(_, value)
+ component = value
+ return {
+ alive = function() return true end,
+ invalidate = function() end,
+ set_pinned = function(_, value)
+ pins[#pins + 1] = value
+ if value == false and fail_unpin then error("cannot unpin") end
+ end,
+ }
+ end,
+ })
+ progress.bind({ tool_call_id = "spacing-lifecycle" })
+ local card = progress.card("worker", "child")
+ card:event({ type = "block_start", block_type = "text", index = 1 })
+ card:event({ type = "content_delta", index = 1, delta = "final line" })
+ local active = component:render(80)
+ assert(active[#active] == "", "a successful pin enables the gap")
+
+ progress.settle({ id = "spacing-lifecycle" })
+ local still_pinned = component:render(80)
+ assert(still_pinned[#still_pinned] == "", "a failed unpin keeps the board active")
+ fail_unpin = false
+ progress.settle({ id = "spacing-lifecycle" })
+ local unpinned = component:render(80)
+ assert(unpinned[#unpinned] ~= "", "a successful unpin removes the gap")
+ assert(#pins == 3 and pins[1] == true and pins[2] == false and pins[3] == false,
+ "unpin retries only after the failed lifecycle transition")
+ progress.reset()
+ end },
+
+ { "concurrent UUIDv7 cards across boards show distinguishable id prefixes", function()
+ progress.reset()
+ local first_board = board("call-1", true)
+ local first = "0198aaaa-aaaa-7aaa-8aaa-aaaaaaaaaaaa"
+ progress.card("worker", first)
+ local initial_line = first_board:render(120)[2]
+ assert(initial_line:find("0198aaaa…", 1, true), initial_line)
+
+ local second_board = board("call-2", true)
+ local second = "0198aaaa-aaaa-7aab-8aaa-aaaaaaaaaaaa"
+ progress.card("worker", second)
+
+ local first_line = first_board:render(120)[2]
+ local second_line = second_board:render(120)[2]
+ assert(first_line ~= second_line, "distinct session ids must not render identical card headers")
+ assert(first_line:find("0198aaaa%-aaaa%-7aaa"), first_line)
+ assert(second_line:find("0198aaaa%-aaaa%-7aab"), second_line)
+ progress.reset()
+ end },
+
+ { "Ctrl+O state expands and recollapses every existing board", function()
+ progress.reset()
+ local first = board("call-a", true)
+ local a = progress.card("alpha", "a")
+ for i = 1, 8 do
+ a:event({ type = "block_start", block_type = "text", index = i })
+ a:event({ type = "content_delta", index = i, delta = "line-" .. i .. "\n" })
+ a:event({ type = "block_complete", block_type = "text", index = i, text = "line-" .. i })
+ end
+ local second = board("call-b", true)
+ local b = progress.card("beta", "b")
+ for i = 1, 8 do b:event({ type = "content_delta", index = 99, delta = "ignored" }) end
+ for i = 1, 8 do
+ b:event({ type = "block_start", block_type = "text", index = i })
+ b:event({ type = "content_delta", index = i, delta = "beta-" .. i .. "\n" })
+ end
+
+ assert(#first:render(80) == 6, "blank + header + at most four recent lines")
+ assert(#second:render(80) == 6, "each board is independently collapsed")
+ progress.collapse({ collapsed = false })
+ assert(#first:render(80) == 10, "expanded board should retain all eight lines")
+ assert(#second:render(80) == 10, "global state should expand concurrent boards")
+ progress.collapse({ collapsed = true })
+ assert(#first:render(80) == 6 and #second:render(80) == 6)
+ progress.reset()
+ end },
+
+ { "settled historical boards keep responding to Ctrl+O until their handles die", function()
+ progress.reset()
+ local alive = { a = true, b = true }
+ local invalidations = { a = 0, b = 0 }
+ local components = {}
+ for _, id in ipairs({ "a", "b" }) do
+ progress.claim({
+ id = id,
+ tool_name = "subagents.run",
+ collapsed = true,
+ set_component = function(_, component)
+ components[id] = component
+ return {
+ alive = function() return alive[id] end,
+ invalidate = function() invalidations[id] = invalidations[id] + 1 end,
+ set_pinned = function() end,
+ }
+ end,
+ })
+ progress.bind({ tool_call_id = id })
+ local card = progress.card(id, id)
+ for i = 1, 6 do card:event({ type = "block_start", block_type = "text", index = i })
+ card:event({ type = "content_delta", index = i, delta = id .. i .. "\n" }) end
+ progress.settle({ id = id })
+ end
+
+ progress.reset() -- turn_end clears coroutine bindings, not historical components
+ progress.collapse({ collapsed = false })
+ assert(#components.a:render(80) == 8 and #components.b:render(80) == 8,
+ "settled concurrent boards should both expand after turn_end")
+ alive.a = false
+ local a_invalidations = invalidations.a
+ progress.collapse({ collapsed = true })
+ assert(invalidations.a == a_invalidations, "dead historical handles should be pruned")
+ assert(#components.b:render(80) == 6, "a live concurrent board should remain isolated")
+ alive.b = false
+ progress.reset()
+ end },
+
+ { "prompts are presentation-only in expanded mode and bounded", function()
+ progress.reset()
+ local component = board("call-prompts", true)
+ local card = progress.card("inline", "child", nil, {
+ prompt = "inspect this\nthen summarize",
+ system_prompt = "INLINE-SYSTEM " .. string.rep("x", 64 * 1024),
+ })
+ card:event({ type = "block_start", block_type = "text", index = 1 })
+ card:event({ type = "content_delta", index = 1, delta = "assistant line\n" })
+
+ local compact = plain(component:render(100))
+ assert(compact:find("assistant line", 1, true), compact)
+ assert(not compact:find("inspect this", 1, true), compact)
+ assert(not compact:find("INLINE-SYSTEM", 1, true), compact)
+
+ progress.collapse({ collapsed = false })
+ local expanded = plain(component:render(100))
+ assert(expanded:find("system prompt: INLINE-SYSTEM", 1, true), expanded)
+ assert(expanded:find("prompt: inspect this", 1, true), expanded)
+ assert(expanded:find("then summarize", 1, true), expanded)
+ assert(#card.system_prompt == 32 * 1024, "presentation metadata must be bounded")
+ assert(#card.history == 1, "presentation metadata must not enter accumulated output history")
+ progress.reset()
+ end },
+
+ { "expanded history includes streamed assistant text and available tool call fields", function()
+ progress.reset()
+ local component = board("call-tools", false)
+ local card = progress.card("worker", "child")
+ card:event({ type = "block_start", block_type = "text", index = 0 })
+ card:event({ type = "content_delta", index = 0, delta = "assistant output" })
+ card:event({ type = "block_complete", block_type = "text", index = 0, text = "assistant output" })
+ card:event({ type = "block_start", block_type = "tool_use", index = 1 })
+ card:event({ type = "tool_details", index = 1, id = "tool-7", name = "std.echo" })
+ card:event({ type = "block_complete", block_type = "tool_use", index = 1,
+ id = "tool-7", name = "std.echo", text = '{"text":"hello"}' })
+ card:event({ type = "tool_dispatch_result", tool_results = {
+ { tool_use_id = "tool-7", output = "hello\nworld", is_error = false },
+ { tool_use_id = "tool-8", output = "boom", is_error = true },
+ } })
+
+ local text = plain(component:render(80))
+ assert(text:find("assistant output", 1, true), text)
+ assert(text:find("std.echo [tool-7]", 1, true), text)
+ assert(text:find('input: {"text":"hello"}', 1, true), text)
+ assert(text:find("result [tool-7]: hello", 1, true), text)
+ assert(text:find(" world", 1, true), text)
+ assert(text:find("error [tool-8]: boom", 1, true), text)
+ progress.reset()
+ end },
+
+ { "tool names use internal dotted spelling in progress labels", function()
+ progress.reset()
+ local component = board("call-tool-names", false)
+ local card = progress.card("worker", "child")
+ card:event({ type = "tool_details", index = 1, id = "wire", name = "std__shell" })
+ card:event({ type = "tool_details", index = 2, id = "internal", name = "std.read" })
+ card:event({ type = "block_complete", block_type = "tool_use", index = 3,
+ id = "complete", name = "web__fetch" })
+
+ local text = plain(component:render(80))
+ assert(text:find("std.shell [wire]", 1, true), text)
+ assert(text:find("std.read [internal]", 1, true), text)
+ assert(text:find("web.fetch [complete]", 1, true), text)
+ assert(not text:find("std__shell", 1, true), text)
+ progress.reset()
+ end },
+
+ { "pathological single-line payloads are bounded before history ingestion", function()
+ progress.reset()
+ local component = board("call-huge", false)
+ local card = progress.card("worker", "huge")
+ local huge = "head\0" .. string.rep("x", 2 * 1024 * 1024)
+ card:event({ type = "block_start", block_type = "text", index = 1 })
+ card:event({ type = "content_delta", index = 1, delta = huge })
+ card:event({ type = "tool_dispatch_result", tool_results = {
+ { tool_use_id = "huge-tool", output = huge },
+ } })
+
+ assert(#card.history == 2)
+ assert(#card.history[1] <= 4096 and #card.history[2] <= 4096,
+ "assistant and tool lines must be bounded at ingestion")
+ local text = plain(component:render(120))
+ assert(text:find("head x", 1, true), "ordinary control sanitization should be preserved")
+ progress.reset()
+ end },
+
+ { "rendering sanitizes controls, stays width-bound, and caps retained history", function()
+ progress.reset()
+ local component = board("call-bounds", false)
+ local card = progress.card("bad\27label", "child")
+ for i = 1, 600 do
+ card:event({ type = "block_start", block_type = "text", index = i })
+ card:event({ type = "content_delta", index = i,
+ delta = string.format("history-%03d-abcdefghijklmnopqrstuvwxyz\n", i) })
+ end
+ local lines = component:render(16)
+ local text = plain(lines)
+ assert(not text:find("\27", 1, true), "control bytes must not reach the renderer")
+ assert(not text:find("history%-001"), "old history should be evicted")
+ assert(text:find("history%-600"), "recent history should remain")
+ assert(#lines <= 4098, "rendering itself must remain bounded")
+ for _, line in ipairs(lines) do
+ assert(utf8.len(line) <= 16, "line exceeded component width: " .. line)
+ end
+ progress.reset()
+ end },
+}