import { describe, expect, it } from "vitest"; import { useRevealedText } from "@/hooks/use-revealed-text"; import { runPluginClientBundle, type PluginClientRuntime } from "./evaluate"; const runtime = { paseo: {}, async rpc() {}, openSettings() {}, openScreen() {}, openSurface() {}, openPanel() {}, addHeaderButton() { return { update() {}, remove() {} }; }, addComposerPill() { return { update() {}, remove() {} }; }, } as unknown as PluginClientRuntime; function evaluatePluginClientBundle(id: string, source: string) { return runPluginClientBundle(id, source, runtime); } function bundle(body: string): string { return `(function(require) { const module = { exports: {} }; module.exports.default = function(plugin) { ${body}; return function() {}; }; return module.exports; })`; } describe("evaluatePluginClientBundle", () => { it("releases button registrations when client setup throws", () => { let active = 0; function addButton() { active++; return { update() {}, remove() { active--; }, }; } expect(() => runPluginClientBundle( "failed-buttons", bundle(` plugin.addHeaderButton({ id: "header", workspaceId: "workspace", button: { title: "Header", icon: "Scan", behavior: { kind: "action", onPress() {} } } }); plugin.addComposerPill({ id: "pill", workspaceId: "workspace", agentId: "agent", button: { title: "Pill", icon: "Scan", behavior: { kind: "action", onPress() {} } } }); throw new Error("Setup failed"); `), { ...runtime, addHeaderButton: addButton, addComposerPill: addButton }, ), ).toThrow("Setup failed"); expect(active).toBe(0); }); it("accepts memoized settings screens", () => { const plugin = evaluatePluginClientBundle( "settings", bundle(` const Component = require("react").memo(function Settings() { return null; }); plugin.addSettingsScreen({ id: "display", title: "Display", icon: "Settings", Component }); `), ); expect(plugin.settingsScreens.map((screen) => screen.id)).toEqual(["display"]); }); it("returns idempotent removers for every client registration", () => { let pillCount = 0; const plugin = runPluginClientBundle( "removals", bundle(` function Component() { return null; } const schema = { safeParse(value) { return { success: true, data: value }; } }; globalThis.__pluginRemovals = [ plugin.addScreen({ id: "main", title: "Main", Component }), plugin.addSettingsScreen({ id: "display", title: "Display", icon: "Settings", Component }), plugin.addSidebarHeaderItem({ id: "main", title: "Main", Component }), plugin.addSidebarFooterItem({ id: "status", title: "Status", Component }), plugin.addWorkspacePanel({ id: "panel", title: "Panel", icon: "Blocks", context: "workspace", Component }), plugin.addCommandCenterItem({ id: "command", title: "Command", icon: "Blocks", context: "global", onSelect() {} }), plugin.addSlashCommand({ name: "review", description: "Review", argumentHint: "", context: "workspace", onSubmit() {} }), plugin.addComposerPill({ id: "pill", workspaceId: "workspace", agentId: "agent", button: { title: "Pill", icon: "Scan", behavior: { kind: "action", onPress() {} } } }).remove, plugin.addAttachmentSource({ id: "issues", title: "Issues", icon: "Blocks", pickerTitle: "Attach issue", searchPlaceholder: "Search", search: { name: "issues.search", input: {}, output: {} } }), plugin.addTheme({ id: "night", name: "Night", appearance: "dark", colors: { background: "#000", foreground: "#fff", raised: "#111", control: "#222", border: "#333", mutedForeground: "#aaa", ring: "#555" } }), plugin.addTimelineTransformer({ id: "transformer", query: { itemType: "tool_call" }, transform() { return { items: [] }; } }), plugin.addTimelineRenderer({ kind: "card", version: 1, schema, Component }), ]; `), { ...runtime, addComposerPill() { pillCount += 1; return { update() {}, remove() { pillCount -= 1; }, }; }, }, ); const removals = Reflect.get(globalThis, "__pluginRemovals") as Array<() => void>; expect( [ plugin.surfaces, plugin.settingsScreens, plugin.sidebarItems.header, plugin.sidebarItems.footer, plugin.workspacePanels, plugin.commandCenterItems, plugin.clientSlashCommands, plugin.attachmentSources, plugin.themes, plugin.timelineTransformers, plugin.timelineRenderers, ].every((items) => items.length === 1), ).toBe(true); expect(pillCount).toBe(1); for (const remove of removals) { remove(); remove(); } expect( [ plugin.surfaces, plugin.settingsScreens, plugin.sidebarItems.header, plugin.sidebarItems.footer, plugin.workspacePanels, plugin.commandCenterItems, plugin.clientSlashCommands, plugin.attachmentSources, plugin.themes, plugin.timelineTransformers, plugin.timelineRenderers, ].every((items) => items.length === 0), ).toBe(true); expect(pillCount).toBe(0); Reflect.deleteProperty(globalThis, "__pluginRemovals"); }); it.each(["tool_call", "notification"])("collects %s transformers and renderers", (itemType) => { const plugin = evaluatePluginClientBundle( "reports", bundle(` function Card() { return null; } const schema = { safeParse(value) { return { success: true, data: value }; } }; plugin.addTimelineTransformer({ id: "test-report", query: { itemType: "${itemType}" }, transform() { return { items: [] }; }, }); plugin.addTimelineRenderer({ kind: "test-report", version: 1, schema, Component: Card, }); `), ); expect(plugin.timelineTransformers.map(({ id, query }) => ({ id, query }))).toEqual([ { id: "test-report", query: { itemType } }, ]); expect(plugin.timelineRenderers.map(({ kind, version }) => ({ kind, version }))).toEqual([ { kind: "test-report", version: 1 }, ]); }); it("rejects unknown timeline item types", () => { expect(() => evaluatePluginClientBundle( "reports", bundle(` plugin.addTimelineTransformer({ id: "bad-query", query: { itemType: "settled" }, transform() { return { items: [] }; }, }); `), ), ).toThrow("Timeline transformer bad-query has invalid item type: settled"); }); it("collects screens and sidebar header and footer items", () => { const plugin = evaluatePluginClientBundle( "example", bundle(` function Screen() { return null; } const Item = require("react").memo(function Item() { return null; }); plugin.addScreen({ id: "main", title: " Main ", Component: Screen }); plugin.addScreen({ id: "bot", title: (params) => params.botId, Component: Screen }); plugin.addSidebarHeaderItem({ id: "main", title: " Example ", Component: Item }); plugin.addSidebarFooterItem({ id: "main", title: "Status", Component: Item }); `), ); expect(plugin.id).toBe("example"); expect(plugin.surfaces.map((surface) => surface.id)).toEqual(["main", "bot"]); expect(plugin.surfaces[0]?.title).toBe("Main"); expect(plugin.surfaces[1]?.title).toBeTypeOf("function"); expect(plugin.sidebarItems.header.map(({ id, title }) => ({ id, title }))).toEqual([ { id: "main", title: "Example" }, ]); expect(plugin.sidebarItems.footer.map(({ id, title }) => ({ id, title }))).toEqual([ { id: "main", title: "Status" }, ]); expect(plugin.legacySidebarItems).toEqual([]); }); it.each([ [`plugin.addScreen("main", Screen);`, "addScreen takes { id, title, Component }"], [`plugin.addScreen({ id: "main", Component: Screen });`, "Screen main needs a title"], [ `plugin.addScreen({ id: "main", title: " ", Component: Screen });`, "Screen main needs a title", ], [ `plugin.addScreen({ id: "main", title: 42, Component: Screen });`, "Screen main needs a title", ], [ `plugin.addScreen({ id: "main", title: "Main", Component: "Main" });`, "Screen main is not a component", ], ])("rejects a malformed screen: %s", (call, message) => { expect(() => evaluatePluginClientBundle("example", bundle(`function Screen() { return null; } ${call}`)), ).toThrow(message); }); it("rejects duplicate and malformed sidebar items within a section", () => { expect(() => evaluatePluginClientBundle( "example", bundle(` function Item() { return null; } plugin.addSidebarFooterItem({ id: "main", title: "One", Component: Item }); plugin.addSidebarFooterItem({ id: "main", title: "Two", Component: Item }); `), ), ).toThrow("Duplicate sidebar footer item: main"); expect(() => evaluatePluginClientBundle( "example", bundle(`plugin.addSidebarHeaderItem({ id: "main", title: "Main", Component: "Main" });`), ), ).toThrow("Sidebar item main is not a component"); expect(() => evaluatePluginClientBundle( "example", bundle( `plugin.addSidebarHeaderItem({ id: "main", title: " ", Component() { return null; } });`, ), ), ).toThrow("Sidebar item main has no title"); }); it("expands the addSurface and addSidebarItem aliases to a screen and a legacy header item", () => { const plugin = evaluatePluginClientBundle( "example", bundle(` function Surface() { return null; } plugin.addSurface("main", Surface); plugin.addSidebarItem({ id: "entry", title: "Example", icon: "Blocks", surface: "main" }); `), ); expect(plugin.surfaces.map(({ id, title }) => ({ id, title }))).toEqual([ { id: "main", title: "main" }, ]); expect(plugin.sidebarItems).toEqual({ header: [], footer: [] }); expect(plugin.legacySidebarItems).toEqual([ { id: "entry", title: "Example", icon: "Blocks", surface: "main" }, ]); }); it("releases an addSidebarItem alias's header id", () => { const plugin = evaluatePluginClientBundle( "example", bundle(` plugin.addScreen({ id: "main", title: "Main", Component: function Screen() { return null; } }); const remove = plugin.addSidebarItem({ id: "entry", title: "Example", icon: "Blocks", surface: "main" }); remove(); plugin.addSidebarHeaderItem({ id: "entry", title: "Replacement", Component() { return null; } }); `), ); expect(plugin.sidebarItems.header.map((item) => item.title)).toEqual(["Replacement"]); expect(plugin.legacySidebarItems).toEqual([]); }); it("rejects a header item that reuses an addSidebarItem id", () => { expect(() => evaluatePluginClientBundle( "example", bundle(` plugin.addScreen({ id: "main", title: "Main", Component: function Screen() { return null; } }); plugin.addSidebarItem({ id: "entry", title: "Example", icon: "Blocks", surface: "main" }); plugin.addSidebarHeaderItem({ id: "entry", title: "Other", Component() { return null; } }); `), ), ).toThrow("Duplicate sidebar header item: entry"); }); it("collects a declarative attachment source", () => { const plugin = evaluatePluginClientBundle( "linear", bundle(` plugin.addAttachmentSource({ id: "issues", title: "Linear issue", icon: "CircleDot", pickerTitle: "Attach Linear issue", searchPlaceholder: "Search by identifier or title", search: { name: "issues.search", input: {}, output: {} }, }); `), ); expect(plugin.attachmentSources).toEqual([ { id: "issues", title: "Linear issue", icon: "CircleDot", pickerTitle: "Attach Linear issue", searchPlaceholder: "Search by identifier or title", search: { name: "issues.search", input: {}, output: {} }, }, ]); }); it("collects contextual workspace panels and Command Center items", () => { const plugin = evaluatePluginClientBundle( "review", bundle(` function ReviewPanel() { return null; } plugin.addWorkspacePanel({ id: "review", title: "Review", icon: "Scan", context: "agent", Component: ReviewPanel, }); plugin.addCommandCenterItem({ id: "open-review", title: "Open review", icon: "Scan", context: "agent", onSelect() {}, }); `), ); expect( plugin.workspacePanels.map(({ id, title, icon, context, locations }) => ({ id, title, icon, context, locations, })), ).toEqual([ { id: "review", title: "Review", icon: "Scan", context: "agent", locations: ["workspace"], }, ]); expect( plugin.commandCenterItems.map(({ id, title, icon, context }) => ({ id, title, icon, context, })), ).toEqual([{ id: "open-review", title: "Open review", icon: "Scan", context: "agent" }]); }); it("registers workspace and agent panels with stable aliases", () => { const plugin = evaluatePluginClientBundle( "panels", bundle(` const Component = () => null; plugin.addWorkspacePanel({ id: "workspace", title: "Workspace", icon: "File", Component }); plugin.addAgentPanel({ id: "agent", title: "Agent", icon: "File", Component }); plugin.addWorkspacePanel({ id: "legacy", title: "Legacy", icon: "File", context: "agent", Component }); `), ); expect(plugin.workspacePanels.map(({ id, context }) => ({ id, context }))).toEqual([ { id: "workspace", context: "workspace" }, { id: "agent", context: "agent" }, { id: "legacy", context: "agent" }, ]); }); it("registers inline SVG and component icons", () => { const svg = ''; const plugin = evaluatePluginClientBundle( "review", bundle(` function ReviewPanel() { return null; } function BadgeIcon() { return null; } plugin.addWorkspacePanel({ id: "review", title: "Review", icon: { svg: ${JSON.stringify(` ${svg}\n`)} }, context: "workspace", Component: ReviewPanel, }); plugin.addCommandCenterItem({ id: "open-review", title: "Open review", icon: BadgeIcon, context: "global", onSelect() {}, }); plugin.addAttachmentSource({ id: "issues", title: "Issues", icon: { svg: ${JSON.stringify(svg)} }, pickerTitle: "Attach issue", searchPlaceholder: "Search", search: { name: "issues.search", input: {}, output: {} }, }); `), ); expect(plugin.workspacePanels[0]?.icon).toEqual({ svg }); expect(plugin.commandCenterItems[0]?.icon).toHaveProperty("name", "BadgeIcon"); expect(plugin.attachmentSources[0]?.icon).toEqual({ svg }); }); it.each([ [ "workspace panel", `plugin.addWorkspacePanel({ id: "review", title: "Review", icon: ICON, context: "workspace", Component() { return null; } })`, "Workspace panel review has an invalid icon: script elements are not allowed", ], [ "Command Center item", `plugin.addCommandCenterItem({ id: "review", title: "Review", icon: ICON, context: "global", onSelect() {} })`, "Command Center item review has an invalid icon: script elements are not allowed", ], [ "settings screen", `plugin.addSettingsScreen({ id: "review", title: "Review", icon: ICON, Component() { return null; } })`, "Settings screen review has an invalid icon: script elements are not allowed", ], [ "attachment source", `plugin.addAttachmentSource({ id: "review", title: "Review", icon: ICON, pickerTitle: "Pick", searchPlaceholder: "Search", search: { name: "issues.search", input: {}, output: {} } })`, "Attachment source review has an invalid icon: script elements are not allowed", ], ])("fails the plugin when a %s registers an unsafe SVG", (_kind, registration, message) => { const icon = JSON.stringify({ svg: "" }); expect(() => evaluatePluginClientBundle("review", bundle(registration.replace("ICON", icon))), ).toThrow(message); }); it("normalizes and validates workspace panel locations", () => { const plugin = evaluatePluginClientBundle( "review", bundle(` function ReviewPanel() { return null; } plugin.addWorkspacePanel({ id: "review", title: "Review", icon: "Scan", context: "agent", locations: ["workspace", "explorer"], Component: ReviewPanel, }); `), ); expect(plugin.workspacePanels[0]?.locations).toEqual(["workspace", "explorer"]); for (const [locations, message] of [ ["[]", "must support at least one location"], ['["sidebar"]', "has invalid location: sidebar"], ['["explorer", "explorer"]', "has duplicate locations"], ] as const) { expect(() => evaluatePluginClientBundle( "review", bundle(` function ReviewPanel() { return null; } plugin.addWorkspacePanel({ id: "review", title: "Review", icon: "Scan", context: "agent", locations: ${locations}, Component: ReviewPanel, }); `), ), ).toThrow(message); } }); it("rejects duplicate workspace panel and Command Center ids", () => { expect(() => evaluatePluginClientBundle( "review", bundle(` function Panel() { return null; } const panel = { id: "review", title: "Review", icon: "Scan", context: "workspace", Component: Panel }; plugin.addWorkspacePanel(panel); plugin.addWorkspacePanel(panel); `), ), ).toThrow("Duplicate workspace panel: review"); expect(() => evaluatePluginClientBundle( "review", bundle(` const item = { id: "review", title: "Review", icon: "Scan", context: "global", onSelect() {} }; plugin.addCommandCenterItem(item); plugin.addCommandCenterItem(item); `), ), ).toThrow("Duplicate Command Center item: review"); }); it("runs the client entry with the full runtime context", () => { const plugin = evaluatePluginClientBundle( "review", bundle(` if (!plugin.paseo || !plugin.rpc || !plugin.openSurface || !plugin.openPanel || !plugin.addComposerPill) { throw new Error("missing client runtime"); } `), ); expect(plugin.id).toBe("review"); }); it("rejects duplicate attachment source ids", () => { expect(() => evaluatePluginClientBundle( "linear", bundle(` const source = { id: "issues", title: "Linear issue", icon: "CircleDot", pickerTitle: "Attach Linear issue", searchPlaceholder: "Search", search: { name: "issues.search", input: {}, output: {} }, }; plugin.addAttachmentSource(source); plugin.addAttachmentSource(source); `), ), ).toThrow("Duplicate attachment source: issues"); }); it("collects a contributed theme", () => { const plugin = evaluatePluginClientBundle( "catppuccin", bundle(` plugin.addTheme({ id: "mocha", name: "Catppuccin Mocha", appearance: "dark", colors: { background: "#1e1e2e", foreground: "#cdd6f4", raised: "#313244", control: "#45475a", border: "#45475a", accent: "#cba6f7", mutedForeground: "#a6adc8", ring: "#6c7086", }, }); `), ); expect(plugin.themes.map((theme) => [theme.id, theme.name])).toEqual([ ["mocha", "Catppuccin Mocha"], ]); expect(plugin.themes[0]?.colors.accent).toBe("#cba6f7"); }); it("rejects a theme with a color that is not a hex value", () => { expect(() => evaluatePluginClientBundle( "catppuccin", bundle(` plugin.addTheme({ id: "mocha", name: "Catppuccin Mocha", appearance: "dark", colors: { background: "rebeccapurple", foreground: "#cdd6f4", raised: "#313244", control: "#45475a", border: "#45475a", mutedForeground: "#a6adc8", ring: "#6c7086", }, }); `), ), ).toThrow("Must be a hex color"); }); it("rejects duplicate theme ids", () => { expect(() => evaluatePluginClientBundle( "catppuccin", bundle(` const theme = { id: "mocha", name: "Catppuccin Mocha", appearance: "dark", colors: { background: "#1e1e2e", foreground: "#cdd6f4", raised: "#313244", control: "#45475a", border: "#45475a", mutedForeground: "#a6adc8", ring: "#6c7086", }, }; plugin.addTheme(theme); plugin.addTheme(theme); `), ), ).toThrow("Duplicate theme: mocha"); }); it("rejects a sidebar placement whose surface does not exist", () => { expect(() => evaluatePluginClientBundle( "example", bundle(` plugin.addSidebarItem({ id: "main", title: "Example", icon: "Blocks", surface: "missing" }); `), ), ).toThrow("references missing surface missing"); }); it("rejects a bundle without a default contribution function", () => { expect(() => evaluatePluginClientBundle("example", `(function() { return {}; })`)).toThrow( "must default export a function", ); }); it("requires a cleanup function", () => { expect(() => evaluatePluginClientBundle("example", `(function() { return { default: function() {} }; })`), ).toThrow("must return a cleanup function"); }); it("provides the host Icon component through @getpaseo/plugin/client/ui", () => { const plugin = evaluatePluginClientBundle( "example", `(function(require) { const { Icon } = require("@getpaseo/plugin/client/ui"); const module = { exports: {} }; module.exports.default = function(plugin) { plugin.addScreen({ id: "main", title: "Main", Component: function Surface() { return Icon({ name: "Settings", size: 18, color: "#123456" }); } }); return function() {}; }; return module.exports; })`, ); const Component = plugin.surfaces[0]?.Component; expect(Component).toBeTypeOf("function"); const element = (Component as (props: never) => { props: unknown })({} as never); expect(element).toMatchObject({ props: { size: 18, color: "#123456" } }); }); it("provides Paseo UI through @getpaseo/plugin/client/ui", () => { const plugin = evaluatePluginClientBundle( "example", `(function(require) { const { Icon, Modal, useToast, SettingsSection, SidebarRow, StreamedText } = require("@getpaseo/plugin/client/ui"); const module = { exports: {} }; module.exports.default = function(plugin) { if (typeof Icon !== "function" || typeof Modal !== "function" || typeof Modal.Content !== "function" || typeof useToast !== "function" || typeof SettingsSection !== "function" || typeof SidebarRow !== "function" || typeof StreamedText !== "function") { throw new Error("React Native plugin UI is incomplete"); } plugin.addSurface("main", function Surface() { return null; }); return function() {}; }; return module.exports; })`, ); expect(plugin.surfaces.map((surface) => surface.id)).toEqual(["main"]); }); it("keeps the host reveal hook on the deprecated client/react-native entry", () => { const plugin = evaluatePluginClientBundle( "legacy-reveal", bundle(` const legacy = require("@getpaseo/plugin/client/react-native"); const ui = require("@getpaseo/plugin/client/ui"); if (typeof legacy.useRevealedText !== "function") throw new Error("Missing legacy reveal hook"); if (legacy.StreamedText !== ui.StreamedText) throw new Error("Missing current UI exports"); if ("useRevealedText" in ui) throw new Error("Legacy hook leaked into current UI"); plugin.addScreen({ id: "reveal", title: "Reveal", Component: legacy.useRevealedText }); `), ); expect(plugin.surfaces[0]?.Component).toBe(useRevealedText); }); it("keeps shared and client runtime exports separate", () => { expect(() => evaluatePluginClientBundle( "example", `(function(require) { const shared = require("@getpaseo/plugin"); const client = require("@getpaseo/plugin/client"); const { ExternalLink, Loading, Markdown } = require("@getpaseo/plugin/client/ui"); if (typeof Markdown !== "function") throw new Error("Markdown"); if (typeof ExternalLink !== "function") throw new Error("ExternalLink"); if (typeof Loading !== "function") throw new Error("Loading"); for (const name of ["usePaseo", "useRpc", "useSettings", "useAgent", "useWorkspace", "openExternalUrl", "useFileText", "useFileBytes", "useFileUrl", "PluginFileConflictError", "PluginFileDeletedError"]) { if (name in shared || typeof client[name] !== "function") throw new Error(name); } if ("Icon" in shared || typeof shared.PluginAttachmentItemSchema.parse !== "function") throw new Error("shared exports"); return { default() { return () => {}; } }; })`, ), ).not.toThrow(); }); it.each([ "@getpaseo/plugin/server", "@getpaseo/plugin/server/provider", "@getpaseo/plugin/server/acp", "@getpaseo/plugin/client/host", "@getpaseo/plugin/react-native", "@getpaseo/plugin/ui", "@getpaseo/plugin/host", "@paseo/plugin", ])("rejects %s in the client loader", (specifier) => { expect(() => evaluatePluginClientBundle( "example", `(function(require) { require("${specifier}"); return {}; })`, ), ).toThrow("not available in plugin client code"); }); it("resolves shared RPC helpers from @getpaseo/plugin", () => { const plugin = evaluatePluginClientBundle( "example", `(function(require) { const { defineRpc, defineAttachmentSource } = require("@getpaseo/plugin"); const search = defineRpc({ name: "issues.search", input: {}, output: {} }); const module = { exports: {} }; module.exports.default = function(plugin) { plugin.addAttachmentSource(defineAttachmentSource({ id: "issues", title: "Issue", icon: "CircleDot", pickerTitle: "Attach issue", searchPlaceholder: "Search", search, })); return function() {}; }; return module.exports; })`, ); expect(plugin.attachmentSources.map((source) => source.search.name)).toEqual(["issues.search"]); }); it("rejects modules that are not part of the client runtime", () => { expect(() => evaluatePluginClientBundle( "example", `(function(require) { require("fs"); const module = { exports: {} }; module.exports.default = function() { return function() {}; }; return module.exports; })`, ), ).toThrow('Module "fs" is not available in plugin client code'); }); it("does not publish partial contributions when setup fails", () => { expect(() => evaluatePluginClientBundle( "example", `(function() { return { default: function(plugin) { plugin.addSurface("main", function() { return null; }); throw new Error("setup exploded"); } }; })`, ), ).toThrow("setup exploded"); }); }); it("binds imported getters to each originating installation across delayed callbacks", async () => { const calls: string[] = []; const hostRuntime = (installation: string): PluginClientRuntime => ({ ...runtime, hosts: { getSnapshot: () => [], subscribe: () => () => {}, getPaseoClient(serverId) { calls.push(`${installation}/${serverId}`); return runtime.paseo; }, }, }); const source = bundle(` const { getPaseoClient } = require("@getpaseo/plugin/client"); getPaseoClient("entry-host"); plugin.addCommandCenterItem({ id: "read", title: "Read", icon: "Server", context: "global", onSelect: async () => { await Promise.resolve(); getPaseoClient("target-host"); }, }); `); const first = runPluginClientBundle("same-id", source, hostRuntime("first")); const second = runPluginClientBundle("same-id", source, hostRuntime("second")); // Both callbacks run after the second bundle has evaluated. await first.commandCenterItems[0].onSelect({} as never); await second.commandCenterItems[0].onSelect({} as never); expect(calls).toEqual([ "first/entry-host", "second/entry-host", "first/target-host", "second/target-host", ]); await first.cleanup(); await second.cleanup(); }); describe("file panels", () => { const panel = '{ id: "pdf", title: "PDF", icon: "FileText", files: { extensions: ["pdf"] }, Component() { return null; } }'; it("registers a panel and removes it on cleanup", async () => { const plugin = evaluatePluginClientBundle("files", bundle(`plugin.addFilePanel(${panel})`)); expect(plugin.filePanels.map(({ id, title, files }) => ({ id, title, files }))).toEqual([ { id: "pdf", title: "PDF", files: { extensions: ["pdf"] } }, ]); await plugin.cleanup(); expect(plugin.filePanels).toEqual([]); }); it.each([ ['files: { extensions: ["pdf"] }', 'files: { pattern: "*.pdf" }', "selector"], ['files: { extensions: ["pdf"] }', 'files: { extensions: [".pdf"] }', "selector"], ['files: { extensions: ["pdf"] }', 'files: { extensions: ["*"] }', "selector"], ['files: { extensions: ["pdf"] }', 'files: { extensions: ["foo/bar"] }', "selector"], ['files: { extensions: ["pdf"] }', "files: { extensions: [] }", "selector"], ['files: { extensions: ["pdf"] }', 'files: { extensions: [], filenames: ["a/b"] }', "selector"], ['id: "pdf"', 'id: "Bad"', "file panel id"], ['title: "PDF"', 'title: " "', "title"], ["Component() { return null; }", "Component: {}", "component"], ['icon: "FileText"', 'icon: ""', "icon"], ])("rejects invalid registration %s → %s", (from, to, error) => { expect(() => evaluatePluginClientBundle( "files", bundle(`plugin.addFilePanel(${panel.replace(from, to)})`), ), ).toThrow(error); }); it("accepts filename selectors and rejects duplicate IDs", () => { const source = panel.replace( 'extensions: ["pdf"]', 'extensions: [], filenames: ["Dockerfile"]', ); const plugin = evaluatePluginClientBundle("files", bundle(`plugin.addFilePanel(${source})`)); expect(plugin.filePanels[0]?.files).toEqual({ extensions: [], filenames: ["Dockerfile"] }); expect(() => evaluatePluginClientBundle( "files", bundle(`plugin.addFilePanel(${source}); plugin.addFilePanel(${source})`), ), ).toThrow("Duplicate file panel"); }); });