feat(FN-4232): complete Steps 2-4 — detect stale artifacts and add remediation/tests

Fusion-Task-Id: FN-4232
Fusion-Task-Lineage: 8a1c59d6-2144-4146-99e4-201e9657af92
This commit is contained in:
Fusion
2026-05-13 22:45:29 -07:00
committed by gsxdsm
parent f81a366cdb
commit e35a38fe41
2 changed files with 269 additions and 14 deletions

View File

@@ -2,6 +2,7 @@ import test from "node:test";
import assert from "node:assert/strict";
import {
detectMissingArtifacts,
detectMissingOrStaleArtifacts,
ensureTestArtifacts,
REQUIRED_BUILD_PACKAGES,
} from "../ensure-test-artifacts.mjs";
@@ -101,3 +102,144 @@ test("ensureTestArtifacts rebuilds openclaw for incomplete dist artifacts", () =
assert.equal(calls[0].cmd, "pnpm");
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`;
const sourceFile = `${sourceDir}/index.ts`;
const statFn = (fullPath) => {
if (fullPath.includes("/dist/")) return { mtimeMs: artifactMtime };
if (fullPath === sourceFile) return { mtimeMs: sourceMtime };
return { mtimeMs: 0 };
};
const readdirFn = (dirPath) => {
if (dirPath === sourceDir) {
return [{ name: "index.ts", isDirectory: () => false }];
}
return [];
};
return { statFn, readdirFn };
}
test("detectMissingOrStaleArtifacts returns hermes when dist artifact is older than src", () => {
const { statFn, readdirFn } = createStaleFs("fusion-plugin-hermes-runtime", {
artifactMtime: 1000,
sourceMtime: 3000,
});
const result = detectMissingOrStaleArtifacts("/repo", () => true, statFn, readdirFn);
assert.ok(result.some((pkg) => pkg.name === "@fusion-plugin-examples/hermes-runtime"));
});
test("detectMissingOrStaleArtifacts does not flag hermes when dist is newer than src", () => {
const { statFn, readdirFn } = createStaleFs("fusion-plugin-hermes-runtime", {
artifactMtime: 4000,
sourceMtime: 2000,
});
const result = detectMissingOrStaleArtifacts("/repo", () => true, statFn, readdirFn);
assert.ok(!result.some((pkg) => pkg.name === "@fusion-plugin-examples/hermes-runtime"));
});
test("detectMissingOrStaleArtifacts covers all example plugins for staleness", async (t) => {
const cases = [
["fusion-plugin-hermes-runtime", "@fusion-plugin-examples/hermes-runtime"],
["fusion-plugin-openclaw-runtime", "@fusion-plugin-examples/openclaw-runtime"],
["fusion-plugin-paperclip-runtime", "@fusion-plugin-examples/paperclip-runtime"],
];
for (const [pluginName, pkgName] of cases) {
await t.test(pkgName, () => {
const { statFn, readdirFn } = createStaleFs(pluginName, { artifactMtime: 1000, sourceMtime: 3000 });
const result = detectMissingOrStaleArtifacts("/repo", () => true, statFn, readdirFn);
assert.ok(result.some((pkg) => pkg.name === pkgName));
});
}
});
test("detectMissingOrStaleArtifacts merges missing and stale results without duplicates", () => {
const { statFn, readdirFn } = createStaleFs("fusion-plugin-hermes-runtime", {
artifactMtime: 1000,
sourceMtime: 3000,
});
const result = detectMissingOrStaleArtifacts(
"/repo",
(fullPath) => !fullPath.endsWith("packages/dashboard/dist/index.js"),
statFn,
readdirFn,
);
const names = result.map((pkg) => pkg.name);
assert.ok(names.includes("@fusion/dashboard"));
assert.ok(names.includes("@fusion-plugin-examples/hermes-runtime"));
assert.equal(new Set(names).size, names.length);
});
test("detectMissingArtifacts alias returns same value as detectMissingOrStaleArtifacts", () => {
const { statFn, readdirFn } = createStaleFs("fusion-plugin-hermes-runtime", {
artifactMtime: 1000,
sourceMtime: 3000,
});
const aliasResult = detectMissingArtifacts("/repo", () => true, statFn, readdirFn);
const directResult = detectMissingOrStaleArtifacts("/repo", () => true, statFn, readdirFn);
assert.deepEqual(aliasResult.map((pkg) => pkg.name), directResult.map((pkg) => pkg.name));
});
test("ensureTestArtifacts invokes rebuild command for stale package", () => {
const { statFn, readdirFn } = createStaleFs("fusion-plugin-hermes-runtime", {
artifactMtime: 1000,
sourceMtime: 3000,
});
const calls = [];
const built = ensureTestArtifacts(
"/repo",
(cmd, args, cwd) => calls.push({ cmd, args, cwd }),
() => true,
statFn,
readdirFn,
);
assert.ok(built.includes("@fusion-plugin-examples/hermes-runtime"));
assert.equal(calls.length, 1);
assert.deepEqual(calls[0].args, ["--filter", "@fusion-plugin-examples/hermes-runtime", "build"]);
});
test("ensureTestArtifacts writes FN-4232 remediation block to stderr on rebuild failure", () => {
const { statFn, readdirFn } = createStaleFs("fusion-plugin-hermes-runtime", {
artifactMtime: 1000,
sourceMtime: 3000,
});
let stderr = "";
let exitCode = null;
const built = ensureTestArtifacts(
"/repo",
undefined,
() => true,
statFn,
readdirFn,
{
spawnFn: () => ({ status: 2 }),
exitFn: (code) => {
exitCode = code;
},
stderrWrite: (chunk) => {
stderr += String(chunk);
return true;
},
},
);
assert.ok(built.includes("@fusion-plugin-examples/hermes-runtime"));
assert.equal(exitCode, 2);
assert.match(stderr, /@fusion-plugin-examples\/hermes-runtime/);
assert.match(stderr, /pnpm install --frozen-lockfile/);
assert.match(stderr, /FN-4232/);
});

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env node
import { existsSync } from "node:fs";
import { existsSync, readdirSync, statSync } from "node:fs";
import path from "node:path";
import { spawnSync } from "node:child_process";
@@ -33,26 +33,139 @@ export const REQUIRED_BUILD_PACKAGES = [
},
];
export function detectMissingArtifacts(rootDir = process.cwd(), existsFn = existsSync) {
return REQUIRED_BUILD_PACKAGES.filter((pkg) =>
pkg.requiredArtifacts.some((artifactPath) => !existsFn(path.join(rootDir, artifactPath))),
);
function collectNewestSourceMtimeMs(sourceDir, statFn, readdirFn) {
let newest = 0;
const stack = [sourceDir];
while (stack.length > 0) {
const current = stack.pop();
let entries = [];
try {
entries = readdirFn(current, { withFileTypes: true });
} catch {
continue;
}
for (const entry of entries) {
if (entry.name.startsWith(".")) continue;
const fullPath = path.join(current, entry.name);
if (entry.isDirectory()) {
if (entry.name === "node_modules" || entry.name === "dist") continue;
stack.push(fullPath);
continue;
}
let stats;
try {
stats = statFn(fullPath);
} catch {
continue;
}
newest = Math.max(newest, stats.mtimeMs);
}
}
return newest;
}
function run(command, args, cwd) {
const result = spawnSync(command, args, { cwd, stdio: "inherit" });
export function isStale(
pkgEntry,
rootDir = process.cwd(),
statFn = statSync,
readdirFn = readdirSync,
existsFn = existsSync,
) {
if (!pkgEntry?.staleAgainstGlobs?.length) return false;
let minArtifactMtimeMs = Number.POSITIVE_INFINITY;
for (const artifactPath of pkgEntry.requiredArtifacts) {
const fullPath = path.join(rootDir, artifactPath);
if (!existsFn(fullPath)) continue;
let stats;
try {
stats = statFn(fullPath);
} catch {
continue;
}
minArtifactMtimeMs = Math.min(minArtifactMtimeMs, stats.mtimeMs);
}
if (!Number.isFinite(minArtifactMtimeMs)) return false;
let maxSourceMtimeMs = 0;
for (const { sourcePath } of pkgEntry.staleAgainstGlobs) {
const sourceDir = path.join(rootDir, sourcePath);
maxSourceMtimeMs = Math.max(maxSourceMtimeMs, collectNewestSourceMtimeMs(sourceDir, statFn, readdirFn));
}
return maxSourceMtimeMs > minArtifactMtimeMs;
}
export function detectMissingOrStaleArtifacts(
rootDir = process.cwd(),
existsFn = existsSync,
statFn = statSync,
readdirFn = readdirSync,
) {
return REQUIRED_BUILD_PACKAGES.filter((pkg) => {
const missing = pkg.requiredArtifacts.some((artifactPath) => !existsFn(path.join(rootDir, artifactPath)));
if (missing) return true;
return isStale(pkg, rootDir, statFn, readdirFn, existsFn);
});
}
export function detectMissingArtifacts(rootDir = process.cwd(), existsFn = existsSync, statFn = statSync, readdirFn = readdirSync) {
return detectMissingOrStaleArtifacts(rootDir, existsFn, statFn, readdirFn);
}
function writeRemediation(stderrWrite, pkgNames, filterCommand) {
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] 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");
}
function run(
command,
args,
cwd,
{
exitFn = process.exit,
stderrWrite = process.stderr.write.bind(process.stderr),
spawnFn = spawnSync,
} = {},
) {
const result = spawnFn(command, args, { cwd, stdio: "inherit" });
if (result.status !== 0) {
process.exit(result.status ?? 1);
const filterCommand = `${command} ${args.join(" ")}`;
const packageNames = args.filter((entry, index) => args[index - 1] === "--filter");
writeRemediation(stderrWrite, packageNames, filterCommand);
exitFn(result.status ?? 1);
}
}
export function ensureTestArtifacts(rootDir = process.cwd(), runFn = run, existsFn = existsSync) {
const missing = detectMissingArtifacts(rootDir, existsFn);
if (missing.length === 0) return [];
export function ensureTestArtifacts(
rootDir = process.cwd(),
runFn = run,
existsFn = existsSync,
statFn = statSync,
readdirFn = readdirSync,
runOptions = {},
) {
const missingOrStale = detectMissingOrStaleArtifacts(rootDir, existsFn, statFn, readdirFn);
if (missingOrStale.length === 0) return [];
const names = missing.map((pkg) => pkg.name);
console.log(`[test-bootstrap] building missing dist artifacts: ${names.join(", ")}`);
runFn("pnpm", [...names.flatMap((name) => ["--filter", name]), "build"], rootDir);
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);
} else {
runFn("pnpm", [...names.flatMap((name) => ["--filter", name]), "build"], rootDir);
}
return names;
}