test(FN-2358): harden clean-worktree CI verification tests

- Assert contributing docs explicitly state that pnpm test must run without prior build outputs
- Add CI workflow test coverage to keep docs and verify:workspace ordering aligned
- Expand Vitest workspace alias assertions to include @fusion/test-utils and src-only replacements
- Validate real symbol imports from workspace packages when dist directories are absent
This commit is contained in:
Fusion
2026-04-23 16:32:01 -07:00
committed by gsxdsm
parent 12c58e2009
commit 64d829f924
3 changed files with 43 additions and 8 deletions

View File

@@ -55,6 +55,7 @@ pnpm typecheck # workspace typechecks
Fusion codifies workspace verification as a deterministic contract: Fusion codifies workspace verification as a deterministic contract:
- `pnpm test` must be runnable in a clean worktree without requiring a prior `pnpm build`. - `pnpm test` must be runnable in a clean worktree without requiring a prior `pnpm build`.
- This includes clean states where `packages/core/dist`, `packages/engine/dist`, and `packages/dashboard/dist` are absent.
- `pnpm verify:workspace` is the canonical pre-merge gate and runs in strict order: - `pnpm verify:workspace` is the canonical pre-merge gate and runs in strict order:
1. `pnpm lint` 1. `pnpm lint`
2. `pnpm test` 2. `pnpm test`

View File

@@ -23,12 +23,14 @@ describe("CI workflow (.github/workflows/ci.yml)", () => {
let workflow: any; let workflow: any;
let content: string; let content: string;
let ciSteps: any[]; let ciSteps: any[];
let contributingContent: string;
beforeAll(() => { beforeAll(() => {
const result = loadWorkflow("ci.yml"); const result = loadWorkflow("ci.yml");
workflow = result.parsed; workflow = result.parsed;
content = result.content; content = result.content;
ciSteps = workflow.jobs?.ci?.steps ?? []; ciSteps = workflow.jobs?.ci?.steps ?? [];
contributingContent = readFileSync(join(workspaceRoot, "docs", "contributing.md"), "utf-8");
}); });
const findStepByRun = (runSnippet: string) => ciSteps.find((step) => typeof step.run === "string" && step.run.includes(runSnippet)); const findStepByRun = (runSnippet: string) => ciSteps.find((step) => typeof step.run === "string" && step.run.includes(runSnippet));
@@ -74,6 +76,14 @@ describe("CI workflow (.github/workflows/ci.yml)", () => {
expect(buildExeIdx).toBeGreaterThan(verifyIdx); expect(buildExeIdx).toBeGreaterThan(verifyIdx);
}); });
it("keeps contributing docs aligned with the clean-worktree verification contract", () => {
expect(contributingContent).toContain("pnpm test` 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`");
expect(contributingContent).toContain("3. `pnpm build`");
});
it("includes binary build step", () => { it("includes binary build step", () => {
expect(content).toContain("build:exe"); expect(content).toContain("build:exe");
}); });

View File

@@ -64,17 +64,27 @@ describe("CLI Vitest workspace resolution", () => {
find: String(/^@fusion\/engine$/), find: String(/^@fusion\/engine$/),
replacement: join(workspaceRoot, "packages", "engine", "src", "index.ts"), replacement: join(workspaceRoot, "packages", "engine", "src", "index.ts"),
}, },
{
find: String(/^@fusion\/dashboard\/planning$/),
replacement: join(workspaceRoot, "packages", "dashboard", "src", "planning.ts"),
},
{ {
find: String(/^@fusion\/dashboard$/), find: String(/^@fusion\/dashboard$/),
replacement: join(workspaceRoot, "packages", "dashboard", "src", "index.ts"), replacement: join(workspaceRoot, "packages", "dashboard", "src", "index.ts"),
}, },
{ {
find: String(/^@fusion\/dashboard\/planning$/), find: String(/^@fusion\/test-utils$/),
replacement: join(workspaceRoot, "packages", "dashboard", "src", "planning.ts"), replacement: join(workspaceRoot, "packages", "core", "src", "__test-utils__", "workspace.ts"),
}, },
]), ]),
); );
for (const entry of normalized) {
expect(entry.replacement).toContain(`${join("packages", "")}`);
expect(entry.replacement).toContain(`${join("src", "")}`);
expect(entry.replacement).not.toContain(`${join("dist", "")}`);
}
const coreGhCliIndex = normalized.findIndex((entry) => entry.find === String(/^@fusion\/core\/gh-cli$/)); const coreGhCliIndex = normalized.findIndex((entry) => entry.find === String(/^@fusion\/core\/gh-cli$/));
const coreIndex = normalized.findIndex((entry) => entry.find === String(/^@fusion\/core$/)); const coreIndex = normalized.findIndex((entry) => entry.find === String(/^@fusion\/core$/));
const planningIndex = normalized.findIndex((entry) => entry.find === String(/^@fusion\/dashboard\/planning$/)); const planningIndex = normalized.findIndex((entry) => entry.find === String(/^@fusion\/dashboard\/planning$/));
@@ -86,11 +96,25 @@ describe("CLI Vitest workspace resolution", () => {
expect(dashboardIndex).toBeGreaterThan(planningIndex); expect(dashboardIndex).toBeGreaterThan(planningIndex);
}); });
it("resolves internal workspace imports during CLI tests when dist outputs are absent", async () => { it("resolves non-mocked symbols from internal workspace packages when dist outputs are absent", async () => {
await expect(import("@fusion/core")).resolves.toBeTruthy(); const [{ tempWorkspace }, { PRIORITY_EXECUTE }, { createRuntimeLogger }, { parseAgentResponse }] = await Promise.all([
await expect(import("@fusion/core/gh-cli")).resolves.toBeTruthy(); import("@fusion/test-utils"),
await expect(import("@fusion/engine")).resolves.toBeTruthy(); import("@fusion/engine"),
await expect(import("@fusion/dashboard")).resolves.toBeTruthy(); import("@fusion/dashboard"),
await expect(import("@fusion/dashboard/planning")).resolves.toBeTruthy(); import("@fusion/dashboard/planning"),
]);
expect(typeof tempWorkspace).toBe("function");
expect(typeof PRIORITY_EXECUTE).toBe("number");
expect(typeof createRuntimeLogger).toBe("function");
expect(parseAgentResponse('{"type":"question","data":{"id":"q","type":"confirm","question":"Ok?","description":"desc"}}')).toEqual({
type: "question",
data: {
id: "q",
type: "confirm",
question: "Ok?",
description: "desc",
},
});
}, 30_000); }, 30_000);
}); });