Files
fusion/packages/cli/src/plugins/__tests__/resolve-plugin-entry-path-sync.test.ts
gsxdsm 4c3186d0d1 FN-6615: prefer fresh plugin source over stale dist
Plugin entry resolution now avoids stale gitignored dist output in dev worktrees.

- Prefer bundled plugin entries when present, preserving published tarball behavior.
- Choose src/index.ts over dist/index.js when source files are newer than dist output.
- Cover CLI and core plugin loaders with freshness and sync tests.
- Add a patch changeset for the published CLI package.

Files changed:
 .changeset/fn-6615-prefer-fresh-src.md             |  5 ++
 .../__tests__/bundled-plugin-install.test.ts       | 46 ++++++++---
 .../resolve-plugin-entry-path-sync.test.ts         | 50 ++++++++++--
 packages/cli/src/plugins/bundled-plugin-install.ts | 93 ++++++++++++++++++----
 packages/core/src/__tests__/plugin-loader.test.ts  | 71 ++++++++++++++++-
 packages/core/src/plugin-loader.ts                 | 93 ++++++++++++++++++----
 6 files changed, 313 insertions(+), 45 deletions(-)

Fusion-Task-Id: FN-6615

Fusion-Task-Lineage: 3d9abfa8-a405-47fa-8330-451c26a0ee75
2026-06-17 19:46:47 -07:00

98 lines
3.8 KiB
TypeScript

/**
* Drift guard for the intentionally duplicated resolvePluginEntryPath.
*
* The CLI keeps a local copy in bundled-plugin-install.ts (so its fs mocks
* work in tests) while @fusion/core owns the copy used by the dashboard
* install/enable routes. This test runs both against real on-disk layouts and
* asserts identical results, so a candidate-list change applied to one copy
* but not the other fails CI instead of silently diverging.
*
* No fs mocks here on purpose — vitest module mocks don't reach the
* externalized @fusion/core import, so real temp directories are the only
* seam that exercises both implementations equally.
*/
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, utimesSync } from "node:fs";
import { dirname, join } from "node:path";
import { tmpdir } from "node:os";
import { resolvePluginEntryPath as cliResolve } from "../bundled-plugin-install.js";
import { resolvePluginEntryPath as coreResolve } from "@fusion/core";
describe("resolvePluginEntryPath: CLI copy stays in sync with @fusion/core", () => {
let dir: string;
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), "entry-path-sync-"));
});
afterEach(() => {
rmSync(dir, { recursive: true, force: true });
});
function touch(relative: string) {
const full = join(dir, relative);
mkdirSync(dirname(full), { recursive: true });
writeFileSync(full, "// entry\n");
}
const older = new Date("2026-01-01T00:00:00.000Z");
const newer = new Date("2026-01-01T00:01:00.000Z");
const layouts: Array<{
name: string;
files: string[];
expected: string | null;
mtimes?: Record<string, Date>;
}> = [
{ name: "bundled.js only", files: ["bundled.js"], expected: "bundled.js" },
{ name: "dist/index.js only", files: ["dist/index.js"], expected: "dist/index.js" },
{ name: "src/index.ts only", files: ["src/index.ts"], expected: "src/index.ts" },
{ name: "bundled.js preferred over src", files: ["bundled.js", "src/index.ts"], expected: "bundled.js" },
{
name: "dist + src, src newer → src/index.ts",
files: ["dist/index.js", "src/index.ts"],
expected: "src/index.ts",
mtimes: { "dist/index.js": older, "src/index.ts": newer },
},
{
name: "dist + src, dist newer → dist/index.js",
files: ["dist/index.js", "src/index.ts"],
expected: "dist/index.js",
mtimes: { "dist/index.js": newer, "src/index.ts": older },
},
{
name: "dist + src, equal mtimes → dist/index.js",
files: ["dist/index.js", "src/index.ts"],
expected: "dist/index.js",
mtimes: { "dist/index.js": older, "src/index.ts": older },
},
{
name: "dist + src, non-index src file newer → src/index.ts",
files: ["dist/index.js", "src/index.ts", "src/settings.ts"],
expected: "src/index.ts",
mtimes: { "dist/index.js": older, "src/index.ts": older, "src/settings.ts": newer },
},
{
name: "bundled.js + dist + src, src newer → bundled.js",
files: ["bundled.js", "dist/index.js", "src/index.ts"],
expected: "bundled.js",
mtimes: { "dist/index.js": older, "src/index.ts": newer },
},
{ name: "all three → bundled.js", files: ["bundled.js", "dist/index.js", "src/index.ts"], expected: "bundled.js" },
{ name: "no entry files", files: ["README.md"], expected: null },
];
for (const layout of layouts) {
it(`resolves identically for: ${layout.name}`, () => {
for (const f of layout.files) touch(f);
for (const [file, mtime] of Object.entries(layout.mtimes ?? {})) {
utimesSync(join(dir, file), mtime, mtime);
}
const expected = layout.expected === null ? null : join(dir, layout.expected);
expect(cliResolve(dir)).toBe(expected);
expect(coreResolve(dir)).toBe(expected);
});
}
});