feat(FN-2262): merge fusion/fn-2262 (auto-resolved)
- feat(FN-2262): document Paperclip runtime configuration and constraints - docs(FN-2261): update README with implementation details - feat(FN-2261): add paperclip runtime resolution compatibility tests - feat(FN-2261): add runtime adapter and registration tests - feat(FN-2261): integrate adapter into plugin entrypoint - feat(FN-2261): implement PaperclipRuntimeAdapter - fix(FN-2261): remove pi-coding-agent re-exports from types.ts - feat(FN-2261): define runtime types for Paperclip plugin - feat(FN-2260): merge fusion/fn-2260
This commit is contained in:
@@ -1,8 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { AlertTriangle, ExternalLink, Eye, Loader2, Monitor, Play, RefreshCw, RotateCw, ShieldAlert, Square } from "lucide-react";
|
||||
import type { DetectedDevServerCommand, DevServerState, DevServerSession } from "../api";
|
||||
import type { DetectedDevServerCommand } from "../api";
|
||||
import { useDevServer } from "../hooks/useDevServer";
|
||||
import { useDevServerConfig } from "../hooks/useDevServerConfig";
|
||||
import { useDevServerLogs } from "../hooks/useDevServerLogs";
|
||||
import { usePreviewEmbed } from "../hooks/usePreviewEmbed";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
@@ -105,80 +104,21 @@ function truncateCommand(command: string): string {
|
||||
}
|
||||
|
||||
export function DevServerView({ addToast, projectId }: DevServerViewProps) {
|
||||
const devServer = useDevServer(projectId);
|
||||
const {
|
||||
config,
|
||||
loading: configLoading,
|
||||
error: configError,
|
||||
selectScript,
|
||||
clearSelection,
|
||||
setPreviewUrlOverride,
|
||||
refresh: refreshConfig,
|
||||
} = useDevServerConfig(projectId);
|
||||
|
||||
const legacyServerState = (devServer.serverState as DevServerState | null | undefined) ?? null;
|
||||
const session = devServer.session ?? (legacyServerState ? normalizeLegacyServerState(legacyServerState) : null);
|
||||
const detectedCommands = devServer.detectedCommands ?? devServer.candidates ?? [];
|
||||
const isLoading = devServer.isLoading ?? devServer.loading ?? configLoading;
|
||||
const error = devServer.error ?? configError ?? null;
|
||||
|
||||
const startServer = useCallback(async (command: string, cwd?: string) => {
|
||||
if (typeof devServer.startServer === "function") {
|
||||
await devServer.startServer(command, cwd);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof devServer.start === "function") {
|
||||
await devServer.start({ command, cwd });
|
||||
}
|
||||
}, [devServer]);
|
||||
|
||||
const stopServer = useCallback(async () => {
|
||||
if (typeof devServer.stopServer === "function") {
|
||||
await devServer.stopServer();
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof devServer.stop === "function") {
|
||||
await devServer.stop();
|
||||
}
|
||||
}, [devServer]);
|
||||
|
||||
const restartServer = useCallback(async () => {
|
||||
if (typeof devServer.restartServer === "function") {
|
||||
await devServer.restartServer();
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof devServer.restart === "function") {
|
||||
await devServer.restart();
|
||||
}
|
||||
}, [devServer]);
|
||||
|
||||
const detectCommands = useCallback(async () => {
|
||||
if (typeof devServer.detectCommands === "function") {
|
||||
await devServer.detectCommands();
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof devServer.detect === "function") {
|
||||
await devServer.detect();
|
||||
}
|
||||
}, [devServer]);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
if (typeof devServer.refresh === "function") {
|
||||
await devServer.refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof devServer.refreshStatus === "function") {
|
||||
await devServer.refreshStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
await refreshConfig();
|
||||
}, [devServer, refreshConfig]);
|
||||
session,
|
||||
sessions,
|
||||
logs,
|
||||
detectedCommands,
|
||||
previewUrl,
|
||||
isLoading,
|
||||
error,
|
||||
startServer,
|
||||
stopServer,
|
||||
restartServer,
|
||||
setPreviewUrl,
|
||||
detectCommands,
|
||||
refresh,
|
||||
} = useDevServer(projectId);
|
||||
|
||||
const status = session?.status ?? "stopped";
|
||||
const isRunning = status === "running" || status === "starting";
|
||||
@@ -193,9 +133,8 @@ export function DevServerView({ addToast, projectId }: DevServerViewProps) {
|
||||
loadMore: loadMoreLogs,
|
||||
} = useDevServerLogs(projectId, Boolean(projectId));
|
||||
|
||||
const manualPreviewUrl = config?.previewUrlOverride ?? legacyServerState?.manualPreviewUrl ?? null;
|
||||
const effectivePreviewUrl = manualPreviewUrl ?? devServer.previewUrl ?? session?.previewUrl ?? null;
|
||||
const selectedSource = config?.selectedSource ?? (session?.config?.cwd ? normalizeCwdToSource(session.config.cwd) : null);
|
||||
const effectivePreviewUrl = previewUrl;
|
||||
const selectedSource = session?.config?.cwd ?? null;
|
||||
|
||||
const [showCandidates, setShowCandidates] = useState(true);
|
||||
const [commandInput, setCommandInput] = useState("");
|
||||
@@ -206,15 +145,16 @@ export function DevServerView({ addToast, projectId }: DevServerViewProps) {
|
||||
const [previewMode, setPreviewMode] = useState<PreviewMode>("embedded");
|
||||
|
||||
const previewEmbedUrl = previewMode === "embedded" ? effectivePreviewUrl : null;
|
||||
const previewEmbed = usePreviewEmbed(previewEmbedUrl);
|
||||
const embedStatus = previewEmbed.embedStatus ?? "unknown";
|
||||
const setEmbedStatus = previewEmbed.setEmbedStatus ?? (() => undefined);
|
||||
const resetEmbedStatus = previewEmbed.resetEmbedStatus ?? (() => undefined);
|
||||
const iframeRef = previewEmbed.iframeRef ?? useRef<HTMLIFrameElement | null>(null);
|
||||
const isEmbedded = previewEmbed.isEmbedded ?? false;
|
||||
const isBlocked = previewEmbed.isBlocked ?? false;
|
||||
const blockReason = previewEmbed.blockReason ?? (previewEmbed as { embedContext?: string | null }).embedContext ?? null;
|
||||
const retry = previewEmbed.retry ?? (() => undefined);
|
||||
const {
|
||||
embedStatus,
|
||||
setEmbedStatus,
|
||||
resetEmbedStatus,
|
||||
iframeRef,
|
||||
isEmbedded,
|
||||
isBlocked,
|
||||
blockReason,
|
||||
retry,
|
||||
} = usePreviewEmbed(previewEmbedUrl);
|
||||
|
||||
const [showFallback, setShowFallback] = useState(false);
|
||||
const prevStatusRef = useRef(embedStatus);
|
||||
@@ -273,12 +213,6 @@ export function DevServerView({ addToast, projectId }: DevServerViewProps) {
|
||||
});
|
||||
}, [addToast, detectCommands]);
|
||||
|
||||
useEffect(() => {
|
||||
if (config?.selectedScript !== undefined) {
|
||||
setSelectedScript(config?.selectedScript ?? null);
|
||||
}
|
||||
}, [config?.selectedScript]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedScript) {
|
||||
setShowCandidates(false);
|
||||
@@ -366,40 +300,17 @@ export function DevServerView({ addToast, projectId }: DevServerViewProps) {
|
||||
}, [addToast]);
|
||||
|
||||
const handleSelectCandidate = useCallback((candidate: DetectedDevServerCommand) => {
|
||||
void (async () => {
|
||||
try {
|
||||
if (typeof selectScript === "function") {
|
||||
await selectScript({
|
||||
name: candidate.scriptName,
|
||||
command: candidate.command,
|
||||
source: normalizeCwdToSource(candidate.cwd),
|
||||
});
|
||||
}
|
||||
|
||||
setSelectedScript(candidate.scriptName);
|
||||
setShowCandidates(false);
|
||||
setCommandInput(candidate.command);
|
||||
addToast(`Selected ${candidate.scriptName} script.`, "success");
|
||||
} catch (selectionError) {
|
||||
addToast(normalizeError(selectionError), "error");
|
||||
}
|
||||
})();
|
||||
}, [addToast, selectScript]);
|
||||
setSelectedScript(candidate.scriptName);
|
||||
setShowCandidates(false);
|
||||
setCommandInput(candidate.command);
|
||||
addToast(`Selected ${candidate.scriptName} script.`, "success");
|
||||
}, [addToast]);
|
||||
|
||||
const handleClearSelection = useCallback(() => {
|
||||
void (async () => {
|
||||
try {
|
||||
if (typeof clearSelection === "function") {
|
||||
await clearSelection();
|
||||
}
|
||||
setSelectedScript(null);
|
||||
setShowCandidates(true);
|
||||
addToast("Cleared selected dev server script.", "success");
|
||||
} catch (clearError) {
|
||||
addToast(normalizeError(clearError), "error");
|
||||
}
|
||||
})();
|
||||
}, [addToast, clearSelection]);
|
||||
setSelectedScript(null);
|
||||
setShowCandidates(true);
|
||||
addToast("Cleared selected dev server script.", "success");
|
||||
}, [addToast]);
|
||||
|
||||
const handleStart = () => {
|
||||
const trimmedCommand = commandInput.trim();
|
||||
@@ -409,6 +320,7 @@ export function DevServerView({ addToast, projectId }: DevServerViewProps) {
|
||||
}
|
||||
|
||||
const fallbackCwd = normalizeSourceToCwd(selectedSource) ?? ".";
|
||||
const scriptName = selectedCandidate?.scriptName ?? selectedScript ?? "custom";
|
||||
const cwd = selectedCandidate?.cwd ?? fallbackCwd;
|
||||
|
||||
void runAction(
|
||||
@@ -432,18 +344,7 @@ export function DevServerView({ addToast, projectId }: DevServerViewProps) {
|
||||
|
||||
void runAction(
|
||||
"preview",
|
||||
async () => {
|
||||
if (typeof setPreviewUrlOverride === "function") {
|
||||
await setPreviewUrlOverride(nextUrl);
|
||||
}
|
||||
if (typeof devServer.setPreviewUrl === "function") {
|
||||
await devServer.setPreviewUrl(nextUrl);
|
||||
return;
|
||||
}
|
||||
if (typeof devServer.setManualUrl === "function") {
|
||||
await devServer.setManualUrl(nextUrl);
|
||||
}
|
||||
},
|
||||
() => setPreviewUrl(nextUrl),
|
||||
nextUrl ? "Preview URL updated." : "Preview URL override cleared.",
|
||||
);
|
||||
};
|
||||
@@ -454,7 +355,7 @@ export function DevServerView({ addToast, projectId }: DevServerViewProps) {
|
||||
}
|
||||
}, [error, refresh]);
|
||||
|
||||
const isManualPreviewOverride = Boolean(manualPreviewUrl);
|
||||
const isManualPreviewOverride = false; // With session model, previewUrl is always auto-detected
|
||||
|
||||
const startDisabled = status === "starting" || status === "running" || actionInFlight !== null;
|
||||
const stopDisabled = status === "stopped" || actionInFlight !== null;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
detectDevServerCommands,
|
||||
fetchDevServer,
|
||||
fetchDevServerLogs,
|
||||
fetchDevServers,
|
||||
getDevServerLogsStreamUrl,
|
||||
getDevServerSessionLogsStreamUrl,
|
||||
@@ -354,24 +355,17 @@ export function useDevServer(projectId?: string): UseDevServerReturn {
|
||||
if (contextVersionRef.current !== versionAtStart) {
|
||||
return;
|
||||
}
|
||||
const payload = parseJson<DevServerSession | { status?: DevServerSession["status"]; pid?: number }>(event.data);
|
||||
// If payload is a full session, use it directly
|
||||
if (payload && "config" in payload) {
|
||||
setSession(payload as DevServerSession);
|
||||
return;
|
||||
}
|
||||
// Otherwise, treat as partial update
|
||||
const partial = payload as { status?: DevServerSession["status"]; pid?: number } | undefined;
|
||||
const nextStatus = partial?.status;
|
||||
const payload = parseJson<{ status?: DevServerSession["status"]; pid?: number }>(event.data);
|
||||
const nextStatus = payload?.status;
|
||||
if (nextStatus) {
|
||||
setSession((prev) => (prev
|
||||
? {
|
||||
...prev,
|
||||
status: nextStatus,
|
||||
runtime: partial?.pid
|
||||
runtime: payload.pid
|
||||
? {
|
||||
...(prev.runtime ?? { startedAt: new Date().toISOString() }),
|
||||
pid: partial.pid,
|
||||
pid: payload.pid,
|
||||
}
|
||||
: prev.runtime,
|
||||
}
|
||||
@@ -650,15 +644,6 @@ export function useDevServer(projectId?: string): UseDevServerReturn {
|
||||
await refresh();
|
||||
}, [refresh]);
|
||||
|
||||
// Reset helper for tests: incrementing resetVersion signals hook to re-initialize
|
||||
useEffect(() => {
|
||||
const version = resetVersion;
|
||||
return () => {
|
||||
if (resetVersion !== version) {
|
||||
contextVersionRef.current += 1;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
const previewUrl = extractPreviewUrl(session);
|
||||
const serverState = session ? { ...session, pid: session.runtime?.pid } : null;
|
||||
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type RefObject } from "react";
|
||||
|
||||
export type EmbedStatus = "unknown" | "loading" | "embedded" | "blocked" | "error";
|
||||
export type EmbedDetectionMethod = "auto" | "manual" | null;
|
||||
|
||||
interface UsePreviewEmbedOptions {
|
||||
loadTimeoutMs?: number;
|
||||
detectionMethod?: EmbedDetectionMethod;
|
||||
}
|
||||
|
||||
interface UsePreviewEmbedResult {
|
||||
embedStatus: EmbedStatus;
|
||||
setEmbedStatus: (status: EmbedStatus) => void;
|
||||
resetEmbedStatus: () => void;
|
||||
retry: () => void;
|
||||
isEmbedded: boolean;
|
||||
isBlocked: boolean;
|
||||
blockReason: string | null;
|
||||
detectionMethod: EmbedDetectionMethod;
|
||||
iframeRef: RefObject<HTMLIFrameElement | null>;
|
||||
resetEmbedStatus: () => void;
|
||||
// Extended API for direct status control (backward compatibility)
|
||||
setEmbedStatus: (status: EmbedStatus) => void;
|
||||
retry: () => void;
|
||||
// Legacy aliases for backward compatibility
|
||||
embedContext: string | null;
|
||||
handleIframeLoad: () => void;
|
||||
handleIframeError: () => void;
|
||||
isEmbedded: boolean;
|
||||
@@ -40,13 +49,14 @@ function getContextForStatus(status: EmbedStatus): string | null {
|
||||
}
|
||||
|
||||
export function usePreviewEmbed(url: string | null, options: UsePreviewEmbedOptions = {}): UsePreviewEmbedResult {
|
||||
const loadTimeoutMs = options.loadTimeoutMs ?? DEFAULT_LOAD_TIMEOUT_MS;
|
||||
const { loadTimeoutMs = 10000, detectionMethod: initialDetectionMethod = null } = options;
|
||||
|
||||
const iframeRef = useRef<HTMLIFrameElement | null>(null);
|
||||
const timeoutRef = useRef<number | null>(null);
|
||||
|
||||
const [embedStatus, setEmbedStatusState] = useState<EmbedStatus>("unknown");
|
||||
const [embedContext, setEmbedContext] = useState<string | null>(null);
|
||||
const [blockReason, setBlockReason] = useState<string | null>(null);
|
||||
const [detectionMethod, setDetectionMethod] = useState<EmbedDetectionMethod>(initialDetectionMethod);
|
||||
|
||||
const clearLoadingTimeout = useCallback(() => {
|
||||
if (timeoutRef.current !== null) {
|
||||
@@ -57,20 +67,61 @@ export function usePreviewEmbed(url: string | null, options: UsePreviewEmbedOpti
|
||||
|
||||
const setEmbedStatus = useCallback((status: EmbedStatus) => {
|
||||
setEmbedStatusState(status);
|
||||
setEmbedContext(getContextForStatus(status));
|
||||
setBlockReason(defaultContextForStatus(status));
|
||||
}, []);
|
||||
|
||||
const resetEmbedStatus = useCallback(() => {
|
||||
clearLoadingTimeout();
|
||||
setEmbedStatusState("unknown");
|
||||
setEmbedContext(null);
|
||||
}, [clearLoadingTimeout]);
|
||||
const setBlockedByTimeout = useCallback(() => {
|
||||
setEmbedStatusState("blocked");
|
||||
setBlockReason(TIMEOUT_CONTEXT);
|
||||
}, []);
|
||||
|
||||
const retry = useCallback(() => {
|
||||
useEffect(() => {
|
||||
clearLoadingTimeout();
|
||||
|
||||
if (!url) {
|
||||
setEmbedStatusState("unknown");
|
||||
setBlockReason(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setEmbedStatusState("unknown");
|
||||
setEmbedContext(null);
|
||||
}, [clearLoadingTimeout]);
|
||||
setBlockReason(null);
|
||||
|
||||
let canceled = false;
|
||||
queueMicrotask(() => {
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
setEmbedStatusState("loading");
|
||||
setBlockReason(null);
|
||||
});
|
||||
|
||||
return () => {
|
||||
canceled = true;
|
||||
clearLoadingTimeout();
|
||||
};
|
||||
}, [clearLoadingTimeout, url]);
|
||||
|
||||
useEffect(() => {
|
||||
if (embedStatus !== "loading") {
|
||||
clearLoadingTimeout();
|
||||
return;
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
timeoutRef.current = null;
|
||||
setBlockedByTimeout();
|
||||
}, loadTimeoutMs);
|
||||
|
||||
timeoutRef.current = timer;
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
if (timeoutRef.current === timer) {
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [clearLoadingTimeout, embedStatus, loadTimeoutMs, setBlockedByTimeout]);
|
||||
|
||||
const handleIframeLoad = useCallback(() => {
|
||||
const iframeEl = iframeRef.current;
|
||||
@@ -98,6 +149,9 @@ export function usePreviewEmbed(url: string | null, options: UsePreviewEmbedOpti
|
||||
|
||||
useEffect(() => {
|
||||
clearLoadingTimeout();
|
||||
setEmbedStatusState("unknown");
|
||||
setBlockReason(null);
|
||||
}, [clearLoadingTimeout]);
|
||||
|
||||
if (!url) {
|
||||
setEmbedStatusState("unknown");
|
||||
@@ -111,6 +165,9 @@ export function usePreviewEmbed(url: string | null, options: UsePreviewEmbedOpti
|
||||
|
||||
useEffect(() => {
|
||||
clearLoadingTimeout();
|
||||
setEmbedStatusState("unknown");
|
||||
setBlockReason(null);
|
||||
}, [clearLoadingTimeout]);
|
||||
|
||||
if (!url || embedStatus !== "loading") {
|
||||
return;
|
||||
@@ -134,8 +191,15 @@ export function usePreviewEmbed(url: string | null, options: UsePreviewEmbedOpti
|
||||
|
||||
return {
|
||||
embedStatus,
|
||||
setEmbedStatus,
|
||||
isEmbedded,
|
||||
isBlocked,
|
||||
blockReason,
|
||||
detectionMethod,
|
||||
iframeRef,
|
||||
resetEmbedStatus,
|
||||
// Legacy aliases
|
||||
setEmbedStatus,
|
||||
embedContext: blockReason, // Alias for backward compatibility
|
||||
retry,
|
||||
iframeRef,
|
||||
handleIframeLoad,
|
||||
|
||||
@@ -822,6 +822,98 @@ describe("PluginRunner", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Paperclip runtime compatibility", () => {
|
||||
/**
|
||||
* Verify that the paperclip runtime registration from
|
||||
* plugins/fusion-plugin-paperclip-runtime is correctly resolvable
|
||||
* through the engine's runtime resolution system.
|
||||
*/
|
||||
|
||||
it("should resolve paperclip runtime when registered", () => {
|
||||
const paperclipRuntime = {
|
||||
metadata: {
|
||||
runtimeId: "paperclip",
|
||||
name: "Paperclip Runtime",
|
||||
description: "Paperclip-backed AI session using the user's configured pi provider and model",
|
||||
version: "1.0.0",
|
||||
},
|
||||
factory: vi.fn().mockResolvedValue({}),
|
||||
};
|
||||
mockPluginLoader.getPluginRuntimes.mockReturnValue([
|
||||
{ pluginId: "fusion-plugin-paperclip-runtime", runtime: paperclipRuntime as any },
|
||||
]);
|
||||
|
||||
const result = pluginRunner.getRuntimeById("paperclip");
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.pluginId).toBe("fusion-plugin-paperclip-runtime");
|
||||
expect(result?.runtime.metadata.runtimeId).toBe("paperclip");
|
||||
expect(result?.runtime.metadata.name).toBe("Paperclip Runtime");
|
||||
expect(result?.runtime.metadata.description).toContain("Paperclip");
|
||||
expect(result?.runtime.metadata.version).toBe("1.0.0");
|
||||
});
|
||||
|
||||
it("should expose paperclip runtime metadata correctly", () => {
|
||||
const paperclipRuntime = {
|
||||
metadata: {
|
||||
runtimeId: "paperclip",
|
||||
name: "Paperclip Runtime",
|
||||
description: "Paperclip-backed AI session using the user's configured pi provider and model",
|
||||
version: "1.0.0",
|
||||
},
|
||||
factory: vi.fn().mockResolvedValue({}),
|
||||
};
|
||||
mockPluginLoader.getPluginRuntimes.mockReturnValue([
|
||||
{ pluginId: "fusion-plugin-paperclip-runtime", runtime: paperclipRuntime as any },
|
||||
]);
|
||||
|
||||
const runtimes = pluginRunner.getPluginRuntimes();
|
||||
const paperclip = runtimes.find(r => r.runtime.metadata.runtimeId === "paperclip");
|
||||
|
||||
expect(paperclip).toBeDefined();
|
||||
expect(paperclip?.runtime.metadata).toEqual({
|
||||
runtimeId: "paperclip",
|
||||
name: "Paperclip Runtime",
|
||||
description: "Paperclip-backed AI session using the user's configured pi provider and model",
|
||||
version: "1.0.0",
|
||||
});
|
||||
});
|
||||
|
||||
it("should allow factory invocation for paperclip runtime", async () => {
|
||||
const mockAdapter = {
|
||||
id: "paperclip",
|
||||
name: "Paperclip Runtime",
|
||||
createSession: vi.fn(),
|
||||
promptWithFallback: vi.fn(),
|
||||
describeModel: vi.fn(),
|
||||
dispose: vi.fn(),
|
||||
};
|
||||
const paperclipRuntime = {
|
||||
metadata: {
|
||||
runtimeId: "paperclip",
|
||||
name: "Paperclip Runtime",
|
||||
description: "Paperclip-backed AI session using the user's configured pi provider and model",
|
||||
version: "1.0.0",
|
||||
},
|
||||
factory: vi.fn().mockResolvedValue(mockAdapter),
|
||||
};
|
||||
mockPluginLoader.getPluginRuntimes.mockReturnValue([
|
||||
{ pluginId: "fusion-plugin-paperclip-runtime", runtime: paperclipRuntime as any },
|
||||
]);
|
||||
|
||||
const result = pluginRunner.getRuntimeById("paperclip");
|
||||
expect(result).toBeDefined();
|
||||
|
||||
// Invoke the factory (simulating runtime instantiation)
|
||||
const context = { pluginId: "fusion-plugin-paperclip-runtime" };
|
||||
const runtime = await result!.runtime.factory(context as any);
|
||||
|
||||
expect(paperclipRuntime.factory).toHaveBeenCalledWith(context);
|
||||
expect(runtime).toBe(mockAdapter);
|
||||
expect(runtime.id).toBe("paperclip");
|
||||
expect(runtime.name).toBe("Paperclip Runtime");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getLoader() / getStore()", () => {
|
||||
it("should return the plugin loader", () => {
|
||||
const loader = pluginRunner.getLoader();
|
||||
|
||||
Reference in New Issue
Block a user