Files
fusion/packages/cli/src/__tests__/ci-workflow.test.ts
Fusion 7305e14fc8 test(FN-4197): complete Step 2 — enforce release frozen lockfile contract
Fusion-Task-Id: FN-4197
Fusion-Task-Lineage: 27bb77e2-52a1-4b7d-8830-fe13af235b97
2026-05-13 06:29:35 -07:00

523 lines
19 KiB
TypeScript

import { describe, it, expect, beforeAll } from "vitest";
import { readFileSync, accessSync, constants } from "node:fs";
import { join } from "node:path";
import { parse } from "yaml";
const workspaceRoot = join(import.meta.dirname!, "..", "..", "..", "..");
function loadYamlFile(...pathParts: string[]): any {
const path = join(workspaceRoot, ...pathParts);
const content = readFileSync(path, "utf-8");
const parsed = parse(content) as Record<string, unknown>;
// Some YAML parsers treat the unquoted `on:` key as boolean `true`.
// Normalize it so tests can consistently read `workflow.on`.
if (parsed && parsed.on === undefined) {
(parsed as any).on = (parsed as any)["on"] ?? (parsed as any).true ?? (parsed as any)["true"];
}
return { content, parsed };
}
function loadWorkflow(name: string): any {
return loadYamlFile(".github", "workflows", name);
}
function findCompositeSetupStep(steps: any[]) {
return steps.find((step) => step.uses === "./.github/actions/setup-node-pnpm");
}
describe("CI workflow (.github/workflows/ci.yml)", () => {
let workflow: any;
let content: string;
let compositeAction: any;
let buildSteps: any[];
let testShardJob: any;
let contributingContent: string;
let readmeContent: string;
let cliPackageJsonContent: string;
let extensionSuiteContent: string;
let agentExportSuiteContent: string;
let buildExeSuiteContent: string;
beforeAll(() => {
const result = loadWorkflow("ci.yml");
workflow = result.parsed;
content = result.content;
compositeAction = loadYamlFile(".github", "actions", "setup-node-pnpm", "action.yml").parsed;
buildSteps = workflow.jobs?.build?.steps ?? [];
testShardJob = workflow.jobs?.["test-shards"];
contributingContent = readFileSync(join(workspaceRoot, "docs", "contributing.md"), "utf-8");
readmeContent = readFileSync(join(workspaceRoot, "README.md"), "utf-8");
cliPackageJsonContent = readFileSync(join(workspaceRoot, "packages", "cli", "package.json"), "utf-8");
extensionSuiteContent = readFileSync(
join(workspaceRoot, "packages", "cli", "src", "__tests__", "extension-integration.test.ts"),
"utf-8",
);
agentExportSuiteContent = readFileSync(
join(workspaceRoot, "packages", "cli", "src", "commands", "__tests__", "agent-export.test.ts"),
"utf-8",
);
buildExeSuiteContent = readFileSync(
join(workspaceRoot, "packages", "cli", "src", "__tests__", "build-exe-cross.test.ts"),
"utf-8",
);
});
const findBuildStepByRun = (runSnippet: string) =>
buildSteps.find((step) => typeof step.run === "string" && step.run.includes(runSnippet));
it("is valid YAML", () => {
expect(workflow).toBeDefined();
expect(typeof workflow).toBe("object");
});
it("uses workflow_dispatch trigger (auto CI disabled)", () => {
expect(workflow.on).toHaveProperty("workflow_dispatch");
});
it("does not auto-trigger on push/pull_request", () => {
expect(workflow.on.push).toBeUndefined();
expect(workflow.on.pull_request).toBeUndefined();
});
it("pins dependency bootstrap to frozen lockfile", () => {
const jobs = [workflow.jobs?.lint, workflow.jobs?.["test-shards"], workflow.jobs?.build];
for (const job of jobs) {
expect(findCompositeSetupStep(job?.steps ?? [])).toBeDefined();
}
expect(content).not.toContain("run: pnpm install\n");
expect(content).not.toContain("--no-frozen-lockfile");
expect(compositeAction.inputs?.["install-args"]?.default).toBe("--frozen-lockfile");
});
it("uses deterministic test sharding and keeps lint/build as explicit jobs", () => {
expect(workflow.jobs?.lint).toBeDefined();
expect(testShardJob).toBeDefined();
expect(workflow.jobs?.build).toBeDefined();
expect(testShardJob.strategy?.matrix?.shard).toEqual([1, 2, 3]);
expect(content).toContain("pnpm test:ci:shard --shard ${{ matrix.shard }} --total 3");
expect(content).not.toContain("pnpm verify:workspace");
});
it("runs build job after lint and sharded tests, then executes slow lane and binary packaging", () => {
expect(workflow.jobs?.build?.needs).toEqual(["lint", "test-shards"]);
expect(findBuildStepByRun("pnpm build")).toBeDefined();
expect(findBuildStepByRun("pnpm test:slow-cli")).toBeDefined();
expect(findBuildStepByRun("build:exe")).toBeDefined();
});
it("keeps contributing docs aligned with verification and slow-lane contracts", () => {
expect(contributingContent).toContain("pnpm test:full` must be runnable in a clean worktree without requiring a prior `pnpm build`.");
expect(contributingContent).toContain("`pnpm verify:workspace` is the canonical pre-merge gate");
expect(contributingContent).toContain("1. `pnpm lint`");
expect(contributingContent).toContain("2. `pnpm test:full`");
expect(contributingContent).toContain("3. `pnpm build`");
expect(contributingContent).toContain("`pnpm test` now uses a changed-only entrypoint");
expect(contributingContent).toContain("pnpm test:slow-cli");
expect(contributingContent).toContain("test:pre-release");
expect(contributingContent).toContain("test:extension-integration");
});
it("keeps docs aligned with default and explicit build commands", () => {
expect(readmeContent).toContain("pnpm build # Build default workspace packages (excludes desktop/mobile)");
expect(readmeContent).toContain("pnpm build:all # Build all packages (including desktop/mobile)");
expect(contributingContent).toContain("pnpm build # default build (excludes desktop/mobile)");
expect(contributingContent).toContain("pnpm build:all # full recursive build including desktop/mobile");
});
it("includes binary build step", () => {
expect(content).toContain("build:exe");
});
it("keeps explicit gating for audited CLI integration suites", () => {
expect(cliPackageJsonContent).toContain('"test:slow-cli"');
expect(cliPackageJsonContent).toContain("FUSION_TEST_SLOW_CLI=1");
expect(cliPackageJsonContent).toContain('"test:extension-integration"');
expect(cliPackageJsonContent).toContain("FUSION_TEST_EXTENSION_INTEGRATION=1");
expect(cliPackageJsonContent).toContain("extension-integration.test.ts");
expect(cliPackageJsonContent).toContain('"test:build-exe"');
expect(cliPackageJsonContent).toContain("FUSION_TEST_BUILD_EXE=1");
expect(extensionSuiteContent).toContain("describe.skipIf(!SHOULD_RUN_EXTENSION_INTEGRATION)");
expect(extensionSuiteContent).toContain("FUSION_TEST_EXTENSION_INTEGRATION");
expect(extensionSuiteContent).toContain("dist/extension.js");
expect(agentExportSuiteContent).toContain("describe.skipIf(!SHOULD_RUN_SLOW_CLI)");
expect(agentExportSuiteContent).toContain("FUSION_TEST_SLOW_CLI");
expect(buildExeSuiteContent).toContain('process.env.FUSION_TEST_BUILD_EXE === "1"');
expect(buildExeSuiteContent).toContain('process.env.FUSION_TEST_BUILD_EXE === "true"');
expect(buildExeSuiteContent).not.toContain("Boolean(process.env.FUSION_TEST_BUILD_EXE)");
});
it("includes Bun setup", () => {
expect(content).toContain("oven-sh/setup-bun");
});
it("verifies binary exists after build", () => {
expect(content).toContain("test -f packages/cli/dist/fn");
});
});
describe("PR checks workflow (.github/workflows/pr-checks.yml)", () => {
let workflow: any;
let content: string;
beforeAll(() => {
const result = loadWorkflow("pr-checks.yml");
workflow = result.parsed;
content = result.content;
});
it("is valid YAML", () => {
expect(workflow).toBeDefined();
expect(typeof workflow).toBe("object");
});
it("runs on pull requests targeting main", () => {
expect(workflow.on?.pull_request?.branches).toContain("main");
});
it("uses the same deterministic test sharding command as manual CI", () => {
expect(workflow.jobs?.lint).toBeDefined();
expect(workflow.jobs?.typecheck).toBeDefined();
expect(workflow.jobs?.build).toBeDefined();
expect(workflow.jobs?.["test-shards"]).toBeDefined();
expect(workflow.jobs?.["test-shards"]?.strategy?.matrix?.shard).toEqual([1, 2, 3]);
expect(content).toContain("pnpm test:ci:shard --shard ${{ matrix.shard }} --total 3");
expect(content).not.toContain("run: pnpm test\n");
});
it("keeps lint as install + lint only, without Bun/setup build coupling", () => {
const lintSteps = workflow.jobs?.lint?.steps ?? [];
expect(
lintSteps.some(
(step: any) =>
typeof step.uses === "string" && step.uses.includes("./.github/actions/setup-node-pnpm"),
),
).toBe(true);
expect(
lintSteps.some((step: any) => step.name === "Lint" && typeof step.run === "string" && step.run.includes("pnpm lint")),
).toBe(true);
expect(
lintSteps.some(
(step: any) =>
step.name === "Install Bun" ||
(typeof step.uses === "string" && step.uses.includes("oven-sh/setup-bun")) ||
(typeof step.run === "string" && step.run.includes("pnpm build")),
),
).toBe(false);
});
it("keeps build coverage as an explicit PR gate", () => {
const buildSteps = workflow.jobs?.build?.steps ?? [];
expect(
buildSteps.some(
(step: any) =>
step.name === "Install Bun" ||
(typeof step.uses === "string" && step.uses.includes("oven-sh/setup-bun")),
),
).toBe(true);
expect(
buildSteps.some(
(step: any) => step.name === "Build" && typeof step.run === "string" && step.run.includes("pnpm build"),
),
).toBe(true);
});
it("does not spend PR action minutes on a pre-test workspace build", () => {
const testSteps = workflow.jobs?.["test-shards"]?.steps ?? [];
expect(
testSteps.some(
(step: any) => step.name === "Build" || (typeof step.run === "string" && step.run.includes("pnpm build")),
),
).toBe(false);
});
});
describe("Version & Release workflow (.github/workflows/version.yml)", () => {
let workflow: any;
let content: string;
beforeAll(() => {
const result = loadWorkflow("version.yml");
workflow = result.parsed;
content = result.content;
});
it("is valid YAML", () => {
expect(workflow).toBeDefined();
expect(typeof workflow).toBe("object");
});
it("uses workflow_dispatch trigger (auto release disabled)", () => {
expect(workflow.on).toHaveProperty("workflow_dispatch");
});
it("does not auto-trigger on push", () => {
expect(workflow.on.push).toBeUndefined();
});
it("pins release bootstrap to frozen lockfile", () => {
expect(content).toContain("run: pnpm install --frozen-lockfile");
expect(content).not.toContain("run: pnpm install\n");
expect(content).not.toContain("--no-frozen-lockfile");
});
it("includes pnpm build step", () => {
expect(content).toContain("pnpm build");
});
it("uses changesets/action", () => {
expect(content).toContain("changesets/action");
});
it("has publish command for npm", () => {
expect(content).toContain("pnpm -r publish");
});
it("uses OIDC publishing (no NPM_TOKEN secret)", () => {
expect(content).not.toContain("secrets.NPM_TOKEN");
expect(workflow.permissions["id-token"]).toBe("write");
});
it("has required permissions", () => {
expect(workflow.permissions.contents).toBe("write");
expect(workflow.permissions["pull-requests"]).toBe("write");
});
it("has id-token write permission for npm provenance", () => {
expect(workflow.permissions["id-token"]).toBe("write");
});
it("publishes with --provenance flag", () => {
expect(content).toContain("--provenance");
});
it("configures npm registry-url", () => {
const steps = workflow.jobs.release.steps;
const compositeStep = findCompositeSetupStep(steps);
expect(compositeStep?.with?.["registry-url"]).toBe("https://registry.npmjs.org");
});
});
describe("Binary release workflow (.github/workflows/release.yml)", () => {
let workflow: any;
let content: string;
beforeAll(() => {
const result = loadWorkflow("release.yml");
workflow = result.parsed;
content = result.content;
});
it("is valid YAML", () => {
expect(workflow).toBeDefined();
expect(typeof workflow).toBe("object");
});
it("uses workflow_dispatch trigger (auto binary release disabled)", () => {
expect(workflow.on).toHaveProperty("workflow_dispatch");
});
it("does not auto-trigger on version tags", () => {
expect(workflow.on.push).toBeUndefined();
});
it("has build-binaries job with 4-target matrix", () => {
const matrix = workflow.jobs["build-binaries"].strategy.matrix.include;
expect(matrix).toHaveLength(4);
const targets = matrix.map((m: any) => m.target);
expect(targets).toContain("bun-linux-x64");
expect(targets).toContain("bun-darwin-arm64");
expect(targets).toContain("bun-darwin-x64");
expect(targets).toContain("bun-windows-x64");
});
it("has correct OS runners for each target", () => {
const matrix = workflow.jobs["build-binaries"].strategy.matrix.include;
const osMap: Record<string, string> = {};
matrix.forEach((m: any) => { osMap[m.target] = m.os; });
expect(osMap["bun-linux-x64"]).toBe("ubuntu-latest");
expect(osMap["bun-darwin-arm64"]).toBe("macos-latest");
expect(osMap["bun-darwin-x64"]).toBe("macos-13");
expect(osMap["bun-windows-x64"]).toBe("windows-latest");
});
it("uses softprops/action-gh-release", () => {
expect(content).toContain("softprops/action-gh-release");
});
it("uses frozen-lockfile install in every matrix job", () => {
const steps = workflow.jobs["build-binaries"].steps ?? [];
const setupSteps = steps.filter((step: any) => step.uses === "./.github/actions/setup-node-pnpm");
const hasValidCompositeSetup = setupSteps.some((step: any) => {
const installArgs = step.with?.["install-args"];
return installArgs === undefined || String(installArgs).trim() === "--frozen-lockfile";
});
const hasInlineFrozenInstall = steps.some((step: any) =>
typeof step.run === "string" && /\bpnpm install --frozen-lockfile\b/.test(step.run),
);
expect(hasValidCompositeSetup || hasInlineFrozenInstall).toBe(true);
for (const step of setupSteps) {
const installArgs = step.with?.["install-args"];
if (installArgs !== undefined) {
expect(String(installArgs).trim()).toBe("--frozen-lockfile");
}
}
expect(content).not.toMatch(/run:\s*pnpm install\s*(?:\r?\n)/);
expect(content).not.toContain("--no-frozen-lockfile");
expect(content).not.toMatch(/install-args:\s*["']?\s*["']?\s*(?:\r?\n)/);
});
it("references signing scripts", () => {
expect(content).toContain("scripts/sign-macos.sh");
expect(content).toContain("scripts/sign-windows.ps1");
});
it("generates checksums on all platforms", () => {
expect(content).toContain("sha256sum");
expect(content).toContain("shasum -a 256");
expect(content).toContain("Get-FileHash");
});
it("has contents: write permission", () => {
expect(workflow.permissions.contents).toBe("write");
});
it("has github-release job that depends on build-binaries", () => {
expect(workflow.jobs["github-release"].needs).toContain("build-binaries");
});
});
describe("Test-release workflow (.github/workflows/test-release.yml)", () => {
let workflow: any;
let content: string;
beforeAll(() => {
const result = loadWorkflow("test-release.yml");
workflow = result.parsed;
content = result.content;
});
it("is valid YAML", () => {
expect(workflow).toBeDefined();
expect(typeof workflow).toBe("object");
});
it("has workflow_dispatch trigger", () => {
expect(workflow.on).toHaveProperty("workflow_dispatch");
});
it("has 4-target build matrix", () => {
const matrix = workflow.jobs["build-binaries"].strategy.matrix.include;
expect(matrix).toHaveLength(4);
const targets = matrix.map((m: any) => m.target);
expect(targets).toContain("bun-linux-x64");
expect(targets).toContain("bun-darwin-arm64");
expect(targets).toContain("bun-darwin-x64");
expect(targets).toContain("bun-windows-x64");
});
it("includes smoke tests with --help", () => {
expect(content).toContain("--help");
});
it("has signing steps with secret-availability guards", () => {
expect(content).toContain("APPLE_CERTIFICATE_BASE64 != ''");
expect(content).toContain("WINDOWS_CERTIFICATE_BASE64 != ''");
});
it("uses frozen-lockfile install in every matrix job", () => {
const steps = workflow.jobs["build-binaries"].steps ?? [];
const compositeStep = findCompositeSetupStep(steps);
expect(compositeStep).toBeDefined();
expect(compositeStep.with?.["install-args"] ?? "--frozen-lockfile").toBe("--frozen-lockfile");
expect(content).not.toContain("run: pnpm install\n");
expect(content).not.toContain("--no-frozen-lockfile");
});
it("uploads artifacts", () => {
expect(content).toContain("actions/upload-artifact");
});
it("has a collect job that combines artifacts", () => {
expect(workflow.jobs.collect).toBeDefined();
expect(workflow.jobs.collect.needs).toContain("build-binaries");
expect(content).toContain("all-binaries");
});
});
describe("Code signing — Release workflow secrets", () => {
let content: string;
beforeAll(() => {
const result = loadWorkflow("release.yml");
content = result.content;
});
it("references macOS signing secrets", () => {
expect(content).toContain("secrets.APPLE_CERTIFICATE_BASE64");
expect(content).toContain("secrets.APPLE_CERTIFICATE_PASSWORD");
expect(content).toContain("secrets.APPLE_IDENTITY");
expect(content).toContain("secrets.APPLE_ID");
expect(content).toContain("secrets.APPLE_TEAM_ID");
expect(content).toContain("secrets.APPLE_APP_PASSWORD");
});
it("references Windows signing secrets", () => {
expect(content).toContain("secrets.WINDOWS_CERTIFICATE_BASE64");
expect(content).toContain("secrets.WINDOWS_CERTIFICATE_PASSWORD");
});
it("generates checksums after signing", () => {
const signMacIdx = content.indexOf("Sign macOS binary");
const signWinIdx = content.indexOf("Sign Windows binary");
const checksumLinuxIdx = content.indexOf("Generate checksum (Linux)");
const checksumMacIdx = content.indexOf("Generate checksum (macOS)");
const checksumWinIdx = content.indexOf("Generate checksum (Windows)");
// All checksum steps come after all signing steps
expect(checksumLinuxIdx).toBeGreaterThan(signMacIdx);
expect(checksumLinuxIdx).toBeGreaterThan(signWinIdx);
expect(checksumMacIdx).toBeGreaterThan(signMacIdx);
expect(checksumWinIdx).toBeGreaterThan(signWinIdx);
});
});
describe("Code signing — Scripts", () => {
const scriptsDir = join(workspaceRoot, "scripts");
it("sign-macos.sh exists and is executable", () => {
const scriptPath = join(scriptsDir, "sign-macos.sh");
expect(() => accessSync(scriptPath, constants.F_OK)).not.toThrow();
expect(() => accessSync(scriptPath, constants.X_OK)).not.toThrow();
});
it("sign-windows.ps1 exists", () => {
const scriptPath = join(scriptsDir, "sign-windows.ps1");
expect(() => accessSync(scriptPath, constants.F_OK)).not.toThrow();
});
it("sign-macos.sh references codesign, notarytool, and security import", () => {
const script = readFileSync(join(scriptsDir, "sign-macos.sh"), "utf-8");
expect(script).toContain("codesign");
expect(script).toContain("notarytool");
expect(script).toContain("security import");
});
it("sign-windows.ps1 references signtool", () => {
const script = readFileSync(join(scriptsDir, "sign-windows.ps1"), "utf-8");
expect(script).toContain("signtool");
});
});