openpetswithchatandmcp/apps/desktop/tests/plugin-sdk-bubble-validation.test.ts
2026-06-19 06:16:41 +00:00

87 lines
3.1 KiB
TypeScript

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.");