openpetswithchatandmcp/apps/desktop/tests/plugin-sdk-command-validation.test.ts
2026-06-19 00:10:25 +00:00

99 lines
3.8 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 {
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<string, unknown> {
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.");