import assert from "node:assert/strict" import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs" import { tmpdir } from "node:os" import path from "node:path" import { test } from "node:test" import plugin from "../src/index.ts" import { loadManifest } from "../src/manifest.ts" import { blockedPattern, resolveOptions } from "../src/guardrails.ts" const manifest = loadManifest() function fakeCtx(options: Record, opts: { agents?: { id: string }[]; dir?: string } = {}) { const added: Record[] = [] const hooks: { name: string; fn: (event: unknown) => void }[] = [] const warns: string[] = [] const agentEditor = { list: () => opts.agents ?? [], get: () => undefined, default: () => {}, update: () => {}, remove: () => {}, } const ctx = { options, location: { directory: opts.dir ?? tmpdir() }, skill: { transform: async (fn: (e: { add: (s: unknown) => void }) => void) => fn({ add: (s) => added.push(s) }) }, tool: { hook: async (name: string, fn: (event: unknown) => void) => hooks.push({ name, fn }) }, agent: { transform: async (fn: (e: typeof agentEditor) => void) => fn(agentEditor) }, } const realWarn = console.warn console.warn = (msg: string) => warns.push(msg) return { ctx: ctx as never, added, hooks, warns, restore: () => (console.warn = realWarn), } } test("plugin id", () => { assert.equal(plugin.id, "haylan.pocock") }) test("setup injects every manifest skill with absolute existing paths and autoinvoke flags", async () => { const f = fakeCtx({}) try { await plugin.setup(f.ctx) assert.equal(f.added.length, manifest.length) const ids = f.added.map((s) => s.id) assert.equal(new Set(ids).size, ids.length, "unique ids") const hidden = f.added.filter((s) => s.autoinvoke === false) assert.equal(hidden.length, 16, "user-invoked skills hidden from model") for (const skill of f.added) { assert.ok(path.isAbsolute(skill.path as string), `${skill.id}: absolute path`) assert.ok(existsSync(skill.path as string), `${skill.id}: file exists`) assert.ok((skill.content as string).length > 0, `${skill.id}: content loaded`) const entry = manifest.find((m) => m.id === skill.id)! assert.equal(skill.content, readFileSync(skill.path as string, "utf8")) assert.equal(skill.description, entry.description) } const toSpec = f.added.find((s) => s.id === "to-spec")! assert.equal(toSpec.autoinvoke, false) const tdd = f.added.find((s) => s.id === "tdd")! assert.equal(tdd.autoinvoke, true) } finally { f.restore() } }) test("setup throws on bad options and registers nothing", async () => { const f = fakeCtx({ guardrails: "yes" }) try { await assert.rejects(Promise.resolve(plugin.setup(f.ctx)), /guardrails must be a boolean/) assert.equal(f.added.length + f.hooks.length, 0) } finally { f.restore() } }) test("guardrails off by default; on: hook blocks dangerous git and only on shell tools", async () => { const off = fakeCtx({}) try { await plugin.setup(off.ctx) assert.equal(off.hooks.length, 0) } finally { off.restore() } const on = fakeCtx({ guardrails: true }) try { await plugin.setup(on.ctx) assert.equal(on.hooks.length, 1) assert.equal(on.hooks[0].name, "execute.before") const fire = (tool: string, command: string) => on.hooks[0].fn({ tool, input: { command } }) assert.throws(() => fire("bash", "git push --force origin main"), /BLOCKED/) assert.throws(() => fire("shell", "git reset --hard HEAD~1"), /BLOCKED/) assert.throws(() => fire("bash", "git clean -fd"), /BLOCKED/) fire("bash", "git status --short") fire("bash", "npm test") fire("read", "git push") // non-shell tool untouched fire("bash", "") // no command edge on.hooks[0].fn({ tool: "bash", input: undefined }) } finally { on.restore() } }) test("agent staleness warning: missing, stale and up-to-date installs", async () => { const dir = mkdtempSync(path.join(tmpdir(), "plugin-test-")) try { // missing agents const missing = fakeCtx({}, { agents: [{ id: "build" }], dir }) try { await plugin.setup(missing.ctx) assert.equal(missing.warns.length, 1) assert.match(missing.warns[0], /not installed — run: npx @haylan\/opencode-pocock init/) } finally { missing.restore() } // installed + matching version stamp -> silent mkdirSync(path.join(dir, ".opencode", "agents"), { recursive: true }) const pkg = JSON.parse(readFileSync(path.join(process.cwd(), "package.json"), "utf8")) writeFileSync(path.join(dir, ".opencode", "agents", ".pocock-version"), pkg.version + "\n") const ok = fakeCtx({}, { agents: [{ id: "pocock" }, { id: "pocock-worker" }], dir }) try { await plugin.setup(ok.ctx) assert.deepEqual(ok.warns, []) } finally { ok.restore() } // installed + version mismatch -> refresh hint writeFileSync(path.join(dir, ".opencode", "agents", ".pocock-version"), "0.0.1\n") const stale = fakeCtx({}, { agents: [{ id: "pocock-reviewer" }], dir }) try { await plugin.setup(stale.ctx) assert.equal(stale.warns.length, 1) assert.match(stale.warns[0], /refresh: npx @haylan\/opencode-pocock init/) } finally { stale.restore() } } finally { rmSync(dir, { recursive: true, force: true }) } }) test("options and pattern helpers", () => { assert.deepEqual(resolveOptions(undefined), { guardrails: false }) assert.deepEqual(resolveOptions({ guardrails: true }), { guardrails: true }) assert.equal(blockedPattern("git branch -D feature"), "git branch -D") assert.equal(blockedPattern("git checkout ."), "git checkout .") // grep -E substring semantics, faithful to upstream: "git pushy" contains "git push" assert.equal(blockedPattern("echo git pushy"), "git push") assert.equal(blockedPattern("git status && npm test"), undefined) })