| 1 | import { createHash } from "node:crypto"; |
| 2 | import { readdirSync, readFileSync } from "node:fs"; |
| 3 | import { describe, expect, it } from "vitest"; |
| 4 | import { pluginZipFor } from "../../src/docs/claude-plugins.js"; |
| 5 | import { unzip } from "../../src/lib/archive.js"; |
| 6 | |
| 7 | const dec = new TextDecoder(); |
| 8 | const dir = new URL("../../claude-plugins/", import.meta.url); |
| 9 | |
| 10 | /** The checked-in zip of one plugin (claude-plugins/<name>-<version>.zip), as the build embeds it. */ |
| 11 | function packed(name: string): { bytes: Uint8Array; sha256: string } { |
| 12 | const file = readdirSync(dir).find((f) => new RegExp(`^${name}-\\d+\\.\\d+\\.\\d+\\.zip$`).test(f))!; |
| 13 | const bytes = new Uint8Array(readFileSync(new URL(file, dir))); |
| 14 | return { bytes, sha256: createHash("sha256").update(bytes).digest("hex") }; |
| 15 | } |
| 16 | |
| 17 | const files = async (bytes: Uint8Array) => |
| 18 | Object.fromEntries((await unzip(bytes)).map((e) => ["dir" in e ? e.path : e.path, "dir" in e ? "" : dec.decode(e.content)])); |
| 19 | |
| 20 | describe("Claude Code plugins served by an instance", () => { |
| 21 | for (const name of ["diffinery", "diffinery-ci"]) { |
| 22 | it(`${name}: diffinery.com serves the packed zip as it is`, async () => { |
| 23 | const p = packed(name); |
| 24 | const out = await pluginZipFor("https://diffinery.com", p.bytes, p.sha256); |
| 25 | expect(out.bytes).toBe(p.bytes); |
| 26 | expect(out.sha256).toBe(p.sha256); |
| 27 | }); |
| 28 | |
| 29 | it(`${name}: a self-hosted instance defaults the Diffinery URL setting to itself and changes nothing else`, async () => { |
| 30 | const p = packed(name); |
| 31 | const before = await files(p.bytes); |
| 32 | const out = await pluginZipFor("https://git.example.com", p.bytes, p.sha256); |
| 33 | expect(out.sha256).toBe(createHash("sha256").update(out.bytes).digest("hex")); |
| 34 | const after = await files(out.bytes); |
| 35 | expect(Object.keys(after)).toEqual(Object.keys(before)); |
| 36 | const manifest = JSON.parse(after[".claude-plugin/plugin.json"]!); |
| 37 | expect(manifest.userConfig.url.default).toBe("https://git.example.com"); |
| 38 | expect({ ...manifest, userConfig: undefined }).toEqual({ ...JSON.parse(before[".claude-plugin/plugin.json"]!), userConfig: undefined }); |
| 39 | for (const f of Object.keys(before)) if (f !== ".claude-plugin/plugin.json") expect(after[f]).toBe(before[f]); |
| 40 | // The same instance gets the same bytes every time (a fixed timestamp, as pack-plugins.py writes). |
| 41 | expect((await pluginZipFor("https://git.example.com", p.bytes, p.sha256)).sha256).toBe(out.sha256); |
| 42 | }); |
| 43 | } |
| 44 | |
| 45 | it("the diffinery plugin's MCP server follows the setting", async () => { |
| 46 | const mcp = JSON.parse((await files(packed("diffinery").bytes))[".mcp.json"]!); |
| 47 | expect(mcp.mcpServers.diffinery.url).toBe("${user_config.url}/mcp"); |
| 48 | }); |
| 49 | }); |