115 lines
4.3 KiB
TypeScript
115 lines
4.3 KiB
TypeScript
import { assert, assertEquals } from "jsr:@std/assert@1";
|
|
import { parse } from "jsr:@std/yaml@1";
|
|
|
|
type Mapping = Record<string, unknown>;
|
|
|
|
const mapping = (value: unknown, at: string): Mapping => {
|
|
assert(
|
|
typeof value === "object" && value !== null && !Array.isArray(value),
|
|
at,
|
|
);
|
|
return value as Mapping;
|
|
};
|
|
|
|
const sequence = (value: unknown, at: string): Mapping[] => {
|
|
assert(Array.isArray(value), at);
|
|
return value.map((entry, index) => mapping(entry, `${at}[${index}]`));
|
|
};
|
|
|
|
async function yaml(path: string): Promise<Mapping> {
|
|
return mapping(parse(await Deno.readTextFile(path)), path);
|
|
}
|
|
|
|
async function yamlFiles(directory = "."): Promise<string[]> {
|
|
const result: string[] = [];
|
|
for await (const entry of Deno.readDir(directory)) {
|
|
if (entry.name === ".git" || entry.name === ".superpowers") continue;
|
|
const path = directory === "." ? entry.name : `${directory}/${entry.name}`;
|
|
if (entry.isDirectory) result.push(...await yamlFiles(path));
|
|
if (entry.isFile && /\.ya?ml$/.test(entry.name)) result.push(path);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
Deno.test("all committed YAML parses", async () => {
|
|
const files = await yamlFiles();
|
|
assert(files.length > 0);
|
|
for (const file of files) await yaml(file);
|
|
});
|
|
|
|
Deno.test("the action exposes only the master key and scopes it to preparation", async () => {
|
|
const action = await yaml("action.yml");
|
|
assertEquals(Object.keys(mapping(action.inputs, "inputs")), ["master-key"]);
|
|
const steps = sequence(mapping(action.runs, "runs").steps, "runs.steps");
|
|
const checkout = steps.find((step) => step.uses === "actions/checkout@v4");
|
|
const prepare = steps.find((step) => step.id === "prepare");
|
|
const agent = steps.find((step) => step.id === "agent");
|
|
const post = steps.find((step) => step.id === "post");
|
|
const source = steps.find((step) => step.id === "source");
|
|
assert(checkout && source && prepare && agent && post);
|
|
assertEquals(
|
|
mapping(checkout.with, "checkout.with").token,
|
|
"${{ gitea.token }}",
|
|
);
|
|
assertEquals(
|
|
mapping(prepare.env, "prepare.env").BOT_AGENTS_MASTER_KEY,
|
|
"${{ inputs.master-key }}",
|
|
);
|
|
assertEquals(JSON.stringify(agent).includes("BOT_AGENTS_MASTER_KEY"), false);
|
|
assertEquals(JSON.stringify(post).includes("BOT_AGENTS_MASTER_KEY"), false);
|
|
assertEquals(
|
|
mapping(agent.env, "agent.env").GITEA_TOKEN,
|
|
"${{ gitea.token }}",
|
|
);
|
|
assert(String(post.if).includes("always()"));
|
|
for (const step of [prepare, agent, post]) {
|
|
assert(String(step.run).includes("steps.source.outputs.path"));
|
|
}
|
|
});
|
|
|
|
Deno.test("the repository uses its trusted main action", async () => {
|
|
const workflow = await yaml(".gitea/workflows/agents.yml");
|
|
const job = mapping(mapping(workflow.jobs, "jobs").agent, "jobs.agent");
|
|
const permissions = mapping(job.permissions, "jobs.agent.permissions");
|
|
assertEquals(permissions, {
|
|
code: "write",
|
|
issues: "write",
|
|
"pull-requests": "write",
|
|
});
|
|
const steps = sequence(job.steps, "jobs.agent.steps");
|
|
const local = steps.findIndex((step) => step.uses === "./");
|
|
assertEquals(local, -1);
|
|
const trusted = steps.findIndex((step) =>
|
|
step.uses === "https://git.capsulizers.com/commons/bot-agents@main"
|
|
);
|
|
assert(trusted >= 0);
|
|
assertEquals(
|
|
mapping(steps[trusted].with, "trusted.with")["master-key"],
|
|
"${{ secrets.BOT_AGENTS_MASTER_KEY }}",
|
|
);
|
|
assertEquals(JSON.stringify(workflow).includes("gitea.actor != 'bot'"), true);
|
|
});
|
|
|
|
Deno.test("CI checks every phase and the root isolation suite", async () => {
|
|
const workflow = await yaml(".gitea/workflows/check.yml");
|
|
const job = mapping(mapping(workflow.jobs, "jobs").deno, "jobs.deno");
|
|
const commands = sequence(job.steps, "jobs.deno.steps")
|
|
.map((step) => typeof step.run === "string" ? step.run : "")
|
|
.join("\n");
|
|
for (const entrypoint of ["prepare.ts", "run.ts", "post.ts"]) {
|
|
assert(commands.includes(entrypoint), entrypoint);
|
|
}
|
|
assert(commands.includes("deno test --allow-all"));
|
|
});
|
|
|
|
Deno.test("uses the standard Skills installer without custom discovery", async () => {
|
|
const runner = await Deno.readTextFile("run.ts");
|
|
assert(runner.includes("skills add obra/superpowers"));
|
|
try {
|
|
await Deno.stat("superpowers.ts");
|
|
assert(false, "superpowers.ts should be removed");
|
|
} catch (error) {
|
|
assert(error instanceof Deno.errors.NotFound);
|
|
}
|
|
});
|