feat(FN-3170): harden plugin bundling contract and fix verification regress

Hardened the roadmap plugin bundling contract with new output-validation tests and tsup config cleanup, then resolved regressions those changes introduced in the bundle and package-config test suites. A small dashboard UI tweak (TaskDetailModal) was also included.

Fusion-Task-Id: FN-3170
This commit is contained in:
Fusion
2026-05-10 06:09:30 -07:00
committed by gsxdsm
parent 42692b50de
commit 0a55a4db4b
8 changed files with 60 additions and 17 deletions

View File

@@ -25,6 +25,10 @@ const cursorPluginManifestPath = bundlePath.replace(
"dist/bin.js",
"dist/plugins/fusion-plugin-cursor-runtime/manifest.json",
);
const roadmapPluginBundledPath = bundlePath.replace(
"dist/bin.js",
"dist/plugins/fusion-plugin-roadmap/bundled.js",
);
describe("hasBuiltDashboardAssets", () => {
beforeEach(() => {
@@ -36,6 +40,7 @@ describe("hasBuiltDashboardAssets", () => {
state.existingPaths.add(bundlePath);
state.existingPaths.add(clientIndexPath);
state.existingPaths.add(cursorPluginManifestPath);
state.existingPaths.add(roadmapPluginBundledPath);
expect(hasBuiltDashboardAssets()).toBe(false);
});
@@ -44,6 +49,7 @@ describe("hasBuiltDashboardAssets", () => {
state.existingPaths.add(bundlePath);
state.existingPaths.add(clientIndexPath);
state.existingPaths.add(cursorPluginManifestPath);
state.existingPaths.add(roadmapPluginBundledPath);
state.existingPaths.add(openclawMcpSchemaServerPath);
expect(hasBuiltDashboardAssets()).toBe(false);
@@ -53,6 +59,7 @@ describe("hasBuiltDashboardAssets", () => {
state.existingPaths.add(bundlePath);
state.existingPaths.add(clientIndexPath);
state.existingPaths.add(cursorPluginManifestPath);
state.existingPaths.add(roadmapPluginBundledPath);
state.existingPaths.add(openclawMcpSchemaServerPath);
state.existingPaths.add(droidPluginMcpServerPath);
@@ -63,6 +70,7 @@ describe("hasBuiltDashboardAssets", () => {
state.existingPaths.add(bundlePath);
state.existingPaths.add(clientIndexPath);
state.existingPaths.add(cursorPluginManifestPath);
state.existingPaths.add(roadmapPluginBundledPath);
state.existingPaths.add(openclawMcpSchemaServerPath);
state.existingPaths.add(droidPluginMcpServerPath);
state.indexHtml = dashboardClientStubMarker;

View File

@@ -7,6 +7,7 @@ export const workspaceRoot = join(cliRoot, "..", "..");
export const bundlePath = join(cliRoot, "dist", "bin.js");
export const clientIndexPath = join(cliRoot, "dist", "client", "index.html");
const cursorPluginManifestPath = join(cliRoot, "dist", "plugins", "fusion-plugin-cursor-runtime", "manifest.json");
const roadmapPluginBundledPath = join(cliRoot, "dist", "plugins", "fusion-plugin-roadmap", "bundled.js");
export const openclawMcpSchemaServerPath = join(
cliRoot,
"dist",
@@ -47,6 +48,7 @@ export function hasBuiltDashboardAssets(): boolean {
!existsSync(bundlePath) ||
!existsSync(clientIndexPath) ||
!existsSync(cursorPluginManifestPath) ||
!existsSync(roadmapPluginBundledPath) ||
!existsSync(openclawMcpSchemaServerPath) ||
!existsSync(droidPluginMcpServerPath)
) {

View File

@@ -37,9 +37,11 @@ describe("CLI bundle output", () => {
expect(content).not.toMatch(/from\s+["']@fusion\/core["']/);
expect(content).not.toMatch(/from\s+["']@fusion\/dashboard["']/);
expect(content).not.toMatch(/from\s+["']@fusion\/engine["']/);
expect(content).not.toMatch(/from\s+["']@fusion-plugin-examples\/roadmap["']/);
expect(content).not.toContain('"@fusion/core"');
expect(content).not.toContain('"@fusion/dashboard"');
expect(content).not.toContain('"@fusion/engine"');
expect(content).not.toContain('"@fusion-plugin-examples/roadmap"');
});
it("does not contain runtime memory-backend side-load imports", () => {
@@ -178,6 +180,26 @@ describe("CLI bundle output", () => {
expect(stagedPkg.exports?.["."]?.import).toBe("./bundled.js");
});
it("dist/plugins/fusion-plugin-roadmap/ is staged as bundled runtime output", () => {
const stagedRoot = join(cliRoot, "dist", "plugins", "fusion-plugin-roadmap");
const manifestPath = join(stagedRoot, "manifest.json");
const packageJsonPath = join(stagedRoot, "package.json");
expect(existsSync(manifestPath)).toBe(true);
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")) as { id?: string; name?: string };
expect(manifest.id).toBe("roadmap-planner");
expect(typeof manifest.name).toBe("string");
expect(manifest.name?.length).toBeGreaterThan(0);
expect(existsSync(join(stagedRoot, "bundled.js"))).toBe(true);
expect(existsSync(join(stagedRoot, "src"))).toBe(false);
const stagedPkg = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as {
exports?: { "."?: { import?: string } };
};
expect(stagedPkg.exports?.["."]?.import).toBe("./bundled.js");
});
it("dist/plugins/fusion-plugin-whatsapp-chat/ is staged with a valid manifest", () => {
const stagedRoot = join(cliRoot, "dist", "plugins", "fusion-plugin-whatsapp-chat");
const manifestPath = join(stagedRoot, "manifest.json");

View File

@@ -22,6 +22,11 @@ function loadRootPackageJson(): any {
return JSON.parse(readFileSync(path, "utf-8"));
}
function loadCliPrepackScript(): string {
const path = join(workspaceRoot, "packages", "cli", "scripts", "prepare-publish-manifest.mjs");
return readFileSync(path, "utf-8");
}
function hasProjectArg(script: string | undefined, project: string): boolean {
const parts = script?.trim().split(/\s+/) ?? [];
return parts.some((part, index) => part === "--project" && parts[index + 1] === project);
@@ -29,6 +34,7 @@ function hasProjectArg(script: string | undefined, project: string): boolean {
describe("CLI package.json publishing config", () => {
const pkg = loadPackageJson("cli");
const prepackScript = loadCliPrepackScript();
it('has "bin" field with fn pointing to ./dist/bin.js', () => {
expect(pkg.bin).toBeDefined();
@@ -85,6 +91,12 @@ describe("CLI package.json publishing config", () => {
expect(deps).toContain("ioredis");
});
it("prepack manifest rewrite strips workspace-only plugin/tooling devDependencies", () => {
expect(prepackScript).toContain('delete devDependencies["@fusion/pi-claude-cli"]');
expect(prepackScript).toContain('delete devDependencies["@fusion/pi-llama-cpp"]');
expect(prepackScript).toContain('delete devDependencies["@fusion-plugin-examples/roadmap"]');
});
// Generalized guard derived from tsup.config.ts. Any non-builtin module
// marked `external` MUST be a runtime dep (so `npm install @runfusion/fusion`
// can resolve it after publish), and any module pulled in via `noExternal`