feat(FN-3077): enforce plugin AI security scan gate across install flows

- Add core plugin AI security scan module and schema support for scan toggle/state metadata
- Enforce scan checks during CLI and dashboard plugin install flows, with preserved API error status on scan failures
- Expose plugin scan toggle and rescan actions in dashboard/plugin manager with route and UI coverage
- Update plugin authoring and CLI/dashboard docs, plus add changeset for published CLI package

Fusion-Task-Id: FN-3077
This commit is contained in:
Fusion
2026-05-07 02:14:36 -07:00
committed by gsxdsm
parent 415231bd97
commit fce3668a83
29 changed files with 800 additions and 43 deletions

View File

@@ -202,6 +202,58 @@ async function REQUEST(
}
// ══════════════════════════════════════════════════════════════════
describe("PATCH/POST plugin scan config routes", () => {
let pluginStore: PluginStore;
let pluginLoader: PluginLoader;
let store: TaskStore;
beforeEach(() => {
vi.clearAllMocks();
pluginStore = createMockPluginStore({
getPlugin: vi.fn().mockResolvedValue({ ...INSTALLED_PLUGIN, id: "my-plugin", enabled: true, state: "started" }),
updatePlugin: vi.fn().mockResolvedValue({ ...INSTALLED_PLUGIN, id: "my-plugin", aiScanOnLoad: true }),
});
pluginLoader = createMockPluginLoader({ loadPlugin: vi.fn().mockResolvedValue(undefined) });
store = createMockTaskStore({ getPluginStore: vi.fn().mockReturnValue(pluginStore) });
});
function buildApp() {
const app = express();
app.use(express.json());
app.use("/api", createApiRoutes(store, { pluginStore, pluginLoader }));
return app;
}
it("PATCH /api/plugins/:id updates aiScanOnLoad", async () => {
const res = await REQUEST(buildApp(), "PATCH", "/api/plugins/my-plugin", { aiScanOnLoad: true });
expect(res.status).toBe(200);
expect(pluginStore.updatePlugin).toHaveBeenCalledWith("my-plugin", { aiScanOnLoad: true });
});
it("PATCH /api/plugins/:id returns 400 for invalid body", async () => {
const res = await REQUEST(buildApp(), "PATCH", "/api/plugins/my-plugin", { aiScanOnLoad: "yes" });
expect(res.status).toBe(400);
});
it("PATCH /api/plugins/:id returns 404 for unknown plugin", async () => {
(pluginStore.updatePlugin as ReturnType<typeof vi.fn>).mockRejectedValueOnce(new Error("not found"));
const res = await REQUEST(buildApp(), "PATCH", "/api/plugins/unknown", { aiScanOnLoad: true });
expect(res.status).toBe(404);
});
it("POST /api/plugins/:id/rescan returns plugin payload", async () => {
const res = await REQUEST(buildApp(), "POST", "/api/plugins/my-plugin/rescan", {});
expect(res.status).toBe(200);
expect(pluginLoader.loadPlugin).toHaveBeenCalledWith("my-plugin");
});
it("POST /api/plugins/:id/rescan returns 404 for unknown plugin", async () => {
(pluginStore.getPlugin as ReturnType<typeof vi.fn>).mockRejectedValueOnce(new Error("not found"));
const res = await REQUEST(buildApp(), "POST", "/api/plugins/unknown/rescan", {});
expect(res.status).toBe(404);
});
});
describe("POST /api/plugins mode:install — package root path", () => {
let pluginStore: PluginStore;
let pluginLoader: PluginLoader;