summaryrefslogtreecommitdiff
path: root/spec/test_profiles.lua
diff options
context:
space:
mode:
Diffstat (limited to 'spec/test_profiles.lua')
-rw-r--r--spec/test_profiles.lua138
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 },
+}