-- subagents/toml_workflows.lua: validation, dependency prompt assembly, branch -- failure, terminal ordering, discovery, and the generated commands. -- -- Validation and execution run off definitions built in Lua, so they exercise -- the DAG rules without needing the TOML rock. The parse, discovery, and command -- cases do need toml2lua (and luv, for the recursive walk) and skip without them. -- -- Discovery is pointed at temporary directories by replacing paths.config_roots -- for the duration of the case: the two layers, in the same order the extension -- uses, without reading the machine's real ~/.config. local fake = require("spec.fake_ext") local paths = require("subagents.paths") local toml_workflows = require("subagents.toml_workflows") local function has(text, needle) assert(type(text) == "string", "expected a string, got " .. type(text)) assert(text:find(needle, 1, true), "expected to find " .. needle .. " in:\n" .. tostring(text)) end local function profile_set() local set = { list = {}, by_name = {}, warnings = {} } for _, name in ipairs({ "alpha", "beta", "gamma" }) do local profile = { name = name, description = name, body = "You are " .. name .. ".\n" } set.list[#set.list + 1] = profile set.by_name[name] = profile end return set end local function with_host(fn) local handle = fake.install() local ok, err = pcall(fn, handle, profile_set()) handle.restore() if not ok then error(err, 0) end end local function write(path, text) assert(os.execute("mkdir -p " .. (path:match("^(.*)/[^/]+$") or "."))) local file = assert(io.open(path, "w")) file:write(text) file:close() end -- Point discovery at two temporary layers for the duration of `fn`. local function with_layers(files, fn) local ok_uv, uv = pcall(require, "luv") if not ok_uv then return "skip", "luv is not installed" end if not pcall(require, "toml") then return "skip", "toml2lua is not installed" end local tmp = assert(uv.fs_mkdtemp("/tmp/panto-subagents-workflows-XXXXXX")) for name, text in pairs(files) do write(tmp .. "/" .. name, text) end local original = paths.config_roots paths.config_roots = function(kind) return { tmp .. "/user/" .. kind, tmp .. "/project/" .. kind } end local ok, err = pcall(fn, tmp) paths.config_roots = original os.execute("rm -rf " .. tmp) if not ok then error(err, 0) end end local BRANCH_DEF = { name = "branch", steps = { { id = "a", agent = "alpha", prompt = "Inspect." }, { id = "b", agent = "beta", prompt = "Summarize.", needs = { "a" } }, { id = "c", agent = "gamma", prompt = "Unrelated." }, }, } return { { "a valid definition normalizes and marks its terminal steps", function() local def = assert(toml_workflows.validate(BRANCH_DEF, "fallback")) assert(def.name == "branch") assert(#def.steps == 3) assert(def.terminal.b and def.terminal.c, "b and c are nobody's dependency") assert(def.terminal.a == nil, "a is depended on, so it is not terminal") assert(def.by_id.b.needs[1] == "a") end }, { "the name falls back to the file stem", function() local def = assert(toml_workflows.validate({ steps = BRANCH_DEF.steps }, "from-stem")) assert(def.name == "from-stem", tostring(def.name)) end }, { "structural mistakes are refused with a readable reason", function() local function bad(def, needle) local ok, err = toml_workflows.validate(def, "w") assert(ok == nil, "expected a rejection for " .. needle) has(err, needle) end bad({ steps = {} }, "has no `steps` array") bad({ steps = "nope" }, "has no `steps` array") bad({ steps = { { id = "a", agent = "alpha" } } }, "`prompt` is required") bad({ steps = { { id = "a", prompt = "p" } } }, "`agent` is required") bad({ steps = { { agent = "alpha", prompt = "p" } } }, "`id` is required") bad({ steps = { { id = "a", agent = "alpha", prompt = "p" }, { id = "a", agent = "beta", prompt = "q" }, } }, "duplicate step id 'a'") bad({ steps = { { id = "a", agent = "alpha", prompt = "p", needs = { "ghost" } }, } }, "unknown dependency 'ghost'") bad({ steps = { { id = "a", agent = "alpha", prompt = "p", needs = { "b" } }, { id = "b", agent = "beta", prompt = "q", needs = { "a" } }, } }, "dependency cycle") end }, { "a step's prompt carries the input and each dependency in needs order", function() local step = { id = "b", agent = "beta", prompt = "Summarize.", needs = { "a", "z" } } local settled = { a = { status = "completed", output = "A output" }, z = { status = "failed", error = "boom" }, } local prompt = toml_workflows.step_prompt(step, "the workflow input", settled) assert(prompt == table.concat({ "Summarize.", "", "## Workflow input", "", "the workflow input", "", "## Output of a", "", "A output", "", "## Output of z", "", "[failed: boom]", }, "\n"), string.format("unexpected prompt:\n%s", prompt)) end }, { "a dependent receives its dependency's output and runs after it", function() with_host(function(handle, profiles) local def = assert(toml_workflows.validate({ name = "chain", steps = { { id = "a", agent = "alpha", prompt = "Inspect." }, { id = "b", agent = "beta", prompt = "Summarize.", needs = { "a" } }, }, }, "chain")) handle.queue_for("alpha", { output = "A output" }) handle.queue_for("beta", { output = "B output" }) local results = toml_workflows.run(def, "the input", profiles) assert(#handle.spawns == 2, "both steps ran") assert(handle.spawns[1].label == "alpha", "the root starts first") has(handle.spawns[2].prompt, "## Workflow input\n\nthe input") has(handle.spawns[2].prompt, "## Output of a\n\nA output") assert(#results == 1 and results[1].id == "b", "only the terminal step is returned") assert(results[1].output == "B output") end) end }, { "a failed step skips its dependents while other branches finish", function() with_host(function(handle, profiles) local def = assert(toml_workflows.validate(BRANCH_DEF, "branch")) handle.queue_for("alpha", { status = "failed", error = "alpha died", settle = 1 }) handle.queue_for("gamma", { output = "gamma output", settle = 2 }) local results = toml_workflows.run(def, "input", profiles) assert(#handle.spawns == 2, "the skipped step never spawns") for _, spec in ipairs(handle.spawns) do assert(spec.label ~= "beta", "beta depends on a failure and must not run") end assert(#results == 2, "both terminal steps are reported") assert(results[1].id == "b" and results[1].status == "skipped", "terminals keep declaration order") has(results[1].error, "a dependency did not complete") assert(results[2].id == "c" and results[2].status == "completed") assert(results[2].output == "gamma output") end) end }, { "independent roots start together", function() with_host(function(handle, profiles) local def = assert(toml_workflows.validate({ name = "fan", steps = { { id = "a", agent = "alpha", prompt = "A" }, { id = "c", agent = "gamma", prompt = "C" }, { id = "b", agent = "beta", prompt = "B", needs = { "a", "c" } }, }, }, "fan")) handle.queue_for("alpha", { output = "A out", settle = 2 }) handle.queue_for("gamma", { output = "C out", settle = 1 }) handle.queue_for("beta", { output = "B out" }) local results = toml_workflows.run(def, "input", profiles) assert(handle.max_live == 2, "both roots were in flight at once, peaked at " .. handle.max_live) has(handle.spawns[3].prompt, "## Output of a\n\nA out") has(handle.spawns[3].prompt, "## Output of c\n\nC out") assert(#results == 1 and results[1].id == "b" and results[1].status == "completed") end) end }, { "TOML parses into the same definition shape", function() if not pcall(require, "toml") then return "skip", "toml2lua is not installed" end local def, err = toml_workflows.parse(table.concat({ 'name = "review-chain"', 'description = "Two angles, then a synthesis."', '', '[[steps]]', 'id = "correctness"', 'agent = "alpha"', 'prompt = "Review for correctness."', '', '[[steps]]', 'id = "synthesis"', 'agent = "beta"', 'prompt = "Synthesize."', 'reasoning = "high"', 'needs = ["correctness"]', }, "\n"), "stem") assert(def, tostring(err)) assert(def.name == "review-chain") assert(def.description == "Two angles, then a synthesis.") assert(#def.steps == 2) assert(def.steps[2].reasoning == "high") assert(def.terminal.synthesis and def.terminal.correctness == nil) local bad, bad_err = toml_workflows.parse("name = = broken", "stem") assert(bad == nil, "malformed TOML must not parse") has(bad_err, "invalid TOML") end }, { "discovery shadows by name and registers one command per valid workflow", function() local valid = table.concat({ 'name = "review-chain"', 'description = "Inspect a change from two angles."', '[[steps]]', 'id = "one"', 'agent = "alpha"', 'prompt = "Look."', }, "\n") local user_shadowed = table.concat({ 'name = "shadowed"', 'description = "the user layer"', '[[steps]]', 'id = "one"', 'agent = "alpha"', 'prompt = "user"', }, "\n") local project_shadowed = table.concat({ 'name = "shadowed"', 'description = "the project layer"', '[[steps]]', 'id = "one"', 'agent = "beta"', 'prompt = "project"', }, "\n") local broken = table.concat({ '[[steps]]', 'id = "dup"', 'agent = "alpha"', 'prompt = "one"', '[[steps]]', 'id = "dup"', 'agent = "alpha"', 'prompt = "two"', }, "\n") return with_layers({ ["user/workflows/review-chain.toml"] = valid, ["user/workflows/nested/shadowed.toml"] = user_shadowed, ["project/workflows/shadowed.toml"] = project_shadowed, ["project/workflows/broken.toml"] = broken, }, function() with_host(function(handle, profiles) local found = toml_workflows.discover_and_register(profiles) assert(handle.commands_by_name["workflow:review-chain"], "a valid workflow registers a command") assert(handle.commands_by_name["workflow:review-chain"].description == "Inspect a change from two angles.", "the description comes from the file") assert(handle.commands_by_name["workflow:shadowed"].description == "the project layer", "the project layer shadows the user layer") assert(handle.commands_by_name["workflow:broken"] == nil, "an invalid file registers nothing") assert(#handle.commands == 2, "exactly two commands, saw " .. #handle.commands) local warned = false for _, warning in ipairs(found.warnings) do if warning:find("duplicate step id 'dup'", 1, true) then warned = true end end assert(warned, "the invalid file's error is kept: " .. table.concat(found.warnings, " | ")) -- The command tail becomes the workflow input. handle.queue_for("alpha", { output = "looked" }) local text = handle.commands_by_name["workflow:review-chain"].handler("check the parser") has(text, "step: one") has(text, "status: completed") has(text, "looked") has(handle.spawns[1].prompt, "## Workflow input\n\ncheck the parser") -- And the tool can run the same workflow, or explain a broken one. has(toml_workflows.handle({ name = "broken", prompt = "x" }, profiles), "failed to load") has(toml_workflows.handle({ name = "ghost", prompt = "x" }, profiles), "unknown workflow 'ghost'") end) end) end }, { "a broken project workflow shadows the user one under its declared name", function() local user_valid = table.concat({ 'name = "dup"', 'description = "the user layer"', '[[steps]]', 'id = "one"', 'agent = "alpha"', 'prompt = "user"', }, "\n") -- Declares the same name from a differently-named file, and is invalid. local project_broken = table.concat({ 'name = "dup"', '[[steps]]', 'id = "same"', 'agent = "alpha"', 'prompt = "one"', '[[steps]]', 'id = "same"', 'agent = "alpha"', 'prompt = "two"', }, "\n") return with_layers({ ["user/workflows/dup.toml"] = user_valid, ["project/workflows/whatever.toml"] = project_broken, }, function() with_host(function(handle, profiles) local found = toml_workflows.discover_and_register(profiles) local entry = found.by_name["dup"] assert(entry, "the project error must be indexed under the name it declares") assert(entry.definition == nil, "the shadowed user definition must not survive") has(entry.error, "duplicate step id 'same'") assert(#handle.commands == 0, "a shadowed-out workflow registers no command") has(toml_workflows.handle({ name = "dup", prompt = "x" }, profiles), "failed to load") end) end) end }, { "a command whose workflow cannot run reports the error, not a stack trace", function() with_host(function(handle, profiles) local def = assert(toml_workflows.validate({ name = "ghosted", steps = { { id = "one", agent = "nobody", prompt = "Do it." } }, }, "ghosted")) local ok, err = pcall(toml_workflows.run, def, "input", profiles) assert(not ok, "an unknown agent stops the workflow") has(tostring(err), "unknown agent 'nobody'") has(toml_workflows.handle({ prompt = "x", steps = { { id = "one", agent = "nobody", prompt = "Do it." } }, }, profiles), "Error:") assert(#handle.spawns == 0) end) end }, { "the tool takes exactly one of name and steps", function() with_host(function(handle, profiles) has(toml_workflows.handle({ prompt = "x" }, profiles), "pass exactly one of `name`") has(toml_workflows.handle({ prompt = "x", name = "a", steps = {} }, profiles), "not both") has(toml_workflows.handle({ name = "a" }, profiles), "prompt is required") assert(#handle.spawns == 0) end) end }, { "the tool runs a transient definition", function() with_host(function(handle, profiles) handle.queue_for("alpha", { output = "transient output" }) local text = toml_workflows.handle({ prompt = "the input", steps = { { id = "only", agent = "alpha", prompt = "Do it." } }, }, profiles) has(text, "step: only") has(text, "transient output") has(handle.spawns[1].prompt, "Do it.\n\n## Workflow input\n\nthe input") has(toml_workflows.handle({ prompt = "x", steps = { { id = "only", agent = "alpha", prompt = "p", needs = { "ghost" } } }, }, profiles), "unknown dependency 'ghost'") end) end }, }