/*
RFC-1326 tests for cloudflare.permissions.generate / .validate — the
marked-section writer and drift guard over .env.example and
docs/engineering/workshop-setup.md. Covers marker-missing failure,
registry-rendered rewrite, rerun-safe second run (IDEMP-02 backing test
for the rerun-safe declaration), and CFPERM-01 drift detection.
- RFC-1326: initial — generate/validate fixture tests incl. rerun-safe coverage.
- RFC-1326: single Cloudflare permission registry, token preflight, birth provisioning
*/
import { afterEach, describe, expect, test } from "vitest";
import { mkdtemp, mkdir, rm, readFile, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { createDefaultIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io";
import type { KernelCommandInput, KernelRuntimeContext } from "@warpgogol/werkstatt-engine/kernel";
import {
renderCloudflarePermissionCommentLines,
renderCloudflarePermissionTableRows,
} from "@warpgogol/werkstatt-shared/cloudflare/permissions";
import {
cloudflarePermissionsGenerateCommand,
cloudflarePermissionsValidateCommand,
} from "./cloudflare-permissions.ts";
const ENV_EXAMPLE_FIXTURE = `# header
# ── Cloudflare API
# IMPORTANT: Create a User API Token, NOT an Account API Token.
# BEGIN cloudflare-permissions
# Required permissions:
# STALE → Entry → Edit (nothing)
# END cloudflare-permissions
# Scope: specific account.
CLOUDFLARE_API_TOKEN=
`;
const WORKSHOP_SETUP_FIXTURE = `### 3.2 Permissions
| Permission | Scope | Level | Purpose |
| --- | --- | --- | --- |
| **STALE** | Account | Edit | nothing |
Not needed: Page Rules.
`;
function makeInput(): KernelCommandInput {
return { argv: [], flags: {} } as unknown as KernelCommandInput;
}
async function fixture(opts?: { envExample?: string; setup?: string }): Promise<{
root: string;
context: KernelRuntimeContext;
}> {
const root = await mkdtemp(join(tmpdir(), "cf-perms-"));
await mkdir(join(root, "docs", "engineering"), { recursive: true });
await writeFile(join(root, ".env.example"), opts?.envExample ?? ENV_EXAMPLE_FIXTURE);
await writeFile(
join(root, "docs", "engineering", "workshop-setup.md"),
opts?.setup ?? WORKSHOP_SETUP_FIXTURE,
);
const { io } = createDefaultIO();
const context = {
workspaceRoot: root,
dryRun: false,
logger: { info: () => {}, warn: () => {}, error: () => {}, debug: () => {}, success: () => {} },
io,
} as unknown as KernelRuntimeContext;
return { root, context };
}
let roots: string[] = [];
afterEach(async () => {
for (const root of roots) await rm(root, { recursive: true, force: true });
roots = [];
});
describe("cloudflare.permissions.generate", () => {
test("rewrites both marked sections from the registry", async () => {
const { root, context } = await fixture();
roots.push(root);
const result = (await cloudflarePermissionsGenerateCommand.execute(makeInput(), context))!;
expect(result.exitCode).toBe(0);
const envContent = await readFile(join(root, ".env.example"), "utf8");
for (const line of [
"# Required permissions:",
...renderCloudflarePermissionCommentLines(),
]) {
expect(envContent).toContain(line);
}
expect(envContent).not.toContain("STALE");
// Content outside the markers survives.
expect(envContent).toContain("IMPORTANT: Create a User API Token");
expect(envContent).toContain("CLOUDFLARE_API_TOKEN=");
const docContent = await readFile(
join(root, "docs", "engineering", "workshop-setup.md"),
"utf8",
);
for (const row of renderCloudflarePermissionTableRows()) {
expect(docContent).toContain(row);
}
expect(docContent).not.toContain("**STALE**");
});
test("rerun-safe: second run reports up to date and changes nothing", async () => {
const { root, context } = await fixture();
roots.push(root);
await cloudflarePermissionsGenerateCommand.execute(makeInput(), context);
const envAfterFirst = await readFile(join(root, ".env.example"), "utf8");
const docAfterFirst = await readFile(
join(root, "docs", "engineering", "workshop-setup.md"),
"utf8",
);
const second = (await cloudflarePermissionsGenerateCommand.execute(makeInput(), context))!;
expect(second.exitCode).toBe(0);
expect(second.summary).toContain("up to date");
expect(await readFile(join(root, ".env.example"), "utf8")).toBe(envAfterFirst);
expect(await readFile(join(root, "docs", "engineering", "workshop-setup.md"), "utf8")).toBe(
docAfterFirst,
);
});
test("fails when a target file carries no marked section", async () => {
const { context } = await fixture({ envExample: "# no markers\nCLOUDFLARE_API_TOKEN=\n" });
roots.push(context.workspaceRoot);
const result = (await cloudflarePermissionsGenerateCommand.execute(makeInput(), context))!;
expect(result.exitCode).toBe(1);
});
});
describe("cloudflare.permissions.validate", () => {
test("passes after generate", async () => {
const { context } = await fixture();
roots.push(context.workspaceRoot);
await cloudflarePermissionsGenerateCommand.execute(makeInput(), context);
const result = (await cloudflarePermissionsValidateCommand.execute(makeInput(), context))!;
expect(result.exitCode).toBe(0);
});
test("emits CFPERM-01 on a drifted marked section", async () => {
const { context } = await fixture();
roots.push(context.workspaceRoot);
const result = (await cloudflarePermissionsValidateCommand.execute(makeInput(), context))!;
expect(result.exitCode).toBe(1);
const diagnostics = (result.data as { diagnostics: { ruleId: string }[] }).diagnostics;
expect(diagnostics.some((d) => d.ruleId === "CFPERM-01")).toBe(true);
});
});