test(FN-2343): harden build-output contract coverage

- Add shared test setup helpers that always build dashboard client assets before assertions run
- Remove skip-gated build-output tests and make CLI/dashboard suites deterministic by owning artifact setup
- Enforce hashed vendor chunk naming checks for vendor-react and vendor-xterm in generated assets
- Verify copied CLI client index references real built chunks and does not contain the dashboard stub marker
This commit is contained in:
Fusion
2026-04-23 13:29:23 -07:00
committed by gsxdsm
parent 233d8abef9
commit 18b2c827a0
4 changed files with 95 additions and 29 deletions

View File

@@ -0,0 +1,31 @@
import { execSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { join } from "node:path";
export const cliRoot = join(__dirname, "..", "..");
export const workspaceRoot = join(cliRoot, "..", "..");
export const bundlePath = join(cliRoot, "dist", "bin.js");
export const clientIndexPath = join(cliRoot, "dist", "client", "index.html");
export const dashboardClientStubMarker = "Dashboard assets not built";
function runBuildCommand(command: string, cwd: string) {
execSync(command, {
cwd,
stdio: "pipe",
timeout: 240_000,
});
}
/**
* This suite verifies real copied dashboard client assets in CLI dist output.
* It must build those assets explicitly instead of skip-gating on ambient dist/.
*/
export function buildCliWithRealDashboardAssets() {
runBuildCommand("pnpm --filter @fusion/dashboard build:client", workspaceRoot);
runBuildCommand("pnpm build", cliRoot);
}
export function readClientIndexHtml() {
return readFileSync(clientIndexPath, "utf-8");
}

View File

@@ -1,26 +1,21 @@
import { describe, it, expect, beforeAll } from "vitest"; import { describe, it, expect, beforeAll } from "vitest";
import { execSync } from "node:child_process";
import { readFileSync, existsSync, readdirSync } from "node:fs"; import { readFileSync, existsSync, readdirSync } from "node:fs";
import { join } from "node:path"; import { join } from "node:path";
import {
buildCliWithRealDashboardAssets,
bundlePath,
cliRoot,
clientIndexPath,
dashboardClientStubMarker,
readClientIndexHtml,
} from "./bundle-output-helpers";
const cliRoot = join(__dirname, "..", "..");
const bundlePath = join(cliRoot, "dist", "bin.js");
const clientIndexPath = join(cliRoot, "dist", "client", "index.html");
const tsupConfigPath = join(cliRoot, "tsup.config.ts"); const tsupConfigPath = join(cliRoot, "tsup.config.ts");
const clientDirExists = existsSync(clientIndexPath);
describe("CLI bundle output", () => { describe("CLI bundle output", () => {
beforeAll(() => { beforeAll(() => {
if (existsSync(bundlePath)) { buildCliWithRealDashboardAssets();
return; }, 300_000);
}
execSync("pnpm build", {
cwd: cliRoot,
stdio: "pipe",
timeout: 120_000,
});
}, 180_000);
it("dist/bin.js exists", () => { it("dist/bin.js exists", () => {
expect(existsSync(bundlePath)).toBe(true); expect(existsSync(bundlePath)).toBe(true);
@@ -46,8 +41,19 @@ describe("CLI bundle output", () => {
expect(content).toContain("createServer"); expect(content).toContain("createServer");
}); });
it.skipIf(!clientDirExists)("dashboard client assets are included", () => { it("dashboard client assets are included", () => {
expect(existsSync(clientIndexPath)).toBe(true); expect(existsSync(clientIndexPath)).toBe(true);
const indexHtml = readClientIndexHtml();
expect(indexHtml).toContain("<script");
expect(indexHtml).toMatch(/assets\/.+-[A-Za-z0-9_-]+\.js/);
expect(indexHtml).toMatch(/assets\/vendor-react-[A-Za-z0-9_-]+\.js/);
expect(indexHtml).not.toContain(dashboardClientStubMarker);
const copiedAssetsDir = join(cliRoot, "dist", "client", "assets");
const copiedAssets = readdirSync(copiedAssetsDir);
expect(copiedAssets.some((file) => /^vendor-react-[A-Za-z0-9_-]+\.js$/.test(file))).toBe(true);
expect(copiedAssets.some((file) => /^vendor-xterm-[A-Za-z0-9_-]+\.js$/.test(file))).toBe(true);
}); });
it("tsup config copies dashboard assets from dashboard/dist/client to dist/client", () => { it("tsup config copies dashboard assets from dashboard/dist/client to dist/client", () => {
@@ -69,7 +75,7 @@ describe("CLI bundle output", () => {
it("provides require via createRequire banner", () => { it("provides require via createRequire banner", () => {
const content = readFileSync(bundlePath, "utf-8"); const content = readFileSync(bundlePath, "utf-8");
// Banner should inject createRequire for ESM CJS interop // Banner should inject createRequire for ESM CJS interop
expect(content).toContain('createRequire'); expect(content).toContain("createRequire");
expect(content).toContain("import.meta.url"); expect(content).toContain("import.meta.url");
// Banner should be near the top of the file (after shebang) // Banner should be near the top of the file (after shebang)
const shebangEnd = content.indexOf("\n"); const shebangEnd = content.indexOf("\n");

View File

@@ -0,0 +1,24 @@
import { execSync } from "node:child_process";
import { existsSync } from "node:fs";
import { resolve } from "node:path";
const dashboardRoot = resolve(__dirname, "../..");
export const dashboardClientDistDir = resolve(dashboardRoot, "dist/client");
export const dashboardClientAssetsDir = resolve(dashboardClientDistDir, "assets");
/**
* Build output verification must be deterministic: the suite owns artifact setup
* and never skip-gates coverage based on whatever dist/ state already exists.
*/
export function ensureDashboardClientBuild() {
execSync("pnpm build:client", {
cwd: dashboardRoot,
stdio: "pipe",
timeout: 180_000,
});
if (!existsSync(dashboardClientDistDir) || !existsSync(dashboardClientAssetsDir)) {
throw new Error("Dashboard client build did not produce dist/client assets");
}
}

View File

@@ -1,23 +1,28 @@
import { existsSync, readdirSync, readFileSync } from "node:fs"; import { readdirSync, readFileSync } from "node:fs";
import { resolve } from "node:path"; import { resolve } from "node:path";
import { describe, expect, test } from "vitest"; import { beforeAll, describe, expect, test } from "vitest";
import {
const distDir = resolve(__dirname, "../../dist/client"); dashboardClientAssetsDir,
const assetsDir = resolve(distDir, "assets"); dashboardClientDistDir,
const distExists = existsSync(distDir) && existsSync(assetsDir); ensureDashboardClientBuild,
} from "./build-output-setup";
describe("mobile build output chunking", () => { describe("mobile build output chunking", () => {
test.skipIf(!distExists)("creates vendor chunk files for core dependencies", () => { beforeAll(() => {
const files = readdirSync(assetsDir); ensureDashboardClientBuild();
}, 180_000);
test("creates vendor chunk files for core dependencies", () => {
const files = readdirSync(dashboardClientAssetsDir);
const jsFiles = files.filter((file) => file.endsWith(".js")); const jsFiles = files.filter((file) => file.endsWith(".js"));
expect(jsFiles.length).toBeGreaterThan(2); expect(jsFiles.length).toBeGreaterThan(2);
expect(jsFiles.some((file) => file.includes("vendor-react"))).toBe(true); expect(jsFiles.some((file) => /^vendor-react-[A-Za-z0-9_-]+\.js$/.test(file))).toBe(true);
expect(jsFiles.some((file) => file.includes("vendor-xterm"))).toBe(true); expect(jsFiles.some((file) => /^vendor-xterm-[A-Za-z0-9_-]+\.js$/.test(file))).toBe(true);
}); });
test.skipIf(!distExists)("index.html references chunked asset scripts", () => { test("index.html references chunked asset scripts", () => {
const indexHtml = readFileSync(resolve(distDir, "index.html"), "utf8"); const indexHtml = readFileSync(resolve(dashboardClientDistDir, "index.html"), "utf8");
expect(indexHtml).toContain("<script"); expect(indexHtml).toContain("<script");
expect(indexHtml).toMatch(/assets\/.+-[A-Za-z0-9_-]+\.js/); expect(indexHtml).toMatch(/assets\/.+-[A-Za-z0-9_-]+\.js/);