summaryrefslogtreecommitdiff
path: root/spec/test_progress_replay.lua
diff options
context:
space:
mode:
Diffstat (limited to 'spec/test_progress_replay.lua')
-rw-r--r--spec/test_progress_replay.lua316
1 files changed, 316 insertions, 0 deletions
diff --git a/spec/test_progress_replay.lua b/spec/test_progress_replay.lua
new file mode 100644
index 0000000..aba8e33
--- /dev/null
+++ b/spec/test_progress_replay.lua
@@ -0,0 +1,316 @@
+local fake = require("spec.fake_ext")
+local progress = require("subagents.progress")
+local run = require("subagents.run")
+local luatool = require("subagents.luatool")
+local toml_workflows = require("subagents.toml_workflows")
+
+local function plain(lines)
+ return table.concat(lines, "\n"):gsub("\27%[[%d;]*m", "")
+end
+
+local function has(text, needle)
+ assert(text:find(needle, 1, true), "expected to find " .. needle .. " in:\n" .. text)
+end
+
+local function profile_set()
+ local alpha = { name = "alpha", description = "alpha", body = "ALPHA\n" }
+ local beta = { name = "beta", description = "beta", body = "BETA\n" }
+ return {
+ list = { alpha, beta },
+ by_name = { alpha = alpha, beta = beta },
+ warnings = {},
+ }
+end
+
+local function message(role, text, metadata)
+ return { role = role, text = text, metadata = metadata }
+end
+
+local function tool_use(id, name, input)
+ return {
+ type = "tool_use", id = id, name = name, input = input,
+ }
+end
+
+local function tool_result(id, output, is_error)
+ return {
+ type = "tool_result", tool_use_id = id, is_error = is_error == true,
+ parts = { { text = output } },
+ }
+end
+
+local function claim(handle, id, tool_name)
+ local component
+ local alive = true
+ progress.claim({
+ id = id,
+ tool_name = tool_name or "subagents.run",
+ collapsed = true,
+ set_component = function(_, value)
+ component = value
+ return {
+ render = value.render,
+ invalidate = function() end,
+ alive = function() return alive end,
+ set_pinned = function() end,
+ }
+ end,
+ })
+ assert(component ~= nil, "the production claim path installed a component")
+ return component, function() alive = false end
+end
+
+local function with_host(fn, opts)
+ local handle = fake.install(opts)
+ local ok, err = pcall(fn, handle)
+ progress.reset()
+ handle.restore()
+ if not ok then error(err, 0) end
+end
+
+return {
+ { "replay reconstructs a direct child turn, tools, result, model, and status", function()
+ with_host(function(handle)
+ handle.add_session("child-direct", {
+ message("system", "You are a subagent."),
+ message("system", "Discovered profile.", {
+ subagents = { owner = "0198-primary", agent = "reviewer" },
+ }),
+ message("user", "inspect the change", {
+ subagents = { tool_call_id = "outer-direct", model = "openai:test", reasoning = "high" },
+ }),
+ { role = "assistant", blocks = {
+ { type = "text", text = "checking" },
+ tool_use("tool-1", "std__read", '{"path":"auth.lua"}'),
+ } },
+ { role = "user", blocks = { tool_result("tool-1", "file contents") } },
+ { role = "assistant", blocks = { { type = "text", text = "found the issue" } } },
+ })
+ local component, kill = claim(handle, "outer-direct")
+ progress.collapse({ collapsed = false })
+ local text = plain(component:render(120))
+ has(text, "✔ reviewer child-direct")
+ has(text, "completed")
+ has(text, "↳ openai:test")
+ has(text, "prompt: inspect the change")
+ has(text, "checking")
+ has(text, "std.read [tool-1]")
+ has(text, 'input: {"path":"auth.lua"}')
+ has(text, "result [tool-1]: file contents")
+ has(text, "found the issue")
+ kill()
+ end)
+ end },
+
+ { "replay isolates dynamic and fixed workflow cards and only inline manifests show system prompts", function()
+ with_host(function(handle)
+ handle.add_session("child-dynamic", {
+ message("system", "You are a subagent."),
+ message("system", "INLINE-WORKFLOW-SYSTEM", {
+ subagents = { owner = "0198-primary", agent = "inline", inline = true },
+ }),
+ message("user", "dynamic step", {
+ subagents = { tool_call_id = "outer-dynamic", model = "fixture:one" },
+ }),
+ message("assistant", "dynamic answer"),
+ })
+ handle.add_session("child-fixed", {
+ message("system", "You are a subagent."),
+ message("system", "DISCOVERED-SYSTEM", {
+ subagents = { owner = "0198-primary", agent = "alpha" },
+ }),
+ message("user", "fixed step", {
+ subagents = { tool_call_id = "outer-fixed", model = "fixture:two" },
+ }),
+ message("assistant", "fixed answer"),
+ })
+
+ local dynamic, kill_dynamic = claim(handle, "outer-dynamic", "subagents.lua")
+ local fixed, kill_fixed = claim(handle, "outer-fixed", "subagents.workflow")
+ progress.collapse({ collapsed = false })
+ local dynamic_text = plain(dynamic:render(120))
+ local fixed_text = plain(fixed:render(120))
+ has(dynamic_text, "✔ inline child-dynamic")
+ has(dynamic_text, "system prompt: INLINE-WORKFLOW-SYSTEM")
+ has(dynamic_text, "prompt: dynamic step")
+ has(dynamic_text, "dynamic answer")
+ assert(not dynamic_text:find("fixed answer", 1, true), dynamic_text)
+ has(fixed_text, "✔ alpha child-fixed")
+ has(fixed_text, "prompt: fixed step")
+ has(fixed_text, "fixed answer")
+ assert(not fixed_text:find("system prompt:", 1, true), fixed_text)
+ kill_dynamic()
+ kill_fixed()
+ end)
+ end },
+
+ { "one resumed child can contribute separate turns to separate outer calls", function()
+ with_host(function(handle)
+ handle.add_session("child-shared", {
+ message("system", "role"),
+ message("system", "Alpha", { subagents = { owner = "0198-primary", agent = "alpha" } }),
+ message("user", "first turn", { subagents = { tool_call_id = "outer-a", model = "model-a" } }),
+ message("assistant", "first answer"),
+ message("user", "second turn", { subagents = { tool_call_id = "outer-b", model = "model-b" } }),
+ message("assistant", "second answer"),
+ message("user", "third turn", { subagents = { tool_call_id = "outer-a", model = "model-c" } }),
+ message("assistant", "third answer"),
+ })
+
+ local a, kill_a = claim(handle, "outer-a")
+ local b, kill_b = claim(handle, "outer-b")
+ progress.collapse({ collapsed = false })
+ local a_text = plain(a:render(120))
+ local b_text = plain(b:render(120))
+ has(a_text, "first answer")
+ has(a_text, "third answer")
+ assert(not a_text:find("second answer", 1, true), a_text)
+ has(b_text, "second answer")
+ assert(not b_text:find("first answer", 1, true), b_text)
+ assert(not b_text:find("third answer", 1, true), b_text)
+ local _, shared_cards = a_text:gsub("child%-shared", "")
+ assert(shared_cards == 2, "both turns owned by outer-a are separate cards:\n" .. a_text)
+ kill_a()
+ kill_b()
+ end)
+ end },
+
+ { "replay preserves interleaved spawn order across child sessions", function()
+ with_host(function(handle)
+ handle.add_session("child-a", {
+ message("system", "role"),
+ message("system", "Alpha", { subagents = { owner = "0198-primary", agent = "alpha" } }),
+ message("user", "A1", { subagents = { tool_call_id = "outer-order", sequence = 1, model = "m" } }),
+ message("assistant", "answer A1"),
+ message("user", "A2", { subagents = { tool_call_id = "outer-order", sequence = 3, model = "m" } }),
+ message("assistant", "answer A2"),
+ })
+ handle.add_session("child-b", {
+ message("system", "role"),
+ message("system", "Beta", { subagents = { owner = "0198-primary", agent = "beta" } }),
+ message("user", "B1", { subagents = { tool_call_id = "outer-order", sequence = 2, model = "m" } }),
+ message("assistant", "answer B1"),
+ })
+ local component, kill = claim(handle, "outer-order")
+ progress.collapse({ collapsed = false })
+ local text = plain(component:render(120))
+ local a1 = assert(text:find("answer A1", 1, true))
+ local b1 = assert(text:find("answer B1", 1, true))
+ local a2 = assert(text:find("answer A2", 1, true))
+ assert(a1 < b1 and b1 < a2, text)
+ kill()
+ end)
+ end },
+
+ { "replay keeps cancelled and non-text completed turns terminal", function()
+ with_host(function(handle)
+ handle.add_session("child-cancel", {
+ message("system", "role"),
+ message("system", "Alpha", { subagents = { owner = "0198-primary", agent = "alpha" } }),
+ message("user", "cancelled prompt", { subagents = {
+ tool_call_id = "outer-status", sequence = 1, status = "cancelled",
+ } }),
+ })
+ handle.add_session("child-thinking", {
+ message("system", "role"),
+ message("system", "Beta", { subagents = { owner = "0198-primary", agent = "beta" } }),
+ message("user", "thinking prompt", { subagents = {
+ tool_call_id = "outer-status", sequence = 2, status = "completed",
+ } }),
+ { role = "assistant", blocks = { { type = "thinking", text = "private reasoning" } } },
+ })
+ local component, kill = claim(handle, "outer-status")
+ progress.collapse({ collapsed = false })
+ local text = plain(component:render(120))
+ has(text, "⊘ alpha child-cancel")
+ has(text, "✔ beta child-thinking")
+ assert(not text:find("✖", 1, true), text)
+ kill()
+ end)
+ end },
+
+ { "replay finds a manifest after long primary system context through the bounded API", function()
+ with_host(function(handle)
+ handle.reject_unbounded_replay = true
+ local messages = {}
+ for index = 1, 80 do messages[#messages + 1] = message("system", "context " .. index) end
+ messages[#messages + 1] = message("system", "INLINE-LONG", {
+ subagents = { owner = "0198-primary", agent = "long", inline = true },
+ })
+ messages[#messages + 1] = message("user", "long prompt", {
+ subagents = { tool_call_id = "outer-long", sequence = 1 },
+ })
+ messages[#messages + 1] = message("assistant", "long answer")
+ handle.add_session("child-long", messages)
+ local component, kill = claim(handle, "outer-long")
+ progress.collapse({ collapsed = false })
+ local text = plain(component:render(120))
+ has(text, "✔ long child-long")
+ has(text, "system prompt: INLINE-LONG")
+ assert(handle.bounded_list_calls == 1, "replay must use the bounded catalog API")
+ assert(handle.bounded_load_calls == 2, "replay must use bounded tail + manifest reads")
+ kill()
+ end)
+ end },
+
+ { "durable turns carry the owning outer call and inline manifests are marked", function()
+ with_host(function(handle)
+ local profiles = profile_set()
+ local component, kill = claim(handle, "outer-run")
+ progress.bind({ tool_call_id = "outer-run" })
+ handle.queue_for("alpha", { output = "done" })
+ run.handle({ agent = "alpha", prompt = "direct" }, profiles)
+ assert(handle.runs[1].metadata.subagents.tool_call_id == "outer-run",
+ "direct child metadata names its outer tool call")
+ assert(handle.runs[1].metadata.subagents.sequence == 1,
+ "direct child metadata records its card sequence")
+ assert(handle.runs[1].metadata.subagents.status == "completed",
+ "settled status is retained for durable presentation")
+
+ handle.queue_for("alpha", { output = "fixed done" })
+ toml_workflows.handle({
+ prompt = "fixed",
+ steps = { { id = "step", agent = "alpha", prompt = "run fixed" } },
+ }, profiles)
+ assert(handle.runs[2].metadata.subagents.tool_call_id == "outer-run",
+ "fixed workflow child keeps the same outer owner")
+ assert(handle.runs[2].metadata.subagents.sequence == 2,
+ "fixed workflow child follows spawn order")
+
+ handle.queue_for("inline", { output = "inline done" })
+ local source = [[return subagents.workflow(function(ctx, input)
+ return ctx:agent({ agent = "inline", prompt = input }):await()
+ end)]]
+ luatool.handle({
+ prompt = "dynamic",
+ source = source,
+ agents = { { name = "inline", system_prompt = "INLINE" } },
+ }, profiles)
+ local child = handle.spawns[3]
+ local manifest = child.system_messages[2].metadata.subagents
+ assert(manifest.inline == true, "inline workflow manifest is explicitly marked")
+ assert(handle.runs[3].metadata.subagents.tool_call_id == "outer-run",
+ "inline workflow child keeps the same outer owner")
+ assert(handle.runs[3].metadata.subagents.sequence == 3,
+ "inline workflow child follows spawn order")
+ assert(handle.runs[3].metadata.subagents.status == "completed",
+ "inline completion is durably annotated")
+ kill()
+ progress.reset()
+ _ = component
+ end)
+ end },
+
+ { "missing and malformed child logs degrade to an empty or partial board", function()
+ with_host(function(handle)
+ handle.add_session("malformed", "not a message array")
+ handle.add_session("valid", {
+ message("system", "role"),
+ message("user", "no ownership", { subagents = { model = "model" } }),
+ })
+ local component, kill = claim(handle, "outer-missing")
+ assert(#component:render(100) == 0, "unowned or malformed logs do not invent cards")
+ kill()
+ end, { sessions = {} })
+ end },
+}