feat(FN-4050): add one-click install support for reports plugin
- Add reports plugin to bundled plugin resolver so it can be installed from packaged assets - Expose reports plugin in dashboard plugin manager and server routes for one-click install flows - Add CLI and dashboard test coverage for bundled install behavior and plugin route availability - Update reports/plugin management/settings docs and include a changeset for @runfusion/fusion
This commit is contained in:
@@ -123,6 +123,13 @@ const BUILTIN_PLUGINS: BuiltinPlugin[] = [
|
||||
category: "integration",
|
||||
path: "./plugins/fusion-plugin-dependency-graph",
|
||||
},
|
||||
{
|
||||
id: "fusion-plugin-reports",
|
||||
name: "Reports",
|
||||
description: "View report history, compare runs side-by-side, and export standalone HTML summaries.",
|
||||
category: "integration",
|
||||
path: "./plugins/fusion-plugin-reports",
|
||||
},
|
||||
{
|
||||
id: "fusion-plugin-whatsapp-chat",
|
||||
name: "WhatsApp Chat",
|
||||
|
||||
@@ -267,6 +267,7 @@ describe("PluginManager", () => {
|
||||
expect(screen.getByText("OpenClaw Runtime")).toBeTruthy();
|
||||
expect(screen.getByText("Droid Runtime")).toBeTruthy();
|
||||
expect(screen.getByText("Dependency Graph")).toBeTruthy();
|
||||
expect(screen.getByText("Reports")).toBeTruthy();
|
||||
expect(screen.getByText("WhatsApp Chat")).toBeTruthy();
|
||||
expect(screen.getByText("CLI Printing Press")).toBeTruthy();
|
||||
expect(screen.getByText(/Pairs to WhatsApp Web \(multi-device\) with QR or pairing code/i)).toBeTruthy();
|
||||
@@ -426,6 +427,26 @@ describe("PluginManager", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("installs reports from the built-in section", async () => {
|
||||
render(<PluginManager addToast={addToast} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchPlugins).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const reportsLabel = await screen.findByText("Reports");
|
||||
const reportsCard = reportsLabel.closest(".plugin-builtins-item");
|
||||
expect(reportsCard).toBeTruthy();
|
||||
|
||||
const installButton = within(reportsCard as HTMLElement).getByRole("button", { name: /Install Reports/i });
|
||||
await userEvent.click(installButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(installPlugin).toHaveBeenCalledWith({ path: "./plugins/fusion-plugin-reports" }, undefined);
|
||||
expect(addToast).toHaveBeenCalledWith("Reports installed globally", "success");
|
||||
});
|
||||
});
|
||||
|
||||
it("installs CLI Printing Press from the built-in section", async () => {
|
||||
render(<PluginManager addToast={addToast} />);
|
||||
|
||||
@@ -842,6 +863,37 @@ describe("PluginManager", () => {
|
||||
expect(screen.getByTestId("plugin-manager-detail")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shows Manage for installed reports in built-in section", async () => {
|
||||
vi.mocked(fetchPlugins).mockResolvedValueOnce([
|
||||
{
|
||||
...mockPlugins[0],
|
||||
id: "fusion-plugin-reports",
|
||||
name: "Reports",
|
||||
},
|
||||
]);
|
||||
|
||||
render(<PluginManager addToast={addToast} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText("Reports").length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
const reportsCard = screen.getAllByText("Reports")[1]?.closest(".plugin-builtins-item");
|
||||
expect(reportsCard).toBeTruthy();
|
||||
|
||||
const manageButton = within(reportsCard as HTMLElement).getByRole("button", { name: /^Manage$/i });
|
||||
expect(manageButton).not.toBeDisabled();
|
||||
expect(within(reportsCard as HTMLElement).getAllByText("Installed").length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
await userEvent.click(manageButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchPluginSettings).toHaveBeenCalledWith("fusion-plugin-reports", undefined);
|
||||
});
|
||||
|
||||
expect(screen.getByTestId("plugin-manager-detail")).toBeTruthy();
|
||||
});
|
||||
|
||||
describe("SSE Live Updates", () => {
|
||||
it("subscribes to plugin:lifecycle SSE events", async () => {
|
||||
vi.mocked(fetchPlugins).mockResolvedValueOnce(mockPlugins);
|
||||
|
||||
@@ -517,6 +517,38 @@ describe("POST /api/plugins mode:install — bundled plugin path fallback", () =
|
||||
);
|
||||
});
|
||||
|
||||
it("installs bundled reports plugin when relative path misses cwd", async () => {
|
||||
const bundledManifest = {
|
||||
...VALID_MANIFEST,
|
||||
id: "fusion-plugin-reports",
|
||||
name: "Reports",
|
||||
};
|
||||
mockExistsSync.mockImplementation((p: string) => p.includes("fusion-plugin-reports/manifest.json"));
|
||||
mockAccess.mockImplementation((p: string) => {
|
||||
if (p.includes("fusion-plugin-reports")) return Promise.resolve();
|
||||
return Promise.reject(new Error("not found"));
|
||||
});
|
||||
mockReadFile.mockResolvedValue(JSON.stringify(bundledManifest));
|
||||
(pluginStore.registerPlugin as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
...INSTALLED_PLUGIN,
|
||||
id: "fusion-plugin-reports",
|
||||
name: "Reports",
|
||||
});
|
||||
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/plugins", {
|
||||
mode: "install",
|
||||
path: "./plugins/fusion-plugin-reports",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
expect(pluginStore.registerPlugin).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
manifest: expect.objectContaining({ id: "fusion-plugin-reports" }),
|
||||
path: expect.stringContaining("fusion-plugin-reports"),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("returns 404 with helpful message when local and bundled paths are unresolved", async () => {
|
||||
mockExistsSync.mockReturnValue(false);
|
||||
mockAccess.mockRejectedValue(new Error("not found"));
|
||||
|
||||
@@ -90,6 +90,7 @@ const BUNDLED_PLUGIN_RUNTIMES: Array<{
|
||||
];
|
||||
const BUNDLED_PLUGIN_IDS = new Set([
|
||||
"fusion-plugin-dependency-graph",
|
||||
"fusion-plugin-reports",
|
||||
"fusion-plugin-whatsapp-chat",
|
||||
"fusion-plugin-roadmap",
|
||||
"fusion-plugin-hermes-runtime",
|
||||
|
||||
Reference in New Issue
Block a user