summaryrefslogtreecommitdiff
path: root/spec/test_profiles.lua
blob: f2a54a268cab36f80f54b4bd3603302615798831 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
-- 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 },

    { "config roots follow every host layer, in host order", function()
        local fake = require("spec.fake_ext")
        local paths = require("subagents.paths")
        local handle = fake.install()
        local ok, err = pcall(function()
            local roots = paths.config_roots("workflows")
            assert(#roots == 4, "expected one root per layer, saw " .. #roots)
            assert(roots[1] == "/data/panto/agent/workflows", roots[1])
            assert(roots[2] == "/home/u/.config/panto/workflows", roots[2])
            assert(roots[3] == "/proj/.panto/workflows", roots[3])
            assert(roots[4] == "/proj/.panto/local/workflows", roots[4])
        end)
        handle.restore()
        if not ok then error(err, 0) end
    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 },
}