fix: load live workspace plugin over stale staged bundle in dev
Closes the durability gap behind "grok chat returns empty replies". In a source checkout the dashboard resolved the staged CLI tsup bundle (packages/cli/dist/plugins/<id>/bundled.js), which resolvePluginEntryPath prefers verbatim with no freshness check. The FN-7779 dev prebuild rebuilds each plugin's own plugins/<id>/dist but never the staged tsup bundle, so a source-only plugin fix ran stale until a manual `pnpm build`. getCandidatePluginDirs now probes the live workspace source dir (<repo>/plugins/<id>) before the staged bundle, so dev loads the freshness-checked live plugin (dist-vs-src), self-healing even when the prebuild is skipped. The global-staged dir stays first, so published installs (no workspace dir) are unaffected — asserted by the retained global-install regression test plus a new source-checkout preference test. Verified on a live dashboard: grok chat streams text and the loader now writes its reload copies under plugins/fusion-plugin-grok-runtime/dist, not the staged bundle. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,4 +4,4 @@
|
||||
|
||||
summary: Fix Grok CLI chat returning errors or empty replies in the dashboard.
|
||||
category: fix
|
||||
dev: Two independent defects. (1) The default (no-project) ChatManager received a bare PluginLoader as its runner; Grok CLI routing (deriveGrokRuntimeHintForNoVisibleKey → resolveRuntime) calls getRuntimeById/createRuntimeContext, which only exist on PluginRunner, so a grok-cli/* chat with no Fusion-visible GROK_API_KEY threw "getRuntimeById is not a function" and surfaced the misleading "requires the bundled Grok CLI runtime" error. New resolveChatManagerPluginRunner(options) prefers the engine's PluginRunner (the runner the project-scoped chat path already uses), falling back to the loader only in UI-only mode. (2) The CLI-bundled Grok plugin (packages/cli/dist/plugins/fusion-plugin-grok-runtime/bundled.js) was stale relative to the FN-7796 single-JSON adapter source, so project-scoped grok chat produced empty replies; a `pnpm build` (tsup) regenerates it. The bundled-plugin freshness guard only warns — the dev prebuild does not rebuild the CLI tsup bundle.
|
||||
dev: Two independent defects. (1) The default (no-project) ChatManager received a bare PluginLoader as its runner; Grok CLI routing (deriveGrokRuntimeHintForNoVisibleKey → resolveRuntime) calls getRuntimeById/createRuntimeContext, which only exist on PluginRunner, so a grok-cli/* chat with no Fusion-visible GROK_API_KEY threw "getRuntimeById is not a function" and surfaced the misleading "requires the bundled Grok CLI runtime" error. New resolveChatManagerPluginRunner(options) prefers the engine's PluginRunner (the runner the project-scoped chat path already uses), falling back to the loader only in UI-only mode. (2) In a source checkout the running dashboard resolved the staged CLI tsup bundle (packages/cli/dist/plugins/fusion-plugin-grok-runtime/bundled.js), which resolvePluginEntryPath prefers verbatim with no freshness check; that bundle was stale vs the FN-7796 single-JSON adapter source (the FN-7779 dev prebuild rebuilds each plugin's own dist but never the staged bundle), so project-scoped grok chat produced empty replies. Fixed durably: getCandidatePluginDirs now probes the live workspace source dir (<repo>/plugins/<id>) before the staged bundle, so dev loads the freshness-checked live plugin (self-healing even when the prebuild is skipped). Published installs are unaffected (no workspace dir). A one-time `pnpm build` refreshes any already-stale staged bundle.
|
||||
|
||||
@@ -210,6 +210,44 @@ describe("CLI candidate bundle-directory resolution", () => {
|
||||
expect(result).toBe("installed");
|
||||
});
|
||||
|
||||
/*
|
||||
FNXC:PluginLoader 2026-07-10-00:00:
|
||||
Durability regression for "grok chat returns empty replies". In a source
|
||||
checkout BOTH the staged tsup bundle (<cli>/dist/plugins/<id>/bundled.js) and
|
||||
the live workspace source (<repo>/plugins/<id>) exist. resolvePluginEntryPath
|
||||
prefers bundled.js with no freshness check, so the stale staged bundle used to
|
||||
win and shadow source-only plugin fixes. The workspace source dir must now be
|
||||
probed first so dev loads the freshness-checked live plugin, not the stale bundle.
|
||||
*/
|
||||
it("prefers the live workspace source over the staged tsup bundle in a source checkout", async () => {
|
||||
const ID = "fusion-plugin-grok-runtime";
|
||||
mockExistsSync.mockImplementation((p: string) => {
|
||||
if (typeof p !== "string") return false;
|
||||
// Staged tsup bundle (would win under the old order).
|
||||
if (p.endsWith(`/dist/plugins/${ID}/manifest.json`)) return true;
|
||||
if (p.endsWith(`/dist/plugins/${ID}/bundled.js`)) return true;
|
||||
// Live workspace source dir: <repo>/plugins/<id> (NOT under /dist/plugins/).
|
||||
if (p.includes("/dist/plugins/")) return false;
|
||||
if (p.endsWith(`/plugins/${ID}/manifest.json`)) return true;
|
||||
if (p.endsWith(`/plugins/${ID}/dist/index.js`)) return true;
|
||||
return false;
|
||||
});
|
||||
mockReadFile.mockResolvedValue(JSON.stringify(makeManifest({ id: ID, name: "Grok Runtime" })));
|
||||
mockValidatePluginManifest.mockReturnValue({ valid: true, errors: [] });
|
||||
|
||||
const store = makePluginStore();
|
||||
const loader = makePluginLoader();
|
||||
|
||||
const result = await ensureBundledPluginInstalled(store as never, loader as never, ID);
|
||||
|
||||
expect(result).toBe("installed");
|
||||
const registerCall = store.registerPlugin.mock.calls[0]?.[0] as { path: string };
|
||||
// Must resolve the live workspace entry, never the stale staged bundle.
|
||||
expect(registerCall.path.endsWith(`/plugins/${ID}/dist/index.js`)).toBe(true);
|
||||
expect(registerCall.path).not.toContain("/dist/plugins/");
|
||||
expect(registerCall.path.endsWith("bundled.js")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns missing-bundle when no CLI candidate dir has a manifest", async () => {
|
||||
mockExistsSync.mockReturnValue(false);
|
||||
const store = makePluginStore();
|
||||
|
||||
@@ -27,18 +27,41 @@ import {
|
||||
export { BUNDLED_PLUGIN_IDS, isBundledPluginId, resolvePluginEntryPath } from "@fusion/core";
|
||||
export type { BundledPluginId, EnsureBundledResult } from "@fusion/core";
|
||||
|
||||
/*
|
||||
FNXC:PluginLoader 2026-07-10-00:00:
|
||||
Candidate order encodes a freshness contract, not just a search path.
|
||||
|
||||
Published/global install (the regression the first candidate protects): plugins
|
||||
are staged next to the running bin at `<cli>/dist/plugins/<id>`, and no workspace
|
||||
`plugins/` dir exists — so `join(moduleDir, "plugins", <id>)` MUST stay first and
|
||||
win.
|
||||
|
||||
Source checkout / `pnpm dev` (the durability fix): the running dashboard would
|
||||
otherwise resolve the STAGED tsup bundle at `<cli>/dist/plugins/<id>/bundled.js`,
|
||||
which `resolvePluginEntryPath` prefers verbatim with NO freshness check. That
|
||||
bundle is a build artifact only `tsup` regenerates — the FN-7779 dev prebuild
|
||||
rebuilds each plugin's OWN `plugins/<id>/dist` but never the staged bundle, so a
|
||||
source-only plugin fix (e.g. the FN-7796 Grok adapter) silently ran stale and
|
||||
grok chat returned empty replies. Probe the workspace source dir
|
||||
(`<repo>/plugins/<id>`) BEFORE the staged bundle so dev loads the live plugin
|
||||
whose entry `resolvePluginEntryPath` freshness-checks (dist vs src) — self-healing
|
||||
even when the prebuild is skipped. The workspace dir only exists in a checkout, so
|
||||
published installs are unaffected.
|
||||
*/
|
||||
function getCandidatePluginDirs(pluginId: string): string[] {
|
||||
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
||||
const cliPackageRoot = resolve(moduleDir, "..", "..");
|
||||
|
||||
return [
|
||||
// Bundled/global runtime: moduleDir is typically <cli>/dist, and plugins are
|
||||
// staged under <cli>/dist/plugins/<id>.
|
||||
// staged under <cli>/dist/plugins/<id>. Keep first for the global-install regression.
|
||||
join(moduleDir, "plugins", pluginId),
|
||||
// Source checkout: prefer the live workspace plugin (freshness-checked by
|
||||
// resolvePluginEntryPath) over the stale staged tsup bundle below.
|
||||
join(cliPackageRoot, "..", "..", "plugins", pluginId),
|
||||
// Source/dev fallbacks.
|
||||
join(cliPackageRoot, "dist", "plugins", pluginId),
|
||||
join(cliPackageRoot, "plugins", pluginId),
|
||||
join(cliPackageRoot, "..", "..", "plugins", pluginId),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user