Raw
1 #!/usr/bin/env node
2 // Unit tests for the ACP registry catalog parser.
3 //
4 // Bundles the pure `src/catalog.ts` module (no `vscode` dependency) and
5 // exercises parseCatalog/validateEntry against the official registry schema
6 // (https://github.com/agentclientprotocol/registry), with a fixed platform so
7 // binary-distribution resolution is deterministic.
8 //
9 // Run: node test/registry.mjs
10
11 import { fileURLToPath } from "node:url";
12 import { dirname, join } from "node:path";
13 import { build } from "esbuild";
14 import { createRequire } from "node:module";
15
16 const __dirname = dirname(fileURLToPath(import.meta.url));
17 const repoRoot = join(__dirname, "..");
18
19 const bundle = await build({
20 entryPoints: [join(repoRoot, "src", "catalog.ts")],
21 bundle: true,
22 write: false,
23 format: "cjs",
24 platform: "node",
25 target: "node18",
26 logLevel: "silent"
27 });
28
29 const requireFromTest = createRequire(import.meta.url);
30 const Module = requireFromTest("node:module");
31 const compiled = eval(Module.wrap(bundle.outputFiles[0].text));
32 const moduleObj = { exports: {} };
33 compiled(moduleObj.exports, requireFromTest, moduleObj, "catalog.bundle.js", repoRoot);
34 const { parseCatalog, validateEntry } = moduleObj.exports;
35
36 const MAC = "darwin-aarch64";
37
38 let failures = 0;
39 function check(cond, msg) {
40 if (cond) {
41 console.log(`OK ${msg}`);
42 } else {
43 console.error(`FAIL ${msg}`);
44 failures++;
45 }
46 }
47 function throws(fn, msg) {
48 try {
49 fn();
50 console.error(`FAIL ${msg} (expected throw)`);
51 failures++;
52 } catch {
53 console.log(`OK ${msg}`);
54 }
55 }
56 const parse = (doc, platform = MAC) => parseCatalog(JSON.stringify(doc), platform);
57
58 // npx distribution → `npx -y <package> [args]`.
59 const npx = parse({
60 version: "1.0.0",
61 agents: [
62 {
63 id: "claude-acp",
64 name: "Claude Agent",
65 version: "0.51.0",
66 description: "ACP wrapper for Anthropic's Claude",
67 distribution: { npx: { package: "@agentclientprotocol/claude-agent-acp@0.51.0" } }
68 }
69 ]
70 }).agents[0];
71 check(npx.command === "npx", "npx → command is npx");
72 check(
73 JSON.stringify(npx.args) ===
74 JSON.stringify(["-y", "@agentclientprotocol/claude-agent-acp@0.51.0"]),
75 "npx → args use -y + package"
76 );
77 check(npx.distribution === "npx" && npx.manualInstall === false, "npx → not manual install");
78 check(npx.key === "claude-acp" && npx.version === "0.51.0", "carries id and version");
79
80 // npx with extra args.
81 const npxArgs = parse({
82 agents: [
83 { id: "a", name: "A", distribution: { npx: { package: "pkg@1", args: ["--acp"] } } }
84 ]
85 }).agents[0];
86 check(
87 JSON.stringify(npxArgs.args) === JSON.stringify(["-y", "pkg@1", "--acp"]),
88 "npx → appends distribution args"
89 );
90
91 // uvx distribution → `uvx <package> [args]`.
92 const uvx = parse({
93 agents: [
94 { id: "fast", name: "Fast", distribution: { uvx: { package: "fast-agent-acp==0.7.22", args: ["-x"] } } }
95 ]
96 }).agents[0];
97 check(
98 uvx.command === "uvx" &&
99 JSON.stringify(uvx.args) === JSON.stringify(["fast-agent-acp==0.7.22", "-x"]),
100 "uvx → command + package + args"
101 );
102
103 // binary distribution for the current platform → basename cmd + manual install.
104 const binDoc = {
105 agents: [
106 {
107 id: "amp-acp",
108 name: "Amp",
109 distribution: {
110 binary: {
111 "darwin-aarch64": {
112 archive: "https://example.com/amp-darwin-aarch64.tar.gz",
113 cmd: "./amp-acp"
114 }
115 }
116 }
117 }
118 ]
119 };
120 const bin = parse(binDoc).agents[0];
121 check(bin.command === "amp-acp", "binary → command is cmd basename (./amp-acp → amp-acp)");
122 check(bin.distribution === "binary" && bin.manualInstall === true, "binary → manual install");
123 check(typeof bin.install === "string" && bin.install.includes("example.com"), "binary → install hint with archive URL");
124
125 // binary with no target for this platform → dropped (unrunnable here).
126 check(parse(binDoc, "linux-x86_64").agents.length === 0, "binary → dropped when no platform target");
127
128 // binary + npx prefers npx (zero manual steps).
129 const both = parse({
130 agents: [
131 {
132 id: "dual",
133 name: "Dual",
134 distribution: {
135 npx: { package: "dual@1" },
136 binary: { "darwin-aarch64": { archive: "https://x/y.tgz", cmd: "./dual" } }
137 }
138 }
139 ]
140 }).agents[0];
141 check(both.command === "npx" && both.distribution === "npx", "binary+npx → prefers npx");
142
143 // Invalid / unusable entries are dropped, not fatal.
144 const filtered = parse({
145 agents: [
146 { name: "no id", distribution: { npx: { package: "x" } } },
147 { id: "Bad-Id", name: "bad", distribution: { npx: { package: "x" } } },
148 { id: "nodist", name: "No dist" },
149 { id: "emptydist", name: "Empty", distribution: {} },
150 { id: "good", name: "Good", distribution: { npx: { package: "good@1" } } }
151 ]
152 });
153 check(
154 filtered.agents.length === 1 && filtered.agents[0].key === "good",
155 "drops entries missing id/name/usable distribution"
156 );
157
158 // Duplicate ids: first wins.
159 const deduped = parse({
160 agents: [
161 { id: "dup", name: "First", distribution: { npx: { package: "first@1" } } },
162 { id: "dup", name: "Second", distribution: { npx: { package: "second@1" } } }
163 ]
164 });
165 check(deduped.agents.length === 1 && deduped.agents[0].name === "First", "dedupes by id (first wins)");
166
167 // validateEntry on its own.
168 check(
169 validateEntry({ id: "k", name: "K", distribution: { npx: { package: "p" } } }, MAC) !== null,
170 "validateEntry accepts a valid npx entry"
171 );
172 check(validateEntry({ id: "k", name: "K", distribution: {} }, MAC) === null, "validateEntry rejects empty distribution");
173 check(validateEntry(null, MAC) === null, "validateEntry rejects null");
174
175 // Structural failures throw.
176 throws(() => parseCatalog("not json", MAC), "throws on invalid JSON");
177 throws(() => parseCatalog("[]", MAC), "throws on non-object top level");
178 throws(() => parseCatalog(JSON.stringify({ version: "1.0.0" }), MAC), "throws when agents array missing");
179 throws(() => parseCatalog(JSON.stringify({ version: "2.0.0", agents: [] }), MAC), "throws on unsupported major version");
180
181 if (failures > 0) {
182 console.error(`\n${failures} CHECK(S) FAILED`);
183 process.exit(1);
184 }
185 console.log("\nALL REGISTRY CHECKS PASSED");