feat(FN-4605): complete Step 2 — expand artifact registry and remediation details
Fusion-Task-Id: FN-4605 Fusion-Task-Lineage: 472b0ccb-d015-4ff7-bdf2-1d5d05ae09e9
This commit is contained in:
@@ -58,6 +58,51 @@ test("ensureTestArtifacts rebuilds @fusion/dashboard when its dist is missing",
|
||||
assert.deepEqual(calls[0].args, ["--filter", "@fusion/dashboard", "build"]);
|
||||
});
|
||||
|
||||
test("detectMissingArtifacts flags @fusion/engine when dist/index.js is missing", () => {
|
||||
const missing = detectMissingArtifacts("/repo", (fullPath) => !fullPath.endsWith("packages/engine/dist/index.js"));
|
||||
const names = missing.map((pkg) => pkg.name);
|
||||
|
||||
assert.ok(names.includes("@fusion/engine"));
|
||||
});
|
||||
|
||||
test("ensureTestArtifacts rebuilds @fusion/engine when dist is missing", () => {
|
||||
const calls = [];
|
||||
const built = ensureTestArtifacts(
|
||||
"/repo",
|
||||
(cmd, args, cwd) => calls.push({ cmd, args, cwd }),
|
||||
(fullPath) => !fullPath.endsWith("packages/engine/dist/index.js"),
|
||||
);
|
||||
|
||||
assert.deepEqual(built, ["@fusion/engine"]);
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0].cmd, "pnpm");
|
||||
assert.deepEqual(calls[0].args, ["--filter", "@fusion/engine", "build"]);
|
||||
});
|
||||
|
||||
test("detectMissingArtifacts flags dependency-graph when dist/dashboard-view.js is missing", () => {
|
||||
const missing = detectMissingArtifacts(
|
||||
"/repo",
|
||||
(fullPath) => !fullPath.endsWith("plugins/fusion-plugin-dependency-graph/dist/dashboard-view.js"),
|
||||
);
|
||||
const names = missing.map((pkg) => pkg.name);
|
||||
|
||||
assert.ok(names.includes("@fusion-plugin-examples/dependency-graph"));
|
||||
});
|
||||
|
||||
test("ensureTestArtifacts rebuilds dependency-graph for incomplete dist artifacts", () => {
|
||||
const calls = [];
|
||||
const built = ensureTestArtifacts(
|
||||
"/repo",
|
||||
(cmd, args, cwd) => calls.push({ cmd, args, cwd }),
|
||||
(fullPath) => !fullPath.endsWith("plugins/fusion-plugin-dependency-graph/dist/dashboard-view.js"),
|
||||
);
|
||||
|
||||
assert.deepEqual(built, ["@fusion-plugin-examples/dependency-graph"]);
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0].cmd, "pnpm");
|
||||
assert.deepEqual(calls[0].args, ["--filter", "@fusion-plugin-examples/dependency-graph", "build"]);
|
||||
});
|
||||
|
||||
test("detectMissingArtifacts flags hermes when dist/index.js exists but dist/cli-spawn.js is missing", () => {
|
||||
const missing = detectMissingArtifacts("/repo", (fullPath) => !fullPath.endsWith("dist/cli-spawn.js"));
|
||||
const names = missing.map((pkg) => pkg.name);
|
||||
@@ -103,12 +148,11 @@ test("ensureTestArtifacts rebuilds openclaw for incomplete dist artifacts", () =
|
||||
assert.deepEqual(calls[0].args, ["--filter", "@fusion-plugin-examples/openclaw-runtime", "build"]);
|
||||
});
|
||||
|
||||
function createStaleFs(pluginName, { artifactMtime = 1000, sourceMtime = 2000 } = {}) {
|
||||
const sourceDir = `/repo/plugins/${pluginName}/src`;
|
||||
function createStaleFsForPackage({ sourceDir, artifactPathFragment }, { artifactMtime = 1000, sourceMtime = 2000 } = {}) {
|
||||
const sourceFile = `${sourceDir}/index.ts`;
|
||||
|
||||
const statFn = (fullPath) => {
|
||||
if (fullPath.includes("/dist/")) return { mtimeMs: artifactMtime };
|
||||
if (fullPath.includes(artifactPathFragment)) return { mtimeMs: artifactMtime };
|
||||
if (fullPath === sourceFile) return { mtimeMs: sourceMtime };
|
||||
return { mtimeMs: 0 };
|
||||
};
|
||||
@@ -123,6 +167,12 @@ function createStaleFs(pluginName, { artifactMtime = 1000, sourceMtime = 2000 }
|
||||
return { statFn, readdirFn };
|
||||
}
|
||||
|
||||
function createStaleFs(pluginName, { artifactMtime = 1000, sourceMtime = 2000 } = {}) {
|
||||
const sourceDir = `/repo/plugins/${pluginName}/src`;
|
||||
|
||||
return createStaleFsForPackage({ sourceDir, artifactPathFragment: "/dist/" }, { artifactMtime, sourceMtime });
|
||||
}
|
||||
|
||||
test("detectMissingOrStaleArtifacts returns hermes when dist artifact is older than src", () => {
|
||||
const { statFn, readdirFn } = createStaleFs("fusion-plugin-hermes-runtime", {
|
||||
artifactMtime: 1000,
|
||||
@@ -159,6 +209,29 @@ test("detectMissingOrStaleArtifacts covers all example plugins for staleness", a
|
||||
}
|
||||
});
|
||||
|
||||
test("detectMissingOrStaleArtifacts flags @fusion/engine when dist artifact is older than src", () => {
|
||||
const { statFn, readdirFn } = createStaleFsForPackage(
|
||||
{ sourceDir: "/repo/packages/engine/src", artifactPathFragment: "packages/engine/dist/" },
|
||||
{ artifactMtime: 1000, sourceMtime: 3000 },
|
||||
);
|
||||
|
||||
const result = detectMissingOrStaleArtifacts("/repo", () => true, statFn, readdirFn);
|
||||
assert.ok(result.some((pkg) => pkg.name === "@fusion/engine"));
|
||||
});
|
||||
|
||||
test("detectMissingOrStaleArtifacts flags dependency-graph when dist artifact is older than src", () => {
|
||||
const { statFn, readdirFn } = createStaleFsForPackage(
|
||||
{
|
||||
sourceDir: "/repo/plugins/fusion-plugin-dependency-graph/src",
|
||||
artifactPathFragment: "plugins/fusion-plugin-dependency-graph/dist/",
|
||||
},
|
||||
{ artifactMtime: 1000, sourceMtime: 3000 },
|
||||
);
|
||||
|
||||
const result = detectMissingOrStaleArtifacts("/repo", () => true, statFn, readdirFn);
|
||||
assert.ok(result.some((pkg) => pkg.name === "@fusion-plugin-examples/dependency-graph"));
|
||||
});
|
||||
|
||||
test("detectMissingOrStaleArtifacts merges missing and stale results without duplicates", () => {
|
||||
const { statFn, readdirFn } = createStaleFs("fusion-plugin-hermes-runtime", {
|
||||
artifactMtime: 1000,
|
||||
@@ -210,7 +283,7 @@ test("ensureTestArtifacts invokes rebuild command for stale package", () => {
|
||||
assert.deepEqual(calls[0].args, ["--filter", "@fusion-plugin-examples/hermes-runtime", "build"]);
|
||||
});
|
||||
|
||||
test("ensureTestArtifacts writes FN-4232 remediation block to stderr on rebuild failure", () => {
|
||||
test("ensureTestArtifacts writes detailed FN-4232/FN-4605 remediation block to stderr on rebuild failure", () => {
|
||||
const { statFn, readdirFn } = createStaleFs("fusion-plugin-hermes-runtime", {
|
||||
artifactMtime: 1000,
|
||||
sourceMtime: 3000,
|
||||
@@ -240,6 +313,35 @@ test("ensureTestArtifacts writes FN-4232 remediation block to stderr on rebuild
|
||||
assert.ok(built.includes("@fusion-plugin-examples/hermes-runtime"));
|
||||
assert.equal(exitCode, 2);
|
||||
assert.match(stderr, /@fusion-plugin-examples\/hermes-runtime/);
|
||||
assert.match(stderr, /\[test-bootstrap\] stale \(src newer than dist\): plugins\/fusion-plugin-hermes-runtime\/dist\/index.js/);
|
||||
assert.match(stderr, /\[test-bootstrap\] stale \(src newer than dist\): plugins\/fusion-plugin-hermes-runtime\/dist\/cli-spawn.js/);
|
||||
assert.match(stderr, /pnpm install --frozen-lockfile/);
|
||||
assert.match(stderr, /FN-4232/);
|
||||
assert.match(stderr, /FN-4232, FN-4605/);
|
||||
});
|
||||
|
||||
test("ensureTestArtifacts remediation labels missing artifact paths", () => {
|
||||
let stderr = "";
|
||||
let exitCode = null;
|
||||
|
||||
const built = ensureTestArtifacts(
|
||||
"/repo",
|
||||
undefined,
|
||||
(fullPath) => !fullPath.endsWith("plugins/fusion-plugin-dependency-graph/dist/dashboard-view.js"),
|
||||
() => ({ mtimeMs: 1_000 }),
|
||||
() => [],
|
||||
{
|
||||
spawnFn: () => ({ status: 3 }),
|
||||
exitFn: (code) => {
|
||||
exitCode = code;
|
||||
},
|
||||
stderrWrite: (chunk) => {
|
||||
stderr += String(chunk);
|
||||
return true;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
assert.ok(built.includes("@fusion-plugin-examples/dependency-graph"));
|
||||
assert.equal(exitCode, 3);
|
||||
assert.match(stderr, /\[test-bootstrap\] missing: plugins\/fusion-plugin-dependency-graph\/dist\/dashboard-view.js/);
|
||||
});
|
||||
|
||||
@@ -7,7 +7,20 @@ import { spawnSync } from "node:child_process";
|
||||
export const REQUIRED_BUILD_PACKAGES = [
|
||||
{ name: "@fusion/core", requiredArtifacts: ["packages/core/dist/index.js"] },
|
||||
{ name: "@fusion/dashboard", requiredArtifacts: ["packages/dashboard/dist/index.js"] },
|
||||
{
|
||||
name: "@fusion/engine",
|
||||
requiredArtifacts: ["packages/engine/dist/index.js"],
|
||||
staleAgainstGlobs: [{ sourcePath: "packages/engine/src" }],
|
||||
},
|
||||
{ name: "@fusion/plugin-sdk", requiredArtifacts: ["packages/plugin-sdk/dist/index.js"] },
|
||||
{
|
||||
name: "@fusion-plugin-examples/dependency-graph",
|
||||
requiredArtifacts: [
|
||||
"plugins/fusion-plugin-dependency-graph/dist/index.js",
|
||||
"plugins/fusion-plugin-dependency-graph/dist/dashboard-view.js",
|
||||
],
|
||||
staleAgainstGlobs: [{ sourcePath: "plugins/fusion-plugin-dependency-graph/src" }],
|
||||
},
|
||||
{
|
||||
name: "@fusion-plugin-examples/hermes-runtime",
|
||||
requiredArtifacts: [
|
||||
@@ -118,15 +131,35 @@ export function detectMissingArtifacts(rootDir = process.cwd(), existsFn = exist
|
||||
return detectMissingOrStaleArtifacts(rootDir, existsFn, statFn, readdirFn);
|
||||
}
|
||||
|
||||
function writeRemediation(stderrWrite, pkgNames, filterCommand) {
|
||||
function classifyArtifactIssues(pkgEntry, rootDir, existsFn, statFn, readdirFn) {
|
||||
const missingPaths = pkgEntry.requiredArtifacts.filter((artifactPath) => !existsFn(path.join(rootDir, artifactPath)));
|
||||
if (missingPaths.length > 0) {
|
||||
return { missingPaths, stalePaths: [] };
|
||||
}
|
||||
if (isStale(pkgEntry, rootDir, statFn, readdirFn, existsFn)) {
|
||||
return { missingPaths: [], stalePaths: [...pkgEntry.requiredArtifacts] };
|
||||
}
|
||||
return { missingPaths: [], stalePaths: [] };
|
||||
}
|
||||
|
||||
function writeRemediation(stderrWrite, pkgEntries, filterCommand, rootDir, existsFn = existsSync, statFn = statSync, readdirFn = readdirSync) {
|
||||
stderrWrite("\n[test-bootstrap] FAILED: workspace dist artifact rebuild did not complete.\n");
|
||||
stderrWrite(`[test-bootstrap] command: ${filterCommand}\n`);
|
||||
stderrWrite(`[test-bootstrap] affected packages: ${pkgNames.join(", ")}\n`);
|
||||
stderrWrite(`[test-bootstrap] affected packages: ${pkgEntries.map((pkg) => pkg.name).join(", ")}\n`);
|
||||
for (const pkgEntry of pkgEntries) {
|
||||
const { missingPaths, stalePaths } = classifyArtifactIssues(pkgEntry, rootDir, existsFn, statFn, readdirFn);
|
||||
for (const missingPath of missingPaths) {
|
||||
stderrWrite(`[test-bootstrap] missing: ${missingPath}\n`);
|
||||
}
|
||||
for (const stalePath of stalePaths) {
|
||||
stderrWrite(`[test-bootstrap] stale (src newer than dist): ${stalePath}\n`);
|
||||
}
|
||||
}
|
||||
stderrWrite("[test-bootstrap] next steps:\n");
|
||||
stderrWrite(" 1) pnpm install --frozen-lockfile\n");
|
||||
stderrWrite(" 2) pnpm --filter <pkg> build\n");
|
||||
stderrWrite(" 3) delete <plugin>/dist and re-run pnpm test\n");
|
||||
stderrWrite("[test-bootstrap] reference: FN-4232\n\n");
|
||||
stderrWrite("[test-bootstrap] reference: FN-4232, FN-4605\n\n");
|
||||
}
|
||||
|
||||
function run(
|
||||
@@ -137,13 +170,20 @@ function run(
|
||||
exitFn = process.exit,
|
||||
stderrWrite = process.stderr.write.bind(process.stderr),
|
||||
spawnFn = spawnSync,
|
||||
pkgEntries = [],
|
||||
existsFn = existsSync,
|
||||
statFn = statSync,
|
||||
readdirFn = readdirSync,
|
||||
} = {},
|
||||
) {
|
||||
const result = spawnFn(command, args, { cwd, stdio: "inherit" });
|
||||
if (result.status !== 0) {
|
||||
const filterCommand = `${command} ${args.join(" ")}`;
|
||||
const packageNames = args.filter((entry, index) => args[index - 1] === "--filter");
|
||||
writeRemediation(stderrWrite, packageNames, filterCommand);
|
||||
const packagesToReport = pkgEntries.length > 0
|
||||
? pkgEntries
|
||||
: REQUIRED_BUILD_PACKAGES.filter((pkg) => packageNames.includes(pkg.name));
|
||||
writeRemediation(stderrWrite, packagesToReport, filterCommand, cwd, existsFn, statFn, readdirFn);
|
||||
exitFn(result.status ?? 1);
|
||||
}
|
||||
}
|
||||
@@ -162,7 +202,13 @@ export function ensureTestArtifacts(
|
||||
const names = missingOrStale.map((pkg) => pkg.name);
|
||||
console.log(`[test-bootstrap] rebuilding workspace dist artifacts (missing or stale): ${names.join(", ")}`);
|
||||
if (runFn === run) {
|
||||
runFn("pnpm", [...names.flatMap((name) => ["--filter", name]), "build"], rootDir, runOptions);
|
||||
runFn("pnpm", [...names.flatMap((name) => ["--filter", name]), "build"], rootDir, {
|
||||
...runOptions,
|
||||
pkgEntries: missingOrStale,
|
||||
existsFn,
|
||||
statFn,
|
||||
readdirFn,
|
||||
});
|
||||
} else {
|
||||
runFn("pnpm", [...names.flatMap((name) => ["--filter", name]), "build"], rootDir);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user