diff options
Diffstat (limited to 'spec/test_progress_replay.lua')
| -rw-r--r-- | spec/test_progress_replay.lua | 316 |
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 }, +} |
