fix(FN-2360): harden CLI vitest workspace source aliasing
- Convert internal @fusion workspace aliases in packages/cli/vitest.config.ts to exact anchored regex entrypoint mappings - Preserve subpath-before-root alias order so @fusion/core/gh-cli and @fusion/dashboard/planning resolve correctly - Add vitest-workspace-resolution regression coverage for alias definitions and ordering - Simulate clean worktrees by temporarily hiding internal dist/ directories and verify dynamic imports resolve from source
This commit is contained in:
42
.github/workflows/pr-checks.yml
vendored
Normal file
42
.github/workflows/pr-checks.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
name: PR Checks
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: pr-checks-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
checks:
|
||||||
|
name: Lint, Typecheck, Test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v5
|
||||||
|
with:
|
||||||
|
node-version: "24"
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Install Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: pnpm lint
|
||||||
|
|
||||||
|
- name: Typecheck
|
||||||
|
run: pnpm typecheck
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: pnpm test
|
||||||
@@ -182,7 +182,7 @@ describe("build-exe", () => {
|
|||||||
reject(
|
reject(
|
||||||
new Error(`Server startup timeout\nOutput:\n${startupOutput}`),
|
new Error(`Server startup timeout\nOutput:\n${startupOutput}`),
|
||||||
);
|
);
|
||||||
}, 10_000);
|
}, 30_000);
|
||||||
|
|
||||||
const settle = (
|
const settle = (
|
||||||
result: "ready" | "sqlite-unsupported" | Error,
|
result: "ready" | "sqlite-unsupported" | Error,
|
||||||
@@ -303,5 +303,5 @@ describe("build-exe", () => {
|
|||||||
await stopChildProcess(child);
|
await stopChildProcess(child);
|
||||||
cleanup();
|
cleanup();
|
||||||
}
|
}
|
||||||
}, 20_000);
|
}, 60_000);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||||
|
import { existsSync, renameSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import vitestConfig from "../../vitest.config";
|
||||||
|
|
||||||
|
const cliRoot = join(__dirname, "..", "..");
|
||||||
|
const workspaceRoot = join(cliRoot, "..", "..");
|
||||||
|
|
||||||
|
const internalPackages = ["core", "engine", "dashboard"] as const;
|
||||||
|
const movedDistDirs: Array<{ from: string; to: string }> = [];
|
||||||
|
|
||||||
|
function hideInternalPackageDistDirs() {
|
||||||
|
for (const pkg of internalPackages) {
|
||||||
|
const distPath = join(workspaceRoot, "packages", pkg, "dist");
|
||||||
|
if (!existsSync(distPath)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hiddenPath = `${distPath}.__fn2360-hidden-${process.pid}`;
|
||||||
|
renameSync(distPath, hiddenPath);
|
||||||
|
movedDistDirs.push({ from: distPath, to: hiddenPath });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreInternalPackageDistDirs() {
|
||||||
|
for (let i = movedDistDirs.length - 1; i >= 0; i--) {
|
||||||
|
const { from, to } = movedDistDirs[i];
|
||||||
|
if (existsSync(to)) {
|
||||||
|
renameSync(to, from);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
movedDistDirs.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("CLI Vitest workspace resolution", () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
hideInternalPackageDistDirs();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
restoreInternalPackageDistDirs();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses exact source-entry aliases for internal workspace packages", () => {
|
||||||
|
const aliases = vitestConfig.resolve?.alias;
|
||||||
|
expect(Array.isArray(aliases)).toBe(true);
|
||||||
|
|
||||||
|
const normalized = (aliases ?? []).map((entry) => ({
|
||||||
|
find: String(entry.find),
|
||||||
|
replacement: String(entry.replacement),
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(normalized).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{
|
||||||
|
find: String(/^@fusion\/core\/gh-cli$/),
|
||||||
|
replacement: join(workspaceRoot, "packages", "core", "src", "gh-cli.ts"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: String(/^@fusion\/core$/),
|
||||||
|
replacement: join(workspaceRoot, "packages", "core", "src", "index.ts"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: String(/^@fusion\/engine$/),
|
||||||
|
replacement: join(workspaceRoot, "packages", "engine", "src", "index.ts"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: String(/^@fusion\/dashboard$/),
|
||||||
|
replacement: join(workspaceRoot, "packages", "dashboard", "src", "index.ts"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: String(/^@fusion\/dashboard\/planning$/),
|
||||||
|
replacement: join(workspaceRoot, "packages", "dashboard", "src", "planning.ts"),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const coreGhCliIndex = normalized.findIndex((entry) => entry.find === String(/^@fusion\/core\/gh-cli$/));
|
||||||
|
const coreIndex = normalized.findIndex((entry) => entry.find === String(/^@fusion\/core$/));
|
||||||
|
const planningIndex = normalized.findIndex((entry) => entry.find === String(/^@fusion\/dashboard\/planning$/));
|
||||||
|
const dashboardIndex = normalized.findIndex((entry) => entry.find === String(/^@fusion\/dashboard$/));
|
||||||
|
|
||||||
|
expect(coreGhCliIndex).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(coreIndex).toBeGreaterThan(coreGhCliIndex);
|
||||||
|
expect(planningIndex).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(dashboardIndex).toBeGreaterThan(planningIndex);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves internal workspace imports during CLI tests when dist outputs are absent", async () => {
|
||||||
|
await expect(import("@fusion/core")).resolves.toBeTruthy();
|
||||||
|
await expect(import("@fusion/core/gh-cli")).resolves.toBeTruthy();
|
||||||
|
await expect(import("@fusion/engine")).resolves.toBeTruthy();
|
||||||
|
await expect(import("@fusion/dashboard")).resolves.toBeTruthy();
|
||||||
|
await expect(import("@fusion/dashboard/planning")).resolves.toBeTruthy();
|
||||||
|
}, 30_000);
|
||||||
|
});
|
||||||
@@ -8,14 +8,17 @@ process.env.VITEST_MAX_WORKERS = String(maxWorkers);
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
resolve: {
|
resolve: {
|
||||||
// Use ordered aliases so the subpath match is resolved before @fusion/core.
|
// Keep these aliases exact and ordered (subpaths before package roots).
|
||||||
|
// In fresh worktrees, internal packages may not have dist/ built yet, and
|
||||||
|
// Vite otherwise resolves workspace package exports.import to dist/*.js.
|
||||||
|
// Anchored regex aliases force CLI tests to use source entrypoints instead.
|
||||||
alias: [
|
alias: [
|
||||||
{ find: "@fusion/core/gh-cli", replacement: resolve(__dirname, "../core/src/gh-cli.ts") },
|
{ find: /^@fusion\/core\/gh-cli$/, replacement: resolve(__dirname, "../core/src/gh-cli.ts") },
|
||||||
{ find: "@fusion/core", replacement: resolve(__dirname, "../core/src/index.ts") },
|
{ find: /^@fusion\/core$/, replacement: resolve(__dirname, "../core/src/index.ts") },
|
||||||
{ find: "@fusion/dashboard/planning", replacement: resolve(__dirname, "../dashboard/src/planning.ts") },
|
{ find: /^@fusion\/dashboard\/planning$/, replacement: resolve(__dirname, "../dashboard/src/planning.ts") },
|
||||||
{ find: "@fusion/dashboard", replacement: resolve(__dirname, "../dashboard/src/index.ts") },
|
{ find: /^@fusion\/dashboard$/, replacement: resolve(__dirname, "../dashboard/src/index.ts") },
|
||||||
{ find: "@fusion/engine", replacement: resolve(__dirname, "../engine/src/index.ts") },
|
{ find: /^@fusion\/engine$/, replacement: resolve(__dirname, "../engine/src/index.ts") },
|
||||||
{ find: "@fusion/test-utils", replacement: resolve(__dirname, "../core/src/__test-utils__/workspace.ts") },
|
{ find: /^@fusion\/test-utils$/, replacement: resolve(__dirname, "../core/src/__test-utils__/workspace.ts") },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
test: {
|
test: {
|
||||||
|
|||||||
Reference in New Issue
Block a user