diff options
Diffstat (limited to 'spec/test_profiles.lua')
| -rw-r--r-- | spec/test_profiles.lua | 138 |
1 files changed, 138 insertions, 0 deletions
diff --git a/spec/test_profiles.lua b/spec/test_profiles.lua new file mode 100644 index 0000000..3a052b9 --- /dev/null +++ b/spec/test_profiles.lua @@ -0,0 +1,138 @@ +-- subagents/profiles.lua: two-layer recursive discovery over real directories. +-- +-- Discovery is exercised against temporary directories rather than the machine's +-- real config, by passing explicit roots to `discover` — the same seam the +-- extension uses for the user and project layers, in the same order. +-- +-- Needs luv (the recursive walk) and lyaml (the frontmatter); without either the +-- whole file skips rather than asserting on a degraded parse. + +local profiles = require("subagents.profiles") + +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 + +-- Build both layers once; every case reads the same discovery result. +local function discover_fixture() + local ok_uv, uv = pcall(require, "luv") + if not ok_uv then + return nil, "luv is not installed" + end + if not pcall(require, "lyaml") then + return nil, "lyaml is not installed" + end + + local tmp = assert(uv.fs_mkdtemp("/tmp/panto-subagents-profiles-XXXXXX")) + local user = tmp .. "/user/agents" + local project = tmp .. "/project/agents" + + write(user .. "/reviewer.md", table.concat({ + "---", + "description: Reviews changes", + "model: anthropic:sonnet", + "reasoning: high", + "---", + "You are a reviewer.", + "", + }, "\n")) + write(user .. "/nested/deeper/planner.md", "You plan.\n") + write(user .. "/shared.md", "---\nname: shared\ndescription: from the user layer\n---\nuser body\n") + write(user .. "/renamed.md", "---\nname: from-frontmatter\n---\nbody\n") + write(project .. "/shared.md", "---\nname: shared\ndescription: from the project layer\n---\nproject body\n") + write(project .. "/foreign.md", "---\ndescription: written for another harness\nmodel: claude-sonnet-4\n---\nbody\n") + + local found = profiles.discover({ user, project }) + os.execute("rm -rf " .. tmp) + return found +end + +local fixture, skip_reason = discover_fixture() + +local function fixture_or_skip() + if not fixture then + return nil, skip_reason + end + return fixture +end + +return { + { "frontmatter fields land on the profile", function() + local found, reason = fixture_or_skip() + if not found then + return "skip", reason + end + local reviewer = found.by_name.reviewer + assert(reviewer, "reviewer was not discovered") + assert(reviewer.name == "reviewer", "name should default to the file stem") + assert(reviewer.description == "Reviews changes", tostring(reviewer.description)) + assert(reviewer.model == "anthropic:sonnet", tostring(reviewer.model)) + assert(reviewer.reasoning == "high", tostring(reviewer.reasoning)) + assert(reviewer.body:find("You are a reviewer.", 1, true), "body was lost") + end }, + + { "discovery recurses and defaults the name to the stem", function() + local found, reason = fixture_or_skip() + if not found then + return "skip", reason + end + local planner = found.by_name.planner + assert(planner, "a nested profile was not discovered") + assert(planner.description == "", "a bodyless header means an empty description") + assert(planner.body == "You plan.\n", string.format("%q", planner.body)) + assert(found.by_name["from-frontmatter"], "`name` should override the stem") + assert(found.by_name.renamed == nil, "the stem must not survive an explicit name") + end }, + + { "the project layer shadows the user layer by name", function() + local found, reason = fixture_or_skip() + if not found then + return "skip", reason + end + local shared = found.by_name.shared + assert(shared, "shared was not discovered") + assert(shared.description == "from the project layer", tostring(shared.description)) + assert(shared.body == "project body\n", string.format("%q", shared.body)) + local count = 0 + for _, profile in ipairs(found.list) do + if profile.name == "shared" then + count = count + 1 + end + end + assert(count == 1, "a shadowed profile must appear once, saw " .. count) + end }, + + { "a foreign model spelling warns and inherits instead", function() + local found, reason = fixture_or_skip() + if not found then + return "skip", reason + end + local foreign = found.by_name.foreign + assert(foreign, "foreign was not discovered") + assert(foreign.model == nil, "an unparseable model must be dropped") + assert(foreign.description == "written for another harness", "the rest of the header must survive") + local warned = false + for _, warning in ipairs(found.warnings) do + if warning:find("ignoring model 'claude-sonnet-4'", 1, true) then + warned = true + end + end + assert(warned, "expected a warning naming the ignored model, got: " .. + table.concat(found.warnings, " | ")) + end }, + + { "the list is sorted by name", function() + local found, reason = fixture_or_skip() + if not found then + return "skip", reason + end + assert(#found.list >= 5, "expected every profile in the list, saw " .. #found.list) + for index = 2, #found.list do + assert(found.list[index - 1].name < found.list[index].name, + "list is not sorted at " .. index) + end + end }, +} |
