feat(FN-3590): prioritize workspace source entry for bundled plugins and ad

This merge introduces eval score categorization with a new `eval-scoring.ts` module (FN-3601, FN-3390), hardened bundled plugin entry resolution to prioritize workspace source over installed copies (FN-3590), and documented reply-link threading behavior in the mailbox (FN-3598). It also adds mobile

Fusion-Task-Id: FN-3590
This commit is contained in:
Fusion
2026-05-06 11:47:41 -07:00
committed by gsxdsm
parent 04a2568586
commit a8bfb32c70
7 changed files with 126 additions and 12 deletions

View File

@@ -328,7 +328,7 @@ describe("GET /plugins/:id/settings", () => {
expect(res.body).toEqual({ apiKey: "secret", enabled: true });
});
it("returns 404 for non-existent plugin", async () => {
it("returns 404 for non-existent non-bundled plugin", async () => {
(pluginStore.getPlugin as ReturnType<typeof vi.fn>).mockRejectedValueOnce(
Object.assign(new Error('Plugin "nonexistent" not found'), { code: "ENOENT" }),
);
@@ -339,6 +339,17 @@ describe("GET /plugins/:id/settings", () => {
expect(res.body).toHaveProperty("error");
});
it("returns empty settings for bundled runtime plugin before first install", async () => {
(pluginStore.getPlugin as ReturnType<typeof vi.fn>).mockRejectedValueOnce(
Object.assign(new Error('Plugin "fusion-plugin-hermes-runtime" not found'), { code: "ENOENT" }),
);
const res = await GET(buildApp(), "/api/plugins/fusion-plugin-hermes-runtime/settings");
expect(res.status).toBe(200);
expect(res.body).toEqual({});
});
it("supports projectId query param scoping", async () => {
const scopedPluginStore = createMockPluginStore();
(scopedPluginStore.getPlugin as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
@@ -1054,6 +1065,23 @@ describe("PUT /plugins/:id/settings auto-install for bundled runtime plugins", (
expect(ensure).not.toHaveBeenCalled();
});
it("returns 500 with intentional message when bundled auto-install reports missing bundle", async () => {
(pluginStore.getPlugin as ReturnType<typeof vi.fn>).mockRejectedValueOnce(
new Error("Plugin not found"),
);
const ensure = vi.fn().mockResolvedValue(false);
const res = await REQUEST(
buildAppWithBundleHook(ensure),
"PUT",
"/api/plugins/fusion-plugin-hermes-runtime/settings",
{ settings: { apiKey: "k" } },
);
expect(res.status).toBe(500);
expect(res.body.error).toContain("is unavailable in this build");
});
it("returns 500 when auto-install throws", async () => {
(pluginStore.getPlugin as ReturnType<typeof vi.fn>).mockRejectedValueOnce(
new Error("Plugin not found"),

View File

@@ -3247,7 +3247,15 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
const plugin = await pluginStore.getPlugin(id);
res.json(plugin.settings);
} catch (err: unknown) {
if (err instanceof Error && (err instanceof Error ? err.message : String(err)).includes("not found")) {
const isNotFoundError = err instanceof Error && (err instanceof Error ? err.message : String(err)).includes("not found");
const isBundledFallback = BUNDLED_PLUGIN_RUNTIMES.some((r) => r.pluginId === id);
if (isNotFoundError && isBundledFallback) {
// Bundled runtime plugins can be surfaced in settings before they've
// been lazily installed. Return empty defaults so cards can open.
res.json({});
return;
}
if (isNotFoundError) {
throw notFound(`Plugin "${id}" not found`);
}
throw internalError(err instanceof Error ? err.message : "Unknown error");
@@ -3606,8 +3614,16 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
}
if (!alreadyRegistered) {
try {
await options.ensureBundledPluginInstalled(id);
const installOk = await options.ensureBundledPluginInstalled(id);
if (!installOk) {
throw internalError(
`Bundled plugin "${id}" is unavailable in this build and could not be auto-installed`,
);
}
} catch (installErr) {
if (installErr instanceof ApiError) {
throw installErr;
}
throw internalError(
`Failed to auto-install bundled plugin "${id}": ${installErr instanceof Error ? installErr.message : String(installErr)}`,
);