229 lines
13 KiB
TypeScript
Vendored
229 lines
13 KiB
TypeScript
Vendored
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { createFrontendPluginRegistry, evaluatePluginCommandConditions } from "./frontendPlugin";
|
|
import { executePluginCommand } from "./pluginCommandRegistry";
|
|
import { closePluginDockEntry, usePluginBottomDock } from "./pluginBottomDock";
|
|
import type { InstalledPlugin, PluginMenusContribution } from "@/types/database";
|
|
|
|
// PR-A4 command/menus host side (HOST_PLUGIN_UI_SPEC §4/§5/§8.3/§11): the command
|
|
// the command registry derives toolbar entries from installed-plugin declarations; execution opens the workbench with a host-authored context —
|
|
// presentation: tab (default) opens a workbench tab, panel opens the global bottom dock; the plugin payload rides under
|
|
// context.plugin, while workbenchId/restored/surface are host-injected and never taken from the plugin.
|
|
function installedPlugin(id: string, contributions: InstalledPlugin["manifest"]["contributions"] = []): InstalledPlugin {
|
|
return {
|
|
compatibility: { compatible: true },
|
|
manifest: {
|
|
id,
|
|
name: id,
|
|
version: "1.0.0",
|
|
drivers: [],
|
|
contributions,
|
|
},
|
|
};
|
|
}
|
|
|
|
function localTerminalCommand(actionOverrides: Record<string, unknown> = {}) {
|
|
return {
|
|
type: "command",
|
|
id: "open-local-terminal",
|
|
label: "Local terminal",
|
|
action: {
|
|
type: "open-workbench",
|
|
workbench: "io.dbx.ssh.workbench",
|
|
reuse: "singleton",
|
|
context: { plugin: { mode: "local-terminal" } },
|
|
...actionOverrides,
|
|
},
|
|
};
|
|
}
|
|
|
|
function menusContribution(items: PluginMenusContribution["items"]): PluginMenusContribution {
|
|
return { type: "menus", id: "entrypoints", items };
|
|
}
|
|
|
|
describe("plugin command registry (PR-A4)", () => {
|
|
beforeEach(() => {
|
|
const { entries, visible } = usePluginBottomDock();
|
|
entries.value.splice(0);
|
|
visible.value = false;
|
|
});
|
|
|
|
it("derives visible appToolbar entries ordered by order then command id", () => {
|
|
const registry = createFrontendPluginRegistry([
|
|
installedPlugin("io.dbx.ssh", [
|
|
{ type: "workbench", id: "io.dbx.ssh.workbench", label: "SSH" },
|
|
localTerminalCommand(),
|
|
menusContribution([
|
|
{ location: "commandPalette", command: "open-local-terminal", group: "primary", order: 100 },
|
|
{ location: "appToolbar", command: "open-local-terminal", group: "navigation", order: 100, default_visible: true },
|
|
]),
|
|
] as unknown as InstalledPlugin["manifest"]["contributions"]),
|
|
]);
|
|
const entries = registry.listToolbarMenuCommands();
|
|
expect(entries).toHaveLength(1);
|
|
expect(entries[0].plugin.manifest.id).toBe("io.dbx.ssh");
|
|
expect(entries[0].command.id).toBe("open-local-terminal");
|
|
|
|
// §5.2: toolbar items default to hidden — anything without default_visible: true never renders.
|
|
const hidden = createFrontendPluginRegistry([installedPlugin("io.dbx.ssh", [localTerminalCommand(), menusContribution([{ location: "appToolbar", command: "open-local-terminal", group: "navigation", order: 100 }])] as unknown as InstalledPlugin["manifest"]["contributions"])]);
|
|
expect(hidden.listToolbarMenuCommands()).toHaveLength(0);
|
|
});
|
|
|
|
it("executes a tab command with a host-authored context and singleton reuse", () => {
|
|
const registry = createFrontendPluginRegistry([installedPlugin("io.dbx.ssh", [{ type: "workbench", id: "io.dbx.ssh.workbench", label: "SSH" }, localTerminalCommand()] as unknown as InstalledPlugin["manifest"]["contributions"])]);
|
|
const openPluginWorkbench = vi.fn();
|
|
const result = executePluginCommand(registry, { openPluginWorkbench } as never, "io.dbx.ssh", "open-local-terminal");
|
|
expect(result.error).toBeUndefined();
|
|
expect(openPluginWorkbench).toHaveBeenCalledTimes(1);
|
|
const [pluginId, workbenchId, options] = openPluginWorkbench.mock.calls[0];
|
|
expect(pluginId).toBe("io.dbx.ssh");
|
|
expect(workbenchId).toBe("io.dbx.ssh.workbench");
|
|
expect(options.forceNew).toBe(false);
|
|
// plugin payload under context.plugin; reserved fields are host-injected and override forged values.
|
|
expect(options.context.plugin).toEqual({ mode: "local-terminal" });
|
|
expect(typeof options.context.workbenchId).toBe("string");
|
|
expect(options.context.restored).toBe(false);
|
|
expect(options.context.surface).toBe("tab");
|
|
expect(usePluginBottomDock().visible.value).toBe(false);
|
|
});
|
|
|
|
it("singleton (default) reuses the dock entry per panel execution; the + menu stays the multi-open affordance", () => {
|
|
const registry = createFrontendPluginRegistry([
|
|
installedPlugin("io.dbx.ssh", [
|
|
{ type: "workbench", id: "io.dbx.ssh.workbench", label: "SSH" },
|
|
localTerminalCommand({ presentation: "panel", instance_key: "local-terminal", context: { plugin: { mode: "local-terminal" }, workbenchId: "plugin-forged", surface: "tab" } }),
|
|
] as unknown as InstalledPlugin["manifest"]["contributions"]),
|
|
]);
|
|
const openPluginWorkbench = vi.fn();
|
|
const { entries, activeEntryId, visible } = usePluginBottomDock();
|
|
|
|
const first = executePluginCommand(registry, { openPluginWorkbench } as never, "io.dbx.ssh", "open-local-terminal");
|
|
expect(first.error).toBeUndefined();
|
|
expect(entries.value).toHaveLength(1);
|
|
expect(visible.value).toBe(true);
|
|
const firstEntry = entries.value[0];
|
|
expect(firstEntry.pluginId).toBe("io.dbx.ssh");
|
|
expect(firstEntry.workbenchContributionId).toBe("io.dbx.ssh.workbench");
|
|
expect(firstEntry.context.plugin).toEqual({ mode: "local-terminal" });
|
|
// plugin-forged reserved fields are overridden by host-authoritative values; workbenchId = entry id and stable.
|
|
expect(firstEntry.context.workbenchId).toBe(firstEntry.id);
|
|
expect(firstEntry.context.workbenchId).not.toBe("plugin-forged");
|
|
expect(firstEntry.context.restored).toBe(false);
|
|
expect(firstEntry.context.surface).toBe("panel");
|
|
expect(activeEntryId.value).toBe(firstEntry.id);
|
|
// §4.1 reuse key = pluginId + commandId + presentation + instance_key.
|
|
expect(firstEntry.instanceKey).toBe("local-terminal");
|
|
|
|
// a second singleton execution re-activates the existing instance instead of spawning one;
|
|
// repeated multi-open goes through the dock "+" menu (options_action/connection targets), not here.
|
|
executePluginCommand(registry, { openPluginWorkbench } as never, "io.dbx.ssh", "open-local-terminal");
|
|
expect(entries.value).toHaveLength(1);
|
|
expect(activeEntryId.value).toBe(firstEntry.id);
|
|
expect(openPluginWorkbench).not.toHaveBeenCalled();
|
|
|
|
// closing an entry removes its tab; the panel hides once all entries are gone.
|
|
closePluginDockEntry(entries.value[0].id);
|
|
expect(visible.value).toBe(false);
|
|
});
|
|
|
|
it("reuse:new spawns one dock entry per execution", () => {
|
|
const registry = createFrontendPluginRegistry([
|
|
installedPlugin("io.dbx.ssh", [
|
|
{ type: "workbench", id: "io.dbx.ssh.workbench", label: "SSH" },
|
|
localTerminalCommand({ presentation: "panel", reuse: "new", instance_key: "local-terminal", context: { plugin: { mode: "local-terminal" } } }),
|
|
] as unknown as InstalledPlugin["manifest"]["contributions"]),
|
|
]);
|
|
const { entries, activeEntryId } = usePluginBottomDock();
|
|
executePluginCommand(registry, { openPluginWorkbench: vi.fn() } as never, "io.dbx.ssh", "open-local-terminal");
|
|
executePluginCommand(registry, { openPluginWorkbench: vi.fn() } as never, "io.dbx.ssh", "open-local-terminal");
|
|
expect(entries.value).toHaveLength(2);
|
|
expect(activeEntryId.value).toBe(entries.value[1]!.id);
|
|
});
|
|
|
|
it("drops plugin-forged reserved fields and honors reuse:new with forceNew", () => {
|
|
const registry = createFrontendPluginRegistry([
|
|
installedPlugin("io.dbx.ssh", [
|
|
{ type: "workbench", id: "io.dbx.ssh.workbench", label: "SSH" },
|
|
localTerminalCommand({ reuse: "new", context: { plugin: { mode: "local-terminal" }, workbenchId: "plugin-forged", restored: true, surface: "panel" } }),
|
|
] as unknown as InstalledPlugin["manifest"]["contributions"]),
|
|
]);
|
|
const openPluginWorkbench = vi.fn();
|
|
const result = executePluginCommand(registry, { openPluginWorkbench } as never, "io.dbx.ssh", "open-local-terminal");
|
|
expect(result.error).toBeUndefined();
|
|
const [, , options] = openPluginWorkbench.mock.calls[0];
|
|
expect(options.forceNew).toBe(true);
|
|
expect(options.context.workbenchId).not.toBe("plugin-forged");
|
|
expect(options.context.restored).toBe(false);
|
|
expect(options.context.surface).toBe("tab");
|
|
});
|
|
|
|
it("reports unknown commands and dangling workbench references instead of opening a tab", () => {
|
|
const registry = createFrontendPluginRegistry([installedPlugin("io.dbx.ssh", [localTerminalCommand({ workbench: "io.dbx.ssh.missing" })] as unknown as InstalledPlugin["manifest"]["contributions"])]);
|
|
const openPluginWorkbench = vi.fn();
|
|
expect(executePluginCommand(registry, { openPluginWorkbench } as never, "io.dbx.ssh", "missing-command").error).toBeTruthy();
|
|
expect(executePluginCommand(registry, { openPluginWorkbench } as never, "io.dbx.ssh", "open-local-terminal").error).toContain("missing workbench");
|
|
expect(openPluginWorkbench).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("evaluates §5.3 conditions (equals/notEquals/oneOf, missing key = false)", () => {
|
|
expect(evaluatePluginCommandConditions([{ key: "surface", operator: "equals", value: "tab" }], { surface: "tab" })).toBe(true);
|
|
expect(evaluatePluginCommandConditions([{ key: "surface", operator: "notEquals", value: "tab" }], { surface: "tab" })).toBe(false);
|
|
expect(evaluatePluginCommandConditions([{ key: "connection.state", operator: "oneOf", value: ["connected", "reconnecting"] }], { "connection.state": "connected" })).toBe(true);
|
|
// empty condition group defaults to true; a missing key is always false (§5.3).
|
|
expect(evaluatePluginCommandConditions([], {})).toBe(true);
|
|
expect(evaluatePluginCommandConditions([{ key: "object.type", operator: "equals", value: "table" }], {})).toBe(false);
|
|
// implicit AND within all.
|
|
expect(
|
|
evaluatePluginCommandConditions(
|
|
[
|
|
{ key: "surface", operator: "equals", value: "tab" },
|
|
{ key: "readOnly", operator: "equals", value: false },
|
|
],
|
|
{ surface: "tab", readOnly: false },
|
|
),
|
|
).toBe(true);
|
|
expect(
|
|
evaluatePluginCommandConditions(
|
|
[
|
|
{ key: "surface", operator: "equals", value: "tab" },
|
|
{ key: "readOnly", operator: "equals", value: true },
|
|
],
|
|
{ surface: "tab", readOnly: false },
|
|
),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("gates execution by enablement against the context snapshot", () => {
|
|
const command = (enablement: unknown) =>
|
|
[
|
|
{ type: "workbench", id: "io.dbx.ssh.workbench", label: "SSH" },
|
|
localTerminalCommand(),
|
|
{ type: "command", id: "gated", label: "Gated", enablement, action: { type: "open-workbench", workbench: "io.dbx.ssh.workbench", context: { plugin: { mode: "local-terminal" } } } },
|
|
] as unknown as InstalledPlugin["manifest"]["contributions"];
|
|
const openPluginWorkbench = vi.fn();
|
|
|
|
const pass = createFrontendPluginRegistry([installedPlugin("io.dbx.ssh", command({ all: [{ key: "surface", operator: "equals", value: "tab" }] } as unknown as InstalledPlugin["manifest"]["contributions"]))]);
|
|
expect(executePluginCommand(pass, { openPluginWorkbench: vi.fn() } as never, "io.dbx.ssh", "gated").error).toBeUndefined();
|
|
|
|
const blocked = createFrontendPluginRegistry([installedPlugin("io.dbx.ssh", command({ all: [{ key: "surface", operator: "equals", value: "panel" }] } as unknown as InstalledPlugin["manifest"]["contributions"]))]);
|
|
const result = executePluginCommand(blocked, { openPluginWorkbench } as never, "io.dbx.ssh", "gated");
|
|
expect(result.error).toContain("disabled by enablement");
|
|
expect(openPluginWorkbench).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("listToolbarMenuCommands applies when visibility against the placement surface", () => {
|
|
const menus = (value: string) => menusContribution([{ location: "appToolbar", command: "open-local-terminal", group: "navigation", order: 100, default_visible: true, when: { all: [{ key: "surface", operator: "equals", value }] } }]);
|
|
const show = createFrontendPluginRegistry([installedPlugin("io.dbx.ssh", [localTerminalCommand(), menus("appToolbar")] as unknown as InstalledPlugin["manifest"]["contributions"])]);
|
|
expect(show.listToolbarMenuCommands()).toHaveLength(1);
|
|
const hide = createFrontendPluginRegistry([installedPlugin("io.dbx.ssh", [localTerminalCommand(), menus("panel")] as unknown as InstalledPlugin["manifest"]["contributions"])]);
|
|
expect(hide.listToolbarMenuCommands()).toHaveLength(0);
|
|
});
|
|
|
|
it("finds the declared command targeting a workbench for entry routing", () => {
|
|
const registry = createFrontendPluginRegistry([installedPlugin("io.dbx.ssh", [{ type: "workbench", id: "io.dbx.ssh.workbench", label: "SSH" }, localTerminalCommand()] as unknown as InstalledPlugin["manifest"]["contributions"])]);
|
|
const command = registry.findCommandTargetingWorkbench("io.dbx.ssh", "io.dbx.ssh.workbench");
|
|
expect(command?.id).toBe("open-local-terminal");
|
|
expect(registry.findCommandTargetingWorkbench("io.dbx.ssh", "other.workbench")).toBeUndefined();
|
|
expect(registry.findCommandTargetingWorkbench("other.plugin", "io.dbx.ssh.workbench")).toBeUndefined();
|
|
});
|
|
});
|