import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { createBubbleSpecValidator } from "../src/plugin-sdk-bubble-validation.js"; import type { PluginHostCapabilities } from "../src/plugin-sdk-types.js"; const desktopRoot = process.env.FAMILIAROS_DESKTOP_ROOT ?? resolve(dirname(fileURLToPath(import.meta.url)), ".."); const pluginSdkApiBuilderSource = readFileSync(resolve(desktopRoot, "src/plugin-sdk-api-builder.ts"), "utf8"); const pluginSdkBubbleValidationSource = readFileSync(resolve(desktopRoot, "src/plugin-sdk-bubble-validation.ts"), "utf8"); const pluginSdkValidatorsSource = readFileSync(resolve(desktopRoot, "src/plugin-sdk-validators.ts"), "utf8"); const pluginSdkBubbleSupportSource = readFileSync(resolve(desktopRoot, "src/plugin-sdk-bubble-support.ts"), "utf8"); assert.match(pluginSdkApiBuilderSource, /from "\.\/plugin-sdk-bubble-validation(?:\.js)?"/, "Plugin SDK API builder must import the extracted bubble validation seam."); assert.match(pluginSdkBubbleValidationSource, /export function createBubbleSpecValidator/, "Plugin SDK bubble validation seam must export the validator factory."); assert.match(pluginSdkValidatorsSource, /from "\.\/plugin-sdk-bubble-support(?:\.js)?"/, "Plugin SDK validators must import the extracted bubble support seam."); assert.match(pluginSdkBubbleSupportSource, /export function validateBubbleInput/, "Plugin SDK bubble support must export input validation."); assert.match(pluginSdkBubbleSupportSource, /export function renderLimitedMarkdown/, "Plugin SDK bubble support must export limited markdown rendering."); const requestedPermissions: string[] = []; const validateBubbleSpec = createBubbleSpecValidator({ capabilities: { settings: { dynamicSpeechAllowed: () => true, }, } as PluginHostCapabilities, requirePermission(permission) { requestedPermissions.push(permission); }, quotas: { dynamicTextChars: 2_000, markdownChars: 600, }, resolveAssetRef(ref, kinds) { const name = typeof ref === "object" && ref !== null && "name" in ref ? String((ref as { name: unknown }).name) : "asset"; return { kind: kinds[0]!, name, path: `/tmp/${kinds[0]}/${name}.svg`, }; }, }); const hudBubble = validateBubbleSpec({ pin: true, hud: { items: [ { icon: { kind: "icon", name: "focus" }, value: 79.6, tone: "amber", label: "Focus", }, ], }, }); assert.equal(hudBubble.pin, true); assert.equal(requestedPermissions.includes("familiar:pin"), true); assert.deepEqual(hudBubble.hud?.items, [ { svgPath: "/tmp/icons/focus.svg", value: 80, tone: "amber", label: "Focus", }, ]); assert.throws( () => validateBubbleSpec({ pin: true, text: "hello", hud: { items: [ { icon: { kind: "icon", name: "focus" }, value: 40, }, ], }, }), /Plugin bubble HUD cannot be combined with text(?: or markdown|, markdown, body media, or indicator)\./, ); console.error("Plugin SDK bubble validation seam passed.");