115 lines
4.7 KiB
TypeScript
115 lines
4.7 KiB
TypeScript
import assert from "node:assert/strict"
|
|
import { existsSync, readFileSync, readdirSync } from "node:fs"
|
|
import path from "node:path"
|
|
import { test } from "node:test"
|
|
import { fileURLToPath } from "node:url"
|
|
import { parseFrontmatter } from "../scripts/frontmatter.ts"
|
|
import { loadClassification } from "../scripts/generate.ts"
|
|
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..")
|
|
const entries = loadClassification(path.join(root, ".opencode/state/classification.json"))
|
|
const vendored = entries.filter((e) => e.artifacts.includes("skill:verbatim"))
|
|
const agentFiles = readdirSync(path.join(root, "agents")).filter((f) => f.endsWith(".md"))
|
|
|
|
// V2-native frontmatter only (AGENTS.md convention); legacy v1 keys must never appear.
|
|
const V2_KEYS = new Set([
|
|
"name",
|
|
"description",
|
|
"mode",
|
|
"model",
|
|
"color",
|
|
"permissions",
|
|
"hidden",
|
|
"steps",
|
|
"disabled",
|
|
"request",
|
|
"system",
|
|
])
|
|
|
|
test("agent frontmatter uses V2-native keys only", () => {
|
|
for (const file of agentFiles) {
|
|
const { frontmatter } = parseFrontmatter(readFileSync(path.join(root, "agents", file), "utf8"))
|
|
for (const key of Object.keys(frontmatter)) {
|
|
assert.ok(V2_KEYS.has(key), `${file}: non-V2 frontmatter key '${key}'`)
|
|
}
|
|
assert.ok(frontmatter.mode, `${file}: mode required`)
|
|
assert.ok(frontmatter.description, `${file}: description required`)
|
|
}
|
|
})
|
|
|
|
test("every skill an agent names still exists in the upstream snapshot", () => {
|
|
const allNames = new Set(entries.map((e) => e.name))
|
|
for (const file of agentFiles) {
|
|
const body = readFileSync(path.join(root, "agents", file), "utf8")
|
|
const tokens = new Set(
|
|
[...body.matchAll(/`([a-z][a-z0-9-]*)`/g)].map((m) => m[1]).filter((t) => allNames.has(t)),
|
|
)
|
|
for (const name of tokens) {
|
|
const entry = entries.find((e) => e.name === name)!
|
|
const dir = path.join(root, "upstream/skills", entry.bucket, name)
|
|
assert.ok(
|
|
existsSync(path.join(dir, "SKILL.md")),
|
|
`${file} names skill '${name}' which no longer exists upstream (renamed or removed?)`,
|
|
)
|
|
assert.ok(
|
|
entry.artifacts.includes("skill:verbatim"),
|
|
`${file} names skill '${name}' which is excluded from this package`,
|
|
)
|
|
}
|
|
}
|
|
})
|
|
|
|
test("worker may only load model-invoked vendored skills", () => {
|
|
const worker = readFileSync(path.join(root, "agents/pocock-worker.md"), "utf8")
|
|
const allowed = [...worker.matchAll(/action:\s*skill,\s*resource:\s*"([^"]+)",\s*effect:\s*allow/g)].map(
|
|
(m) => m[1],
|
|
)
|
|
assert.ok(allowed.length >= 3, "worker skill allow-list parsed")
|
|
const userInvoked = new Set(entries.filter((e) => e.user_invoked).map((e) => e.name))
|
|
const vendoredNames = new Set(vendored.map((e) => e.name))
|
|
for (const name of allowed) {
|
|
assert.ok(!userInvoked.has(name), `worker allows user-invoked skill '${name}' (needs a person)`)
|
|
assert.ok(vendoredNames.has(name), `worker allows '${name}' which is not a vendored skill`)
|
|
}
|
|
for (const core of ["tdd", "codebase-design", "diagnosing-bugs"]) {
|
|
assert.ok(allowed.includes(core), `worker missing core skill '${core}'`)
|
|
}
|
|
})
|
|
|
|
test("reviewer is read-only: edit/subagent denied, only code-review allowed", () => {
|
|
const reviewer = readFileSync(path.join(root, "agents/pocock-reviewer.md"), "utf8")
|
|
assert.match(reviewer, /action:\s*edit,\s*resource:\s*"\*",\s*effect:\s*deny/)
|
|
assert.match(reviewer, /action:\s*subagent,\s*resource:\s*"\*",\s*effect:\s*deny/)
|
|
const allowedSkills = [...reviewer.matchAll(/action:\s*skill,\s*resource:\s*"([^"]+)",\s*effect:\s*allow/g)].map(
|
|
(m) => m[1],
|
|
)
|
|
assert.deepEqual(allowedSkills, ["code-review"])
|
|
})
|
|
|
|
test("worker and reviewer cannot spawn subagents (no circular delegation)", () => {
|
|
for (const file of ["pocock-worker.md", "pocock-reviewer.md"]) {
|
|
const body = readFileSync(path.join(root, "agents", file), "utf8")
|
|
assert.match(body, /action:\s*subagent,\s*resource:\s*"\*",\s*effect:\s*deny/, `${file}: subagent deny`)
|
|
}
|
|
})
|
|
|
|
test("classification is fully reviewed and matches the generated tree", () => {
|
|
const needsReview = entries.filter((e) => e.status === "needs-review")
|
|
assert.deepEqual(
|
|
needsReview.map((e) => e.name),
|
|
[],
|
|
"needs-review classifications block the build until reviewed",
|
|
)
|
|
const manifest = JSON.parse(readFileSync(path.join(root, "manifest.json"), "utf8"))
|
|
assert.deepEqual(
|
|
manifest.map((m: { id: string }) => m.id).sort(),
|
|
vendored.map((e) => e.name).sort(),
|
|
)
|
|
for (const e of vendored) {
|
|
assert.ok(existsSync(path.join(root, "skills", e.name, "SKILL.md")), `skills/${e.name} generated`)
|
|
}
|
|
for (const e of entries.filter((x) => x.artifacts.includes("excluded"))) {
|
|
assert.ok(!existsSync(path.join(root, "skills", e.name)), `excluded skill ${e.name} must not ship`)
|
|
}
|
|
})
|