import assert from "node:assert/strict"; import { test } from "node:test"; import { checkApiVersion, definePlugin, findConflicts, HOST_API_VERSION, isValidPluginId, parseSemver, type Plugin, type PluginManifest, } from "./plugin.ts"; // A representative manifest exercising every field — its existence type-checks the contract. // `apiVersion` is a literal: a plugin pins the version it was built against, so importing // HOST_API_VERSION would always equal the host and defeat the check. No `id`/`basePath` — the // host derives both from the plugin's folder name. const scheduling: PluginManifest = definePlugin({ apiVersion: "1.0.0", hooks: { onBoot: () => {} }, nav: [{ children: [{ href: "/scheduling/shifts", id: "scheduling:shifts", label: "Shifts", permission: "scheduling:read" }], icon: "i-cal", id: "scheduling:root", label: "Scheduling", }], permissions: [{ description: "View shifts", token: "scheduling:read" }], routes: [ { handler: () => ({ data: { rows: [] }, view: "shifts" }), method: "GET", path: "/shifts", permission: "scheduling:read" }, { handler: () => ({ redirect: "/scheduling/shifts" }), method: "POST", path: "/shifts", permission: "scheduling:write" }, { handler: (ctx) => void ctx.res.end("raw"), method: "GET", path: "/raw" }, // void = handler wrote res itself ], }); test("definePlugin returns the manifest unchanged — id/mount come from the folder, not the manifest", () => { const m: PluginManifest = { apiVersion: "1.0.0" }; assert.equal(definePlugin(m), m); // identity, not a copy assert.equal(scheduling.routes?.length, 3); }); test("isValidPluginId accepts lowercase/digits/dashes anywhere and rejects everything else", () => { for (const ok of ["scheduling", "people-directory", "people2", "v2", "people--dir", "-people", "people-", "a-1-b", "1"]) { assert.ok(isValidPluginId(ok), ok); } for (const bad of ["People", "people_dir", "a/b", "a.b", "a b", ""]) { assert.ok(!isValidPluginId(bad), bad); } }); test("parseSemver follows the semver core, rejecting ranges, prefixes, leading zeros and missing parts", () => { assert.deepEqual(parseSemver("1.2.3"), { major: 1, minor: 2, patch: 3 }); assert.deepEqual(parseSemver("1.2.3-rc.1+build.5"), { major: 1, minor: 2, patch: 3 }); // prerelease/build tolerated, ignored for (const bad of ["1", "1.2", "1.2.3.4", "v1.2.3", "^1.2.3", "01.2.3", "1.2.x", "1.2.3 ", "", 3, null, undefined]) { assert.equal(parseSemver(bad), null, `${String(bad)} is not a semver`); } }); test("checkApiVersion: semver compat — equal/patch ok, older minor warns, newer-minor/major-mismatch/malformed refuse", () => { assert.equal(checkApiVersion(HOST_API_VERSION).level, "ok"); // "1.0.0" vs "1.0.0" assert.equal(checkApiVersion("1.0.5", "1.0.0").level, "ok"); // patch never affects compatibility assert.equal(checkApiVersion("1.0.0", "1.2.0").level, "warn"); // older minor still runs (additive), nudge to update assert.equal(checkApiVersion("1.3.0", "1.2.0").level, "refuse"); // needs features a newer host has assert.equal(checkApiVersion("2.0.0", "1.5.0").level, "refuse"); // incompatible major (newer) assert.equal(checkApiVersion("1.0.0", "2.0.0").level, "refuse"); // incompatible major (older) for (const bad of ["1", "1.2", "v1.2.3", "01.2.3", "1.2.x", "", 1, undefined, null]) { assert.equal(checkApiVersion(bad).level, "refuse", `${String(bad)} must refuse`); } }); // A minimal discovered plugin (id = folder name; mount path is the derived `/`), per case. const p = (over: Partial & Pick): Plugin => ({ apiVersion: "1.0.0", ...over }); test("findConflicts: a clean set has none", () => { assert.deepEqual(findConflicts([p({ id: "a" }), p({ id: "b" })]), []); }); test("findConflicts: a duplicate id and a colliding route are loud errors", () => { const dupId = findConflicts([p({ id: "a" }), p({ id: "a" })]); assert.ok(dupId.some((c) => c.kind === "id" && c.level === "error")); // Cross-plugin routes can't collide (unique `/` prefix); two identical routes in one can. const noop = () => {}; const dupRoute = findConflicts([p({ id: "a", routes: [{ handler: noop, method: "GET", path: "/t" }, { handler: noop, method: "GET", path: "/t" }], })]); assert.ok(dupRoute.some((c) => c.kind === "route" && c.level === "error" && c.message.includes("/a/t"))); }); test("findConflicts: duplicate nav id is an error, a shared permission token only warns", () => { const navDup = findConflicts([ p({ id: "a", nav: [{ id: "dup", label: "A" }] }), p({ id: "b", nav: [{ id: "dup", label: "B" }] }), ]); assert.ok(navDup.some((c) => c.kind === "nav-id" && c.level === "error" && c.plugins.includes("a") && c.plugins.includes("b"))); // Sharing a permission across plugins is legitimate (shared role) → warn, not error. const permDup = findConflicts([ p({ id: "a", permissions: [{ token: "shared:read" }] }), p({ id: "b", permissions: [{ token: "shared:read" }] }), ]); assert.ok(permDup.some((c) => c.kind === "permission" && c.level === "warn")); }); test("findConflicts: only one plugin may claim the dashboard (`home`) — two is a loud error (§10)", () => { const home = () => ({ html: "dash" }); const dup = findConflicts([p({ id: "a", home }), p({ id: "b", home })]); assert.ok(dup.some((c) => c.kind === "home" && c.level === "error" && c.plugins.includes("a") && c.plugins.includes("b"))); // One home (or none) is fine. assert.deepEqual(findConflicts([p({ id: "a", home }), p({ id: "b" })]).filter((c) => c.kind === "home"), []); });