import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { buildPluginInspectorState, buildPluginRuntimePublicState, clearPluginRuntimeState, createPluginRuntimeState, executePluginCommand, executePluginMenuSelect, notifyPluginConfigListeners, } from "../src/plugin-sdk-bridge-runtime-state.js"; import type { PluginCommand } from "../src/plugin-sdk-types.js"; const desktopRoot = process.env.FAMILIAROS_DESKTOP_ROOT ?? resolve(dirname(fileURLToPath(import.meta.url)), ".."); const pluginSdkBridgeSource = readFileSync(resolve(desktopRoot, "src/plugin-sdk-bridge.ts"), "utf8"); const pluginSdkBridgeRuntimeStateSource = readFileSync(resolve(desktopRoot, "src/plugin-sdk-bridge-runtime-state.ts"), "utf8"); assert.match(pluginSdkBridgeSource, /from "\.\/plugin-sdk-bridge-runtime-state(?:\.js)?"/, "plugin-sdk-bridge must import the extracted runtime-state seam."); assert.match(pluginSdkBridgeRuntimeStateSource, /export function buildPluginRuntimePublicState/, "plugin-sdk bridge runtime-state seam must export public state shaping."); assert.match(pluginSdkBridgeRuntimeStateSource, /export async function executePluginCommand/, "plugin-sdk bridge runtime-state seam must export command execution helpers."); assert.match(pluginSdkBridgeRuntimeStateSource, /export function clearPluginRuntimeState/, "plugin-sdk bridge runtime-state seam must export runtime cleanup helpers."); const state = createPluginRuntimeState(); state.status = { text: "Ready", tone: "success" }; state.menuItems = [{ id: "open", title: "Open" }] as unknown as typeof state.menuItems; state.commands.set("run", { meta: { id: "run", title: "Run" } as PluginCommand, handler: () => undefined, }); state.bubbles.set("bubble", { host: { id: "bubble", update: async () => undefined, dismiss: async () => undefined, pin: async () => undefined, unpin: async () => undefined, }, dismissed: false, }); state.panels.set("panel", { id: "panel", show: async () => undefined, hide: async () => undefined, postMessage: async () => undefined, close: async () => undefined, }); state.eventSubscriptions.set("evt", () => undefined); state.busSubscriptions.set("bus", { topic: "topic", handler: () => undefined }); state.tickSubscriptions.set("tick", () => undefined); state.petWindow.count = 2; const publicState = buildPluginRuntimePublicState(state); assert.equal(publicState.commands.length, 1); assert.equal(publicState.menuItems?.length, 1); assert.deepEqual(publicState.status, { text: "Ready", tone: "success" }); const inspectorState = buildPluginInspectorState(state); assert.equal(inspectorState.activeBubbles, 1); assert.equal(inspectorState.activePanels, 1); assert.equal(inspectorState.eventSubscriptions, 3); assert.equal(inspectorState.quotaCounters.petActions, 2); const seenConfigs: string[] = []; state.configListeners.add((config) => seenConfigs.push(String(config.mode))); notifyPluginConfigListeners(state, { mode: "alpha" }, () => undefined); assert.deepEqual(seenConfigs, ["alpha"]); let ranCommand = false; state.commands.set("exec", { meta: { id: "exec", title: "Execute" } as PluginCommand, handler: () => { ranCommand = true; }, }); await executePluginCommand({ pluginId: "plug", state, commandId: "exec", releaseDelayMs: 0, logger: () => undefined, }); await new Promise((resolve) => setTimeout(resolve, 0)); assert.equal(ranCommand, true); assert.equal(state.userCommandDepth, 0); let selectedItem: string | undefined; state.menuHandlers.add((itemId) => { selectedItem = itemId; }); await executePluginMenuSelect({ pluginId: "plug", state, itemId: "open", releaseDelayMs: 0, logger: () => undefined, }); await new Promise((resolve) => setTimeout(resolve, 0)); assert.equal(selectedItem, "open"); assert.equal(state.userCommandDepth, 0); let bubbleDismissed = 0; state.bubbles.set("dismiss-me", { host: { id: "dismiss-me", update: async () => undefined, dismiss: async () => { bubbleDismissed += 1; }, pin: async () => undefined, unpin: async () => undefined, }, dismissed: false, }); let panelClosed = 0; state.panels.set("close-me", { id: "close-me", show: async () => undefined, hide: async () => undefined, postMessage: async () => undefined, close: async () => { panelClosed += 1; }, }); state.spawnedPets.add("pet-1"); const closedPets: string[] = []; clearPluginRuntimeState({ pluginId: "plug", state, closeFamiliar: async (_pluginId, petHandleId) => { closedPets.push(petHandleId); }, }); await new Promise((resolve) => setTimeout(resolve, 0)); assert.equal(state.commands.size, 0); assert.equal(state.bubbles.size, 0); assert.equal(state.panels.size, 0); assert.deepEqual(closedPets, ["pet-1"]); assert.equal(bubbleDismissed, 1); assert.equal(panelClosed, 1); console.error("Plugin SDK bridge runtime-state validation passed.");