Files
2026-09-14 09:31:20 +01:00

285 lines
11 KiB
JavaScript

import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import fs from "node:fs/promises";
import test, { afterEach } from "node:test";
import os from "node:os";
import path from "node:path";
import {
buildEntry,
buildIndex,
parseManifestFields,
parsePluginsYaml,
} from "./build-index.mjs";
const realFetch = globalThis.fetch;
afterEach(() => {
globalThis.fetch = realFetch;
});
/** Stub global fetch, routing by URL to a release / manifest / repo response. */
function stubGitHub({ release, manifestText, repoMeta }) {
globalThis.fetch = async (url) => {
const u = String(url);
if (u.includes("/releases/latest")) return jsonResponse(release);
if (u.includes("/manifest.yaml")) return textResponse(manifestText ?? "");
if (u.includes("/repos/")) return jsonResponse(repoMeta);
throw new Error(`unexpected fetch: ${u}`);
};
}
const jsonResponse = (body, ok = body !== null) => ({
ok,
status: ok ? 200 : 500,
json: async () => body ?? {},
text: async () => JSON.stringify(body ?? {}),
});
const textResponse = (text) => ({ ok: text !== "", status: text ? 200 : 404, text: async () => text });
const binaryResponse = (body) => ({
ok: true,
status: 200,
body: new ReadableStream({
start(controller) {
controller.enqueue(body);
controller.close();
},
}),
});
test("parsePluginsYaml reads the constrained pointer list", () => {
const specs = parsePluginsYaml(
[
"# a comment",
"plugins:",
" - id: hello",
" repo: kdlbs/kandev-plugin-hello",
" featured: true",
" - id: agent-stats",
" repo: kdlbs/kandev-plugin-agent-stats",
" categories: [analytics, ops]",
].join("\n"),
);
assert.equal(specs.length, 2);
assert.deepEqual(specs[0], { id: "hello", repo: "kdlbs/kandev-plugin-hello", featured: true });
assert.deepEqual(specs[1].categories, ["analytics", "ops"]);
});
test("parsePluginsYaml reads ordered canvas previews", () => {
const specs = parsePluginsYaml(
[
"plugins:",
" - id: board",
" repo: acme/board",
" kind: canvas",
" previews:",
" - url: https://cdn.example/cover.webp",
" alt: Board cover",
" - url: https://cdn.example/detail.webp",
" alt: Board detail",
" featured: true",
].join("\n"),
);
assert.deepEqual(specs[0].previews, [
{ url: "https://cdn.example/cover.webp", alt: "Board cover" },
{ url: "https://cdn.example/detail.webp", alt: "Board detail" },
]);
assert.equal(specs[0].featured, true);
});
test("parseManifestFields extracts presentation keys and ignores the rest", () => {
const fields = parseManifestFields(
[
"id: hello",
"api_version: 1",
'display_name: "Hello"',
"description: A starter plugin",
"icon: icon.svg",
"categories: [getting-started]",
"min_kandev_version: 0.72.0",
"capabilities:",
" state: true",
].join("\n"),
);
assert.equal(fields.display_name, "Hello");
assert.equal(fields.description, "A starter plugin");
assert.equal(fields.icon, "icon.svg");
assert.equal(fields.min_kandev_version, "0.72.0");
assert.deepEqual(fields.categories, ["getting-started"]);
assert.equal("id" in fields, false);
});
test("parseManifestFields reads block-sequence categories", () => {
const fields = parseManifestFields(
["display_name: Multi", "categories:", " - integrations", " - analytics", "author: kandev"].join(
"\n",
),
);
assert.deepEqual(fields.categories, ["integrations", "analytics"]);
assert.equal(fields.author, "kandev");
});
test("buildEntry resolves release, manifest, icon_url and stars", async () => {
stubGitHub({
release: {
tag_name: "v1.2.0",
name: "Release notes",
published_at: "2026-01-01T00:00:00Z",
assets: [{ name: "foo-1.2.0.tar.gz", browser_download_url: "https://dl/foo-1.2.0.tar.gz" }],
},
manifestText: "display_name: Foo\ndescription: A foo\nauthor: kandev\nicon: icon.svg\ncategories: [x]",
repoMeta: { stargazers_count: 42, pushed_at: "2026-02-02T00:00:00Z", owner: { login: "acme" } },
});
const { record, error } = await buildEntry({ id: "foo", repo: "acme/foo" });
assert.equal(error, undefined);
assert.equal(record.name, "Foo");
assert.equal(record.version, "1.2.0");
assert.equal(record.author, "kandev");
assert.equal(record.package_url, "https://dl/foo-1.2.0.tar.gz");
assert.equal(record.icon_url, "https://raw.githubusercontent.com/acme/foo/v1.2.0/icon.svg");
assert.equal(record.stars, 42);
assert.equal(record.updated_at, "2026-02-02T00:00:00Z");
assert.deepEqual(record.categories, ["x"]);
});
test("buildEntry errors (not throws) when there is no installable release", async () => {
stubGitHub({ release: null });
const { record, error } = await buildEntry({ id: "foo", repo: "acme/foo" });
assert.equal(record, undefined);
assert.match(error, /no latest release/);
});
test("buildEntry keeps stars null (never 0) when repo metadata lookup fails", async () => {
stubGitHub({
release: { tag_name: "1.0.0", assets: [{ name: "foo-1.0.0.tar.gz", browser_download_url: "https://dl/foo.tar.gz" }] },
manifestText: "",
repoMeta: null, // -> !ok -> throw -> caught
});
const { record } = await buildEntry({ id: "foo", repo: "acme/foo" });
assert.equal(record.stars, null);
assert.equal(record.author, "acme"); // legacy fallback when the manifest has no author
assert.equal(record.icon_url, null); // no manifest icon
});
test("buildEntry uses inspected canvas presentation metadata and preserves preview order", async () => {
const directory = await fs.mkdtemp(path.join(os.tmpdir(), "kandev-registry-test-"));
const inspector = path.join(directory, "inspector.mjs");
await fs.writeFile(
inspector,
`#!/usr/bin/env node\nprocess.stdout.write(JSON.stringify({id:"board",version:"1.0.0",kind:"canvas",display_name:"Inspected Board",description:"From archive",author:"archive-author",min_kandev_version:"2.0.0",repo_url:"https://github.com/acme/board",license:"MIT"}));\n`,
{ mode: 0o755 },
);
const previousInspector = process.env.KANDEV_CANVAS_PACKAGE_INSPECTOR;
process.env.KANDEV_CANVAS_PACKAGE_INSPECTOR = inspector;
const packageBytes = new Uint8Array([1, 2, 3, 4]);
globalThis.fetch = async (url) => {
const value = String(url);
if (value.includes("/releases/latest")) {
return jsonResponse({
tag_name: "v1.0.0",
assets: [{ name: "board-1.0.0.tar.gz", browser_download_url: "https://dl.example/board.tar.gz" }],
});
}
if (value.includes("/manifest.yaml")) return textResponse("display_name: Board\ndescription: A board\n");
if (value.includes("/repos/")) return jsonResponse({ stargazers_count: 2, owner: { login: "acme" } });
if (value === "https://dl.example/board.tar.gz") return binaryResponse(packageBytes);
throw new Error(`unexpected fetch: ${value}`);
};
try {
const result = await buildEntry({
id: "board",
repo: "acme/board",
kind: "canvas",
previews: [{ url: "https://cdn.example/cover.webp", alt: "Board cover" }],
});
assert.equal(result.error, undefined);
assert.equal(result.record.kind, "canvas");
assert.equal(result.record.name, "Inspected Board");
assert.equal(result.record.description, "From archive");
assert.equal(result.record.author, "archive-author");
assert.equal(result.record.min_kandev_version, "2.0.0");
assert.deepEqual(result.record.previews, [{ url: "https://cdn.example/cover.webp", alt: "Board cover" }]);
assert.equal(result.record.package_sha256, createHash("sha256").update(packageBytes).digest("hex"));
} finally {
if (previousInspector === undefined) delete process.env.KANDEV_CANVAS_PACKAGE_INSPECTOR;
else process.env.KANDEV_CANVAS_PACKAGE_INSPECTOR = previousInspector;
await fs.rm(directory, { recursive: true, force: true });
}
});
test("buildEntry bounds streamed canvas package responses", async () => {
const directory = await fs.mkdtemp(path.join(os.tmpdir(), "kandev-registry-stream-test-"));
const inspector = path.join(directory, "inspector.mjs");
await fs.writeFile(inspector, `process.stdout.write(JSON.stringify({id:"board",version:"1.0.0",kind:"canvas"}));`, { mode: 0o755 });
const previousInspector = process.env.KANDEV_CANVAS_PACKAGE_INSPECTOR;
process.env.KANDEV_CANVAS_PACKAGE_INSPECTOR = inspector;
globalThis.fetch = async (url) => {
const value = String(url);
if (value.includes("/releases/latest")) return jsonResponse({ tag_name: "v1.0.0", assets: [{ name: "board-1.0.0.tar.gz", browser_download_url: "https://dl.example/board.tar.gz" }] });
if (value.includes("/manifest.yaml")) return textResponse("");
if (value.includes("/repos/")) return jsonResponse({ stargazers_count: 1, owner: { login: "acme" } });
if (value === "https://dl.example/board.tar.gz") {
return {
ok: true,
status: 200,
body: new ReadableStream({
start(controller) {
controller.enqueue(new Uint8Array(5 * 1024 * 1024));
controller.enqueue(new Uint8Array(5 * 1024 * 1024 + 1));
controller.close();
},
}),
arrayBuffer: async () => new ArrayBuffer(0),
};
}
throw new Error(`unexpected fetch: ${value}`);
};
try {
const result = await buildEntry({ id: "board", repo: "acme/board", kind: "canvas", previews: [{ url: "https://cdn.example/cover.webp", alt: "Board" }] });
assert.match(result.error, /exceeds the package size limit/);
} finally {
if (previousInspector === undefined) delete process.env.KANDEV_CANVAS_PACKAGE_INSPECTOR;
else process.env.KANDEV_CANVAS_PACKAGE_INSPECTOR = previousInspector;
await fs.rm(directory, { recursive: true, force: true });
}
});
test("buildEntry rejects a canvas without a preview before fetching a release", async () => {
const result = await buildEntry({ id: "board", repo: "acme/board", kind: "canvas" });
assert.match(result.error, /at least one preview/);
});
test("empty plugins.yaml parses to no specs and builds a valid empty index", async () => {
assert.deepEqual(parsePluginsYaml("plugins: []"), []);
const { document, errors } = await buildIndex([]);
assert.equal(document.plugins.length, 0);
assert.equal(errors.length, 0);
assert.equal(document.schema_version, 1);
assert.equal(document.source.name, "Kandev Official");
});
test("buildIndex skips bad entries but still builds the good ones", async () => {
// First entry has a release, second does not.
let call = 0;
globalThis.fetch = async (url) => {
const u = String(url);
if (u.includes("/releases/latest")) {
call += 1;
return call === 1
? jsonResponse({ tag_name: "1.0.0", assets: [{ name: "a-1.0.0.tar.gz", browser_download_url: "https://dl/a" }] })
: jsonResponse(null);
}
if (u.includes("/manifest.yaml")) return textResponse("");
return jsonResponse({ stargazers_count: 1, owner: { login: "o" } });
};
const { document, errors } = await buildIndex([
{ id: "a", repo: "o/a" },
{ id: "b", repo: "o/b" },
]);
assert.equal(document.plugins.length, 1);
assert.equal(document.plugins[0].id, "a");
assert.equal(errors.length, 1);
assert.equal((await buildIndex([{ id: "bad-canvas", repo: "o/bad-canvas", kind: "canvas" }])).canvasErrors.length, 1);
assert.equal(document.schema_version, 1);
});