import { readdir, readFile, rm, stat } from "node:fs/promises"; import process from "node:process"; import { assert, expect, test } from "vite-plus/test"; import { parse } from "yaml"; import { inheritedEnvironment, makeTempDir, runCommand } from "#testing"; // A parsed YAML or JSON mapping, keyed by its dynamic field names. type Mapping = Map; const mapping = (value: unknown, at: string): Mapping => { assert(typeof value === "object" && value !== null && !Array.isArray(value), at); return new Map(Object.entries(value)); }; const sequence = (value: unknown, at: string): Mapping[] => { assert(Array.isArray(value), at); return value.map((entry: unknown, index) => mapping(entry, `${at}[${index}]`)); }; async function yaml(path: string): Promise { const value: unknown = parse(await readFile(path, "utf8")); return mapping(value, path); } async function yamlFiles(): Promise { const entries = await readdir(".", { recursive: true, withFileTypes: true }); return entries .filter((entry) => entry.isFile() && /\.ya?ml$/u.test(entry.name)) .map((entry) => `${entry.parentPath}/${entry.name}`.replace(/^\.\//u, "")) .filter((path) => !/^(?:\.git|\.superpowers|node_modules)\//u.test(path)); } const findStep = (steps: Mapping[], key: string, value: string): Mapping => { const step = steps.find((entry) => entry.get(key) === value); assert(step !== undefined, `${key}: ${value}`); return step; }; test("all committed YAML parses", async () => { const files = await yamlFiles(); assert(files.length > 0); await Promise.all( files.map(async (file) => { await yaml(file); }), ); }); test("the action exposes only the master key and scopes it to preparation", async () => { const action = await yaml("action.yml"); expect([...mapping(action.get("inputs"), "inputs").keys()]).toStrictEqual(["master-key"]); const steps = sequence(mapping(action.get("runs"), "runs").get("steps"), "runs.steps"); for (const step of steps) { const run = step.get("run"); if (typeof run === "string") { expect(run.includes("\n"), String(step.get("name"))).toBe(false); } } const checkout = findStep(steps, "uses", "actions/checkout@v4"); const prepare = findStep(steps, "id", "prepare"); const agent = findStep(steps, "id", "agent"); const post = findStep(steps, "id", "post"); findStep(steps, "id", "source"); expect(mapping(checkout.get("with"), "checkout.with").get("token")).toBe("${{ gitea.token }}"); expect(mapping(prepare.get("env"), "prepare.env").get("BOT_AGENTS_MASTER_KEY")).toBe( "${{ inputs.master-key }}", ); expect(JSON.stringify(Object.fromEntries(agent)).includes("BOT_AGENTS_MASTER_KEY")).toBe(false); expect(JSON.stringify(Object.fromEntries(post)).includes("BOT_AGENTS_MASTER_KEY")).toBe(false); expect(mapping(agent.get("env"), "agent.env").get("GITEA_TOKEN")).toBe("${{ gitea.token }}"); assert(String(post.get("if")).includes("always()")); for (const step of [prepare, agent, post]) { assert(String(step.get("run")).includes("steps.source.outputs.path")); } }); test("the source protection script stages immutable action files", async () => { const temp = await makeTempDir(); const outputFile = `${temp}/output`; try { const environment = inheritedEnvironment(); environment.set("ACTION_PATH", process.cwd()); environment.set("GITHUB_OUTPUT", outputFile); environment.set("RUNNER_TEMP", temp); const result = await runCommand("/bin/bash", ["scripts/protect-source.sh"], environment); assert(result.success, result.stderr); const output = await readFile(outputFile, "utf8"); const path = output.trim().slice(5); const files = [ "src/prepare/index.ts", "src/run/index.ts", "src/post/index.ts", "src/runtime/index.ts", "src/agents/auth.ts", "package.json", "profiles.json", "prompt.md", ]; const stats = await Promise.all( files.map(async (file) => { const info = await stat(`${path}/${file}`); return info; }), ); for (const [index, info] of stats.entries()) { assert(info.isFile(), files[index]); expect(info.mode & 0o022, files[index]).toStrictEqual(0); } } finally { await rm(temp, { recursive: true }); } }); test("module-relative runtime assets exist", async () => { const module = new URL("../src/run/index.ts", import.meta.url); const source = await readFile(module, "utf8"); const assets = [...source.matchAll(/new URL\("(?[^"]+)", import\.meta\.url\)/gu)].map( ([, asset = ""]) => new URL(asset, module), ); const stats = await Promise.all( assets.map(async (asset) => { const info = await stat(asset); return info; }), ); for (const [index, info] of stats.entries()) { assert(info.isFile(), assets[index]?.pathname); } }); test("the repository uses its trusted main action", async () => { const workflow = await yaml(".gitea/workflows/agents.yml"); const job = mapping(mapping(workflow.get("jobs"), "jobs").get("agent"), "jobs.agent"); const permissions = mapping(job.get("permissions"), "jobs.agent.permissions"); expect(Object.fromEntries(permissions)).toStrictEqual({ code: "write", issues: "write", "pull-requests": "write", }); const steps = sequence(job.get("steps"), "jobs.agent.steps"); expect(steps.findIndex((step) => step.get("uses") === "./")).toStrictEqual(-1); const trusted = findStep(steps, "uses", "https://git.capsulizers.com/commons/bot-agents@main"); expect(mapping(trusted.get("with"), "trusted.with").get("master-key")).toBe( "${{ secrets.BOT_AGENTS_MASTER_KEY }}", ); expect(JSON.stringify(Object.fromEntries(workflow)).includes("gitea.actor != 'bot'")).toBe(true); }); test("CI checks every phase and the root isolation suite", async () => { const workflow = await yaml(".gitea/workflows/check.yml"); const job = mapping(mapping(workflow.get("jobs"), "jobs").get("check"), "jobs.check"); const commands = sequence(job.get("steps"), "jobs.check.steps") .map((step) => { const run = step.get("run"); return typeof run === "string" ? run : ""; }) .join("\n"); // `vp check` type-checks every phase, and `vp test` runs each of them under // Node as well as the root-only isolation tests. assert(commands.includes("npm ci")); assert(commands.includes("vp check")); assert(commands.includes("vp test")); }); test("uses the standard Skills installer without custom discovery", async () => { const runner = await readFile("src/run/index.ts", "utf8"); assert(runner.includes("skills add obra/superpowers")); const removed = await stat("superpowers.ts").then( () => false, (error: unknown) => error instanceof Error && "code" in error && error.code === "ENOENT", ); assert(removed, "superpowers.ts should be removed"); }); test("installs the OpenCode CLI", async () => { const action = await readFile("action.yml", "utf8"); assert(action.includes("opencode-ai")); }); test("does not install Deno", async () => { const action = await readFile("action.yml", "utf8"); expect(action).not.toMatch(/\bdeno\b/iu); }); test("profile preparation does not receive provider credentials", async () => { const runner = await readFile("src/run/index.ts", "utf8"); assert(runner.includes("await prepareProfile(profile);")); expect(runner.includes("prepareProfile(profile, token)")).toBe(false); }); test("central profiles fall back from Codex to Claude to OpenCode", async () => { const credentials: unknown = JSON.parse(await readFile("profiles.json", "utf8")); const profiles = sequence(mapping(credentials, "profiles.json").get("profiles"), "profiles"); expect(profiles.map((profile) => profile.get("type"))).toStrictEqual([ "codex", "claude", "opencode", ]); expect(profiles[2]?.get("model")).toBe("openrouter/qwen/qwen3.8-27b"); assert(String(profiles[2]?.get("token")).startsWith("v1:")); });