import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { validateCommandFormValuesSpec, validateCommandSpec, } from "../src/plugin-sdk-command-validation.js"; const desktopRoot = process.env.FAMILIAROS_DESKTOP_ROOT ?? resolve(dirname(fileURLToPath(import.meta.url)), ".."); const pluginSdkValidatorsSource = readFileSync(resolve(desktopRoot, "src/plugin-sdk-validators.ts"), "utf8"); const pluginSdkCommandValidationSource = readFileSync(resolve(desktopRoot, "src/plugin-sdk-command-validation.ts"), "utf8"); assert.match(pluginSdkValidatorsSource, /from "\.\/plugin-sdk-command-validation(?:\.js)?"/, "plugin-sdk-validators must import the extracted command validation seam."); assert.match(pluginSdkCommandValidationSource, /export function validateCommandSpec/, "plugin-sdk command validation seam must export command metadata validation."); assert.match(pluginSdkCommandValidationSource, /export function validateCommandFormValuesSpec/, "plugin-sdk command validation seam must export command form value validation."); const helpers = { check(ok: boolean, message: string): void { if (!ok) throw new Error(message); }, clampNumber(value: number, min: number, max: number): number { if (!Number.isFinite(value)) return min; return Math.min(Math.max(value, min), max); }, isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); }, commandIdPattern: /^[A-Za-z0-9._:-]{1,64}$/, namedHostIcons: new Set(["info", "check", "alert", "heart", "star", "bell", "coffee", "timer", "droplet", "sparkles", "zap", "moon", "sun", "food", "play", "pause"]), } as const; const command = validateCommandSpec( { id: "status.run", title: "Run status", description: "Gather a quick plugin status snapshot.", placement: "top", priority: 1200, featured: true, icon: { kind: "icon", name: "badge" } as const, form: { submitLabel: "Run", fields: [ { id: "count", type: "number", label: "Count", min: 1, max: 10, default: 3 }, { id: "day", type: "select", label: "Day", options: [{ label: "Mon", value: "mon" }, { label: "Tue", value: "tue" }], default: "mon" }, ], }, }, (ref) => { assert.deepEqual(ref, { kind: "icon", name: "badge" }); return { kind: "icons", name: "badge", path: "assets/icons/badge.svg" }; }, helpers, ); assert.equal(command.id, "status.run"); assert.equal(command.priority, 1000, "Command priority should clamp to the safe range."); assert.equal(command.form?.fields.length, 2); assert.deepEqual(command.icon, { kind: "icon", name: "badge" }); const values = validateCommandFormValuesSpec(command.form!, { count: "7", day: "tue", }, helpers); assert.deepEqual(values, { count: 7, day: "tue" }); const multiSelectValues = validateCommandFormValuesSpec({ fields: [ { id: "days", type: "multiSelect", label: "Days", options: [{ label: "Mon", value: "mon" }, { label: "Tue", value: "tue" }], default: ["mon"], }, ], }, { days: ["tue"] }, helpers); assert.deepEqual(multiSelectValues, { days: ["tue"] }); assert.throws( () => validateCommandFormValuesSpec(command.form!, { count: 0, day: "mon" }, helpers), /Count is too small/, ); assert.throws( () => validateCommandFormValuesSpec(command.form!, { count: 2, day: "fri" }, helpers), /Day has an invalid value/, ); assert.throws( () => validateCommandSpec( { id: "bad", title: "Bad", icon: "definitely-not-a-host-icon" } as never, () => ({ kind: "icons", name: "ignored", path: "ignored" }), helpers, ), /Invalid plugin command icon/, ); console.error("Plugin SDK command validation seam passed.");