feat(FN-2394): add Fusion reinstall flow for Pi extensions
- Add dashboard route and server handling to reinstall the Fusion Pi extension - Introduce a client API helper and wire a reinstall action into PiExtensionsManager - Polish action spacing and document Fusion reinstall recovery in the CLI README - Expand route, API, and component tests including reinstall refresh stability coverage - Add a changeset for @runfusion/fusion patch release
This commit is contained in:
@@ -55,6 +55,7 @@ import {
|
||||
fetchPiSettings,
|
||||
updatePiSettings,
|
||||
installPiPackage,
|
||||
reinstallFusionPiPackage,
|
||||
fetchPiExtensions,
|
||||
updatePiExtensions,
|
||||
fetchProjectTasks,
|
||||
@@ -5962,6 +5963,45 @@ describe("installPiPackage", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("reinstallFusionPiPackage", () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
it("sends POST to /api/pi-settings/reinstall-fusion", async () => {
|
||||
globalThis.fetch = vi.fn().mockReturnValue(
|
||||
mockFetchResponse(true, { success: true, source: "npm:@runfusion/fusion" })
|
||||
);
|
||||
|
||||
const result = await reinstallFusionPiPackage();
|
||||
|
||||
expect(result).toEqual({ success: true, source: "npm:@runfusion/fusion" });
|
||||
expect(globalThis.fetch).toHaveBeenCalledWith("/api/pi-settings/reinstall-fusion", expect.objectContaining({
|
||||
method: "POST",
|
||||
}));
|
||||
});
|
||||
|
||||
it("forwards projectId query parameter when provided", async () => {
|
||||
globalThis.fetch = vi.fn().mockReturnValue(
|
||||
mockFetchResponse(true, { success: true, source: "npm:@runfusion/fusion" })
|
||||
);
|
||||
|
||||
await reinstallFusionPiPackage("proj-123");
|
||||
|
||||
expect(globalThis.fetch).toHaveBeenCalledWith("/api/pi-settings/reinstall-fusion?projectId=proj-123", expect.objectContaining({
|
||||
method: "POST",
|
||||
}));
|
||||
});
|
||||
|
||||
it("throws API error message on failure", async () => {
|
||||
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(false, { error: "Reinstall failed" }, 500));
|
||||
|
||||
await expect(reinstallFusionPiPackage()).rejects.toThrow("Reinstall failed");
|
||||
});
|
||||
});
|
||||
|
||||
describe("fetchPiExtensions", () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
|
||||
@@ -657,6 +657,13 @@ export async function installPiPackage(source: string): Promise<{ success: boole
|
||||
});
|
||||
}
|
||||
|
||||
/** Reinstall Fusion's bundled pi package and ensure it remains in global Pi settings. */
|
||||
export async function reinstallFusionPiPackage(projectId?: string): Promise<{ success: boolean; source: string }> {
|
||||
return api<{ success: boolean; source: string }>(withProjectId("/pi-settings/reinstall-fusion", projectId), {
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
export async function uploadAttachment(id: string, file: File, projectId?: string): Promise<TaskAttachment> {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
RefreshCw,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { fetchPiSettings, updatePiSettings, installPiPackage, fetchPiExtensions, updatePiExtensions, type PiSettings, type PiExtensionEntry } from "../api";
|
||||
import { fetchPiSettings, updatePiSettings, installPiPackage, reinstallFusionPiPackage, fetchPiExtensions, updatePiExtensions, type PiSettings, type PiExtensionEntry } from "../api";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
|
||||
interface PiExtensionsManagerProps {
|
||||
@@ -67,6 +67,7 @@ export function PiExtensionsManager({ addToast, projectId }: PiExtensionsManager
|
||||
const [settings, setSettings] = useState<PiSettings | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [installing, setInstalling] = useState(false);
|
||||
const [reinstallingFusion, setReinstallingFusion] = useState(false);
|
||||
const [newSource, setNewSource] = useState("");
|
||||
const [expandedPackages, setExpandedPackages] = useState<Set<number>>(new Set());
|
||||
|
||||
@@ -154,6 +155,19 @@ export function PiExtensionsManager({ addToast, projectId }: PiExtensionsManager
|
||||
}
|
||||
};
|
||||
|
||||
const handleReinstallFusion = async () => {
|
||||
try {
|
||||
setReinstallingFusion(true);
|
||||
await reinstallFusionPiPackage(projectId);
|
||||
await Promise.all([loadSettings(), loadExtensions()]);
|
||||
addToast("Fusion skill reinstalled successfully", "success");
|
||||
} catch (err) {
|
||||
addToast(`Failed to reinstall Fusion skill: ${err instanceof Error ? err.message : String(err)}`, "error");
|
||||
} finally {
|
||||
setReinstallingFusion(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemovePackage = async (sourceToRemove: string) => {
|
||||
if (!settings) return;
|
||||
|
||||
@@ -263,6 +277,15 @@ export function PiExtensionsManager({ addToast, projectId }: PiExtensionsManager
|
||||
{installing ? "Installing…" : "Add"}
|
||||
</button>
|
||||
</div>
|
||||
<div className="pi-ext-add-form-row">
|
||||
<button
|
||||
className="btn"
|
||||
onClick={handleReinstallFusion}
|
||||
disabled={reinstallingFusion}
|
||||
>
|
||||
{reinstallingFusion ? "Reinstalling Fusion…" : "Reinstall Fusion skill"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Package list */}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { PiExtensionsManager } from "../PiExtensionsManager";
|
||||
const mockFetchPiSettings = vi.fn();
|
||||
const mockUpdatePiSettings = vi.fn();
|
||||
const mockInstallPiPackage = vi.fn();
|
||||
const mockReinstallFusionPiPackage = vi.fn();
|
||||
const mockFetchPiExtensions = vi.fn();
|
||||
const mockUpdatePiExtensions = vi.fn();
|
||||
|
||||
@@ -53,6 +54,7 @@ vi.mock("../../api", () => ({
|
||||
fetchPiSettings: (...args: unknown[]) => mockFetchPiSettings(...args),
|
||||
updatePiSettings: (...args: unknown[]) => mockUpdatePiSettings(...args),
|
||||
installPiPackage: (...args: unknown[]) => mockInstallPiPackage(...args),
|
||||
reinstallFusionPiPackage: (...args: unknown[]) => mockReinstallFusionPiPackage(...args),
|
||||
fetchPiExtensions: (...args: unknown[]) => mockFetchPiExtensions(...args),
|
||||
updatePiExtensions: (...args: unknown[]) => mockUpdatePiExtensions(...args),
|
||||
}));
|
||||
@@ -91,6 +93,7 @@ describe("PiExtensionsManager", () => {
|
||||
// Default mock for fetchPiExtensions to return empty settings
|
||||
mockFetchPiExtensions.mockResolvedValue({ extensions: [], disabledIds: [], settingsPath: "" });
|
||||
mockUpdatePiExtensions.mockResolvedValue({ extensions: [], disabledIds: [], settingsPath: "" });
|
||||
mockReinstallFusionPiPackage.mockResolvedValue({ success: true, source: "npm:@runfusion/fusion" });
|
||||
});
|
||||
|
||||
describe("Rendering", () => {
|
||||
@@ -113,6 +116,15 @@ describe("PiExtensionsManager", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("renders reinstall Fusion skill action", async () => {
|
||||
mockFetchPiSettings.mockResolvedValueOnce(mockPiSettings);
|
||||
render(<PiExtensionsManager addToast={addToast} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "Reinstall Fusion skill" })).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("renders package list with source badges", async () => {
|
||||
mockFetchPiSettings.mockResolvedValueOnce(mockPiSettings);
|
||||
render(<PiExtensionsManager addToast={addToast} />);
|
||||
@@ -268,6 +280,55 @@ describe("PiExtensionsManager", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Reinstall Fusion action", () => {
|
||||
it("calls reinstall API and refreshes settings and extensions on success", async () => {
|
||||
mockFetchPiSettings.mockResolvedValue(mockPiSettings);
|
||||
mockFetchPiExtensions.mockResolvedValue({ extensions: [], disabledIds: [], settingsPath: "" });
|
||||
mockReinstallFusionPiPackage.mockResolvedValueOnce({ success: true, source: "npm:@runfusion/fusion" });
|
||||
|
||||
render(<PiExtensionsManager addToast={addToast} projectId="proj-123" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "Reinstall Fusion skill" })).toBeTruthy();
|
||||
});
|
||||
|
||||
const settingsCallsBefore = mockFetchPiSettings.mock.calls.length;
|
||||
const extensionCallsBefore = mockFetchPiExtensions.mock.calls.length;
|
||||
|
||||
await userEvent.click(screen.getByRole("button", { name: "Reinstall Fusion skill" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockReinstallFusionPiPackage).toHaveBeenCalledWith("proj-123");
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(mockFetchPiSettings.mock.calls.length).toBeGreaterThan(settingsCallsBefore);
|
||||
expect(mockFetchPiExtensions.mock.calls.length).toBeGreaterThan(extensionCallsBefore);
|
||||
});
|
||||
expect(addToast).toHaveBeenCalledWith("Fusion skill reinstalled successfully", "success");
|
||||
});
|
||||
|
||||
it("shows error toast and resets loading state when reinstall fails", async () => {
|
||||
mockFetchPiSettings.mockResolvedValue(mockPiSettings);
|
||||
mockReinstallFusionPiPackage.mockRejectedValueOnce(new Error("Boom"));
|
||||
|
||||
render(<PiExtensionsManager addToast={addToast} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "Reinstall Fusion skill" })).toBeTruthy();
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByRole("button", { name: "Reinstall Fusion skill" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(addToast).toHaveBeenCalledWith("Failed to reinstall Fusion skill: Boom", "error");
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "Reinstall Fusion skill" })).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Remove package", () => {
|
||||
it("calls updatePiSettings with package removed from list", async () => {
|
||||
mockFetchPiSettings.mockResolvedValueOnce(mockPiSettings);
|
||||
|
||||
@@ -36549,6 +36549,9 @@ html .column.drag-over * {
|
||||
}
|
||||
|
||||
.pi-ext-add-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-sm);
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user