/** * Install, update, uninstall and switching, against a real PluginLoader and * fixture plugins on disk. The database and the registry are faked. */ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import crypto from "node:crypto"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import * as tar from "tar"; import { createFixturePlugin } from "./fixture-plugin.js"; import type { LoadedPlugin } from "../../plugins/loader.js"; interface Row { id: string; name: string; version: string; tier: string; source: string; state: string; lastError: string | null; registryId: string | null; autoUpdate: boolean; pinnedVersion: string | null; manifestJson: string; } const db = vi.hoisted(() => ({ rows: new Map>(), settings: new Map(), grants: [] as Array<{ pluginId: string; capability: string }>, })); const registry = vi.hoisted(() => ({ index: { registry: "official", name: "", plugins: [] as unknown[] }, /** Indexes of the other registries, by id. Missing means unreachable. */ others: {} as Record>, download: vi.fn(), })); vi.mock("../../database/repositories/factory.js", () => ({ createCurrentPluginRepository: () => ({ listAll: async () => [...db.rows.values()], findById: async (id: string) => db.rows.get(id) ?? null, create: async (input: Record) => { const row = { registryId: null, lastError: null, autoUpdate: false, pinnedVersion: null, ...input, }; db.rows.set(input.id as string, row); return row; }, update: async (id: string, input: Record) => { const row = db.rows.get(id); if (!row) return null; for (const [key, value] of Object.entries(input)) { if (value !== undefined) row[key] = value; } return row; }, delete: async (id: string) => db.rows.delete(id), }), createCurrentSettingsRepository: () => ({ get: async (key: string) => db.settings.get(key) ?? null, set: async (key: string, value: string) => void db.settings.set(key, value), }), createCurrentPluginPermissionGrantRepository: () => ({ listByPlugin: async (id: string) => db.grants.filter((g) => g.pluginId === id), grant: async (input: { pluginId: string; capability: string }) => void db.grants.push(input), }), createCurrentPluginStorageRepository: () => ({ get: async () => null, set: async () => {}, delete: async () => false, listKeys: async () => [], countKeys: async () => 0, }), })); vi.mock("../../plugins/data.js", () => ({ migratePlugin: async () => [], removePluginData: async () => ({ tables: [], kvKeys: 0, migrations: 0 }), describePluginTables: async () => [], registerTable: () => ({}), forgetTables: () => {}, })); vi.mock("../../plugins/permissions.js", async (importOriginal) => ({ ...(await importOriginal()), })); vi.mock("../../plugins/registry-index.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, fetchRegistryIndex: async (source?: { id: string }) => { if (!source || source.id === "official") { return actual.parseRegistryIndex(registry.index); } const other = registry.others[source.id]; if (!other) throw new Error(`${source.id} is offline`); return actual.parseRegistryIndex(other); }, downloadRelease: registry.download, }; }); vi.mock("../../plugins/index.js", async () => { const { PluginLoader } = await import("../../plugins/loader.js"); const loader = new PluginLoader(); const seed = async (plugins: LoadedPlugin[]) => { for (const plugin of plugins) { if (db.rows.has(plugin.id)) { Object.assign(db.rows.get(plugin.id)!, { version: plugin.manifest.version, manifestJson: JSON.stringify(plugin.manifest), }); continue; } db.rows.set(plugin.id, { id: plugin.id, name: plugin.manifest.name, version: plugin.manifest.version, tier: plugin.source === "bundled" ? "bundled" : "community", source: plugin.source, state: plugin.source === "bundled" ? "enabled" : "disabled", lastError: null, registryId: null, autoUpdate: false, pinnedVersion: null, manifestJson: JSON.stringify(plugin.manifest), } satisfies Row); } }; return { getPluginRuntime: () => ({ loader }), activatePlugin: (id: string) => loader.activate(id), deactivatePlugin: (id: string) => loader.deactivate(id), registerLoadedPlugins: seed, unloadPlugin: async (id: string) => { if (!loader.get(id)) return; await loader.deactivate(id); loader.forget(id); }, }; }); const { getPluginRuntime } = await import("../../plugins/index.js"); const manage = await import("../../plugins/manage.js"); const roots: string[] = []; let bundled: string; let data: string; function tempRoot(prefix: string): string { const root = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); roots.push(root); return root; } const DEFAULT_CAPS = ["hosts:read", "kv:own"]; function registryEntry( id: string, versions: Array<{ version: string; capabilities?: string[] }>, prereleases: Array<{ version: string; capabilities?: string[] }> = [], ) { const toEntry = (v: { version: string; capabilities?: string[] }) => ({ version: v.version, api: "1", url: `https://example.com/${id}-${v.version}.tmxplug`, sha256: "a".repeat(64), signature: "sig", size: 1, capabilities: v.capabilities ?? ["hosts:read", "kv:own"], }); return { id, name: id, versions: versions.map(toEntry), prereleases: prereleases.map(toEntry), }; } /** A .tmxplug of a fixture plugin, standing in for a verified download. */ async function stageArtifact( id: string, manifestOverrides: Record = {}, ) { const build = tempRoot("termix-build-"); createFixturePlugin({ id, root: build, manifestOverrides }); const staging = path.join(data, "plugins", ".downloads"); fs.mkdirSync(staging, { recursive: true }); const file = path.join(staging, `${id}-staged.tmxplug`); await tar.c({ gzip: true, file, cwd: path.join(build, id) }, ["."]); return { file, signatureFile: `${file}.sig` }; } async function boot() { const { loader } = getPluginRuntime(); await manage.applyStoredPluginChoices(loader); const loaded = await loader.loadAll(); const { registerLoadedPlugins } = await import("../../plugins/index.js"); await registerLoadedPlugins(loaded); for (const plugin of loaded) { if (db.rows.get(plugin.id)?.state === "enabled") { await loader.activate(plugin.id); } } return loader; } beforeEach(() => { bundled = tempRoot("termix-bundled-"); data = tempRoot("termix-data-"); process.env.TERMIX_BUNDLED_PLUGINS_DIR = bundled; process.env.DATA_DIR = data; db.rows.clear(); db.settings.clear(); db.grants.length = 0; registry.index.plugins = []; registry.others = {}; registry.download.mockReset(); }); afterEach(async () => { const { loader } = getPluginRuntime(); await loader.shutdown(); for (const plugin of loader.list()) loader.forget(plugin.id); delete process.env.TERMIX_BUNDLED_PLUGINS_DIR; delete process.env.DATA_DIR; while (roots.length) { fs.rmSync(roots.pop()!, { recursive: true, force: true }); } }); describe("uninstall and reinstall of a bundled plugin", () => { it("deletes it, keeps it gone after a restart, and downloads it again", async () => { createFixturePlugin({ id: "docker", root: bundled }); registry.index.plugins = [registryEntry("docker", [{ version: "1.0.0" }])]; let loader = await boot(); expect(loader.get("docker")?.state).toBe("active"); await manage.uninstallPlugin("docker"); expect(loader.get("docker")).toBeUndefined(); expect(db.rows.has("docker")).toBe(false); expect(fs.existsSync(path.join(bundled, "docker"))).toBe(false); expect(JSON.parse(db.settings.get("plugins_uninstalled")!)).toEqual([ "docker", ]); await loader.shutdown(); for (const plugin of loader.list()) loader.forget(plugin.id); // A recreated container brings the image's copy back; it stays ignored. createFixturePlugin({ id: "docker", root: bundled }); loader = await boot(); expect(loader.get("docker")).toBeUndefined(); registry.download.mockImplementation(() => stageArtifact("docker")); const result = await manage.installPlugin("docker", { userId: "admin", capabilities: DEFAULT_CAPS, }); expect(result.state).toBe("active"); expect(registry.download).toHaveBeenCalledOnce(); expect(loader.get("docker")?.source).toBe("user"); expect(fs.existsSync(path.join(data, "plugins", "docker.tmxplug"))).toBe( true, ); expect(db.rows.get("docker")).toMatchObject({ registryId: "official", tier: "official", }); expect(db.grants.map((g) => g.capability)).toEqual([ "hosts:read", "kv:own", ]); await loader.shutdown(); for (const plugin of loader.list()) loader.forget(plugin.id); loader = await boot(); expect(loader.get("docker")?.source).toBe("user"); }); it("deletes a plugin dropped in as a plain folder", async () => { createFixturePlugin({ id: "notes", root: path.join(data, "plugins") }); const loader = await boot(); expect(loader.get("notes")?.source).toBe("user"); await manage.uninstallPlugin("notes"); expect(fs.existsSync(path.join(data, "plugins", "notes"))).toBe(false); expect(fs.existsSync(path.join(data, "plugins"))).toBe(true); expect(db.settings.get("plugins_uninstalled")).toBeUndefined(); }); it("refuses to install something already installed", async () => { createFixturePlugin({ id: "docker", root: bundled }); registry.index.plugins = [registryEntry("docker", [{ version: "1.0.0" }])]; await boot(); await expect( manage.installPlugin("docker", { userId: "admin" }), ).rejects.toMatchObject({ code: "ALREADY_INSTALLED" }); }); }); describe("installing from the registry", () => { it("puts the artifact in place, grants what it declares and starts it", async () => { registry.index.plugins = [registryEntry("notes", [{ version: "1.0.0" }])]; registry.download.mockImplementation(() => stageArtifact("notes", { capabilities: ["kv:own"] }), ); const loader = await boot(); const result = await manage.installPlugin("notes", { userId: "admin", capabilities: DEFAULT_CAPS, }); expect(result).toMatchObject({ id: "notes", state: "active" }); expect(fs.existsSync(path.join(data, "plugins", "notes.tmxplug"))).toBe( true, ); expect(loader.get("notes")?.source).toBe("user"); expect(db.rows.get("notes")?.tier).toBe("official"); expect(db.grants).toEqual([ expect.objectContaining({ pluginId: "notes", capability: "kv:own" }), ]); }); it("refuses a plugin that is not in the registry", async () => { await boot(); await expect( manage.installPlugin("nope", { userId: "admin" }), ).rejects.toMatchObject({ code: "NOT_IN_REGISTRY" }); }); it("pins an explicitly picked older version", async () => { registry.index.plugins = [ registryEntry("notes", [{ version: "1.1.0" }, { version: "1.0.0" }]), ]; registry.download.mockImplementation(() => stageArtifact("notes")); await boot(); await manage.installPlugin("notes", { userId: "admin", version: "1.0.0", capabilities: DEFAULT_CAPS, }); expect(db.rows.get("notes")?.pinnedVersion).toBe("1.0.0"); }); it("uninstalls a registry plugin and its files", async () => { registry.index.plugins = [registryEntry("notes", [{ version: "1.0.0" }])]; registry.download.mockImplementation(() => stageArtifact("notes")); await boot(); await manage.installPlugin("notes", { userId: "admin", capabilities: DEFAULT_CAPS, }); await manage.uninstallPlugin("notes"); expect(fs.existsSync(path.join(data, "plugins", "notes.tmxplug"))).toBe( false, ); expect(db.rows.has("notes")).toBe(false); expect(db.settings.get("plugins_uninstalled")).toBeUndefined(); }); }); describe("updates", () => { it("waits for consent when a release asks for new capabilities", async () => { createFixturePlugin({ id: "docker", root: bundled }); registry.index.plugins = [ registryEntry("docker", [ { version: "1.1.0", capabilities: ["hosts:read", "kv:own", "notify:send"], }, { version: "1.0.0" }, ]), ]; await boot(); await expect( manage.updatePlugin("docker", { userId: "admin" }), ).rejects.toMatchObject({ code: "CAPABILITIES_ADDED", details: { capabilities: ["notify:send"] }, }); expect(registry.download).not.toHaveBeenCalled(); }); it("lists an update and skips it in update-all when pinned", async () => { createFixturePlugin({ id: "docker", root: bundled }); registry.index.plugins = [ registryEntry("docker", [{ version: "1.1.0" }, { version: "1.0.0" }]), ]; await boot(); const listing = await manage.listRegistry(); expect(listing.plugins[0]).toMatchObject({ installed: true, installedVersion: "1.0.0", latestVersion: "1.1.0", updateAvailable: true, addedCapabilities: [], bundled: true, }); await manage.setPluginOptions("docker", { pinned: true }); const result = await manage.updateAllPlugins({ userId: "admin" }); expect(result.updated).toEqual([]); expect(registry.download).not.toHaveBeenCalled(); }); }); describe("beta channel", () => { it("offers betas only to a plugin on the beta channel", async () => { createFixturePlugin({ id: "docker", root: bundled }); registry.index.plugins = [ registryEntry( "docker", [{ version: "1.0.0" }], [{ version: "1.1.0-beta.1" }], ), ]; await boot(); expect((await manage.listRegistry()).plugins[0]).toMatchObject({ channel: "stable", latestVersion: "1.0.0", latestBeta: "1.1.0-beta.1", updateAvailable: false, }); expect( (await manage.listRegistry()).plugins[0].versions.map((v) => [ v.version, v.prerelease, ]), ).toEqual([ ["1.1.0-beta.1", true], ["1.0.0", false], ]); await manage.setPluginOptions("docker", { channel: "beta" }); expect((await manage.listRegistry()).plugins[0]).toMatchObject({ channel: "beta", latestVersion: "1.1.0-beta.1", updateAvailable: true, }); }); it("keeps an installed beta after going back to stable", async () => { createFixturePlugin({ id: "docker", root: bundled, manifestOverrides: { version: "1.1.0-beta.1" }, }); registry.index.plugins = [ registryEntry( "docker", [{ version: "1.0.0" }], [{ version: "1.1.0-beta.1" }], ), ]; await boot(); expect((await manage.listRegistry()).plugins[0]).toMatchObject({ channel: "stable", installedVersion: "1.1.0-beta.1", updateAvailable: false, }); const result = await manage.updateAllPlugins({ userId: "admin" }); expect(result.updated).toEqual([]); expect(registry.download).not.toHaveBeenCalled(); registry.index.plugins = [ registryEntry("docker", [{ version: "1.1.0" }, { version: "1.0.0" }]), ]; expect((await manage.listRegistry()).plugins[0]).toMatchObject({ latestVersion: "1.1.0", updateAvailable: true, }); }); it("installs the newest beta when asked to follow betas", async () => { registry.index.plugins = [ registryEntry( "notes", [{ version: "1.0.0" }], [{ version: "1.1.0-beta.2" }], ), ]; registry.download.mockImplementation(() => stageArtifact("notes", { version: "1.1.0-beta.2" }), ); await boot(); const result = await manage.installPlugin("notes", { userId: "admin", channel: "beta", capabilities: DEFAULT_CAPS, }); expect(result.version).toBe("1.1.0-beta.2"); expect(db.rows.get("notes")).toMatchObject({ channel: "beta", pinnedVersion: null, }); }); it("moves every installed plugin to one channel", async () => { createFixturePlugin({ id: "docker", root: bundled }); createFixturePlugin({ id: "notes", root: bundled }); await boot(); expect((await manage.setAllPluginChannels("beta")).changed.sort()).toEqual([ "docker", "notes", ]); expect(db.rows.get("docker")?.channel).toBe("beta"); expect((await manage.setAllPluginChannels("beta")).changed).toEqual([]); }); }); describe("dependencies", () => { it("starts a dependency first and stops dependents first", async () => { createFixturePlugin({ id: "base", root: bundled }); createFixturePlugin({ id: "addon", root: bundled, manifestOverrides: { dependencies: { base: "^1.0.0" } }, }); const loader = await boot(); const off = await manage.setPluginState("base", false); expect(off.disable).toEqual(["addon"]); expect(loader.get("addon")?.state).toBe("stopped"); expect(loader.get("base")?.state).toBe("stopped"); const plan = await manage.planStateChange("addon", true); expect(plan.enable).toEqual(["base"]); const on = await manage.setPluginState("addon", true); expect(on.state).toBe("active"); expect(loader.get("base")?.state).toBe("active"); }); it("restarts dependents it stopped when turned back on", async () => { createFixturePlugin({ id: "base", root: bundled }); createFixturePlugin({ id: "addon", root: bundled, manifestOverrides: { dependencies: { base: "^1.0.0" } }, }); createFixturePlugin({ id: "manual", root: bundled, manifestOverrides: { dependencies: { base: "^1.0.0" } }, }); const loader = await boot(); await manage.setPluginState("manual", false); await manage.setPluginState("base", false); expect(loader.get("addon")?.state).toBe("stopped"); await manage.setPluginState("base", true); expect(loader.get("addon")?.state).toBe("active"); expect(db.rows.get("addon")).toMatchObject({ state: "enabled", lastError: null, }); expect(loader.get("manual")?.state).not.toBe("active"); expect(db.rows.get("manual")?.state).toBe("disabled"); }); it("refuses to uninstall a plugin a running one depends on", async () => { createFixturePlugin({ id: "base", root: bundled }); createFixturePlugin({ id: "addon", root: bundled, manifestOverrides: { dependencies: { base: "^1.0.0" } }, }); await boot(); await expect(manage.uninstallPlugin("base")).rejects.toMatchObject({ code: "HAS_DEPENDENTS", details: { dependents: ["addon"] }, }); }); }); describe("reloading a bundled plugin for the dev runner", () => { it("loads the rebuilt copy and restarts what depends on it", async () => { createFixturePlugin({ id: "base", root: bundled }); createFixturePlugin({ id: "addon", root: bundled, manifestOverrides: { dependencies: { base: "^1.0.0" } }, }); const loader = await boot(); createFixturePlugin({ id: "base", root: bundled, manifestOverrides: { version: "1.1.0" }, }); const result = await manage.reloadBundledPlugin("base"); expect(result).toEqual({ id: "base", version: "1.1.0", state: "active" }); expect(loader.get("base")?.manifest.version).toBe("1.1.0"); expect(loader.get("addon")?.state).toBe("active"); expect(db.rows.get("base")?.version).toBe("1.1.0"); }); it("loads and starts a plugin added since boot", async () => { const loader = await boot(); createFixturePlugin({ id: "fresh", root: bundled }); const result = await manage.reloadBundledPlugin("fresh"); expect(result.state).toBe("active"); expect(loader.get("fresh")?.state).toBe("active"); }); it("keeps a disabled plugin stopped", async () => { createFixturePlugin({ id: "base", root: bundled }); const loader = await boot(); await manage.setPluginState("base", false); const result = await manage.reloadBundledPlugin("base"); expect(result.state).toBe("stopped"); expect(loader.get("base")?.state).toBe("loaded"); }); }); describe("consent", () => { it("refuses an install without the consented list, before any download", async () => { registry.index.plugins = [registryEntry("notes", [{ version: "1.0.0" }])]; await boot(); await expect( manage.installPlugin("notes", { userId: "admin" }), ).rejects.toMatchObject({ code: "CONSENT_REQUIRED" }); expect(registry.download).not.toHaveBeenCalled(); }); it("refuses when the consent does not match what the release asks for", async () => { registry.index.plugins = [ registryEntry("notes", [ { version: "1.0.0", capabilities: ["kv:own", "credentials:read"] }, ]), ]; await boot(); await expect( manage.installPlugin("notes", { userId: "admin", capabilities: ["kv:own"], }), ).rejects.toMatchObject({ code: "CONSENT_MISMATCH", details: { capabilities: ["kv:own", "credentials:read"] }, }); expect(registry.download).not.toHaveBeenCalled(); expect(db.rows.has("notes")).toBe(false); }); it("accepts the same list in another order", async () => { registry.index.plugins = [registryEntry("notes", [{ version: "1.0.0" }])]; registry.download.mockImplementation(() => stageArtifact("notes")); await boot(); const result = await manage.installPlugin("notes", { userId: "admin", capabilities: ["kv:own", "hosts:read"], }); expect(result.state).toBe("active"); }); it("needs the added capabilities listed to accept an update", async () => { createFixturePlugin({ id: "docker", root: bundled }); registry.index.plugins = [ registryEntry("docker", [ { version: "1.1.0", capabilities: ["hosts:read", "kv:own", "notify:send"], }, { version: "1.0.0" }, ]), ]; await boot(); await expect( manage.updatePlugin("docker", { userId: "admin", acceptCapabilities: true, capabilities: ["ssh:connect"], }), ).rejects.toMatchObject({ code: "CONSENT_MISMATCH" }); await expect( manage.updatePlugin("docker", { userId: "admin", acceptCapabilities: true, }), ).rejects.toMatchObject({ code: "CONSENT_REQUIRED" }); expect(registry.download).not.toHaveBeenCalled(); }); }); describe("installing from a file", () => { async function artifactBuffer( id: string, manifestOverrides: Record = {}, ) { const staged = await stageArtifact(id, manifestOverrides); const buffer = fs.readFileSync(staged.file); fs.rmSync(staged.file); return buffer; } afterEach(() => { manage.resetUploads(); delete process.env.TERMIX_REQUIRE_SIGNED_PLUGINS; }); it("is refused while developer mode is off", async () => { await boot(); await expect( manage.stageUpload(await artifactBuffer("notes")), ).rejects.toMatchObject({ code: "DEVELOPER_MODE_OFF" }); }); it("is refused when signed plugins are required", async () => { await boot(); await manage.setDeveloperMode(true); process.env.TERMIX_REQUIRE_SIGNED_PLUGINS = "true"; await expect( manage.stageUpload(await artifactBuffer("notes")), ).rejects.toMatchObject({ code: "SIGNED_ONLY" }); }); it("stages, asks for consent, then installs it marked unverified", async () => { const loader = await boot(); await manage.setDeveloperMode(true); const preview = await manage.stageUpload( await artifactBuffer("notes", { capabilities: ["kv:own"] }), ); expect(preview).toMatchObject({ id: "notes", capabilities: ["kv:own"], replaces: null, }); expect(loader.get("notes")).toBeUndefined(); await expect( manage.installUpload(preview.token, { userId: "admin", capabilities: ["kv:own", "hosts:read"], }), ).rejects.toMatchObject({ code: "CONSENT_MISMATCH" }); const result = await manage.installUpload(preview.token, { userId: "admin", capabilities: ["kv:own"], }); expect(result.state).toBe("active"); expect(db.rows.get("notes")).toMatchObject({ tier: manage.UNVERIFIED_TIER, registryId: null, }); expect(loader.get("notes")?.signedBy).toBeUndefined(); expect(fs.existsSync(path.join(data, "plugins", "notes.tmxplug"))).toBe( true, ); // A token is used once. await expect( manage.installUpload(preview.token, { userId: "admin", capabilities: ["kv:own"], }), ).rejects.toMatchObject({ code: "UPLOAD_EXPIRED" }); }); it("replaces an earlier upload but never a bundled plugin", async () => { createFixturePlugin({ id: "docker", root: bundled }); const loader = await boot(); await manage.setDeveloperMode(true); await expect( manage.stageUpload(await artifactBuffer("docker")), ).rejects.toMatchObject({ code: "BUNDLED_ID" }); const first = await manage.stageUpload(await artifactBuffer("notes")); await manage.installUpload(first.token, { userId: "admin", capabilities: first.capabilities, }); const second = await manage.stageUpload( await artifactBuffer("notes", { version: "1.1.0" }), ); expect(second.replaces).toBe("1.0.0"); await manage.installUpload(second.token, { userId: "admin", capabilities: second.capabilities, }); expect(loader.get("notes")?.manifest.version).toBe("1.1.0"); expect(db.rows.get("notes")?.tier).toBe(manage.UNVERIFIED_TIER); }); it("refuses a file that is not a plugin archive", async () => { await boot(); await manage.setDeveloperMode(true); await expect( manage.stageUpload(Buffer.from("not a tarball")), ).rejects.toMatchObject({ code: "INVALID_ARTIFACT" }); }); it("drops staged files when developer mode is turned off", async () => { await boot(); await manage.setDeveloperMode(true); const preview = await manage.stageUpload(await artifactBuffer("notes")); await manage.setDeveloperMode(false); await manage.setDeveloperMode(true); await expect( manage.installUpload(preview.token, { userId: "admin", capabilities: preview.capabilities, }), ).rejects.toMatchObject({ code: "UPLOAD_EXPIRED" }); }); }); describe("release notes", () => { it("reads an installed plugin's CHANGELOG.md", async () => { createFixturePlugin({ id: "docker", root: bundled }); fs.writeFileSync( path.join(bundled, "docker", "CHANGELOG.md"), "# Changelog\n\n## 1.1.0 - 2026-10-06\n\n### Fixed\n- A bug\n\n## 1.0.0\n\n### Added\n- First\n", ); await boot(); expect(await manage.getPluginChangelog("docker")).toEqual([ { version: "1.1.0", date: "2026-10-06", changes: [{ type: "fixed", text: "A bug" }], }, { version: "1.0.0", changes: [{ type: "added", text: "First" }] }, ]); }); it("is empty without a changelog and 404s for an unknown plugin", async () => { createFixturePlugin({ id: "docker", root: bundled }); await boot(); expect(await manage.getPluginChangelog("docker")).toEqual([]); await expect(manage.getPluginChangelog("nope")).rejects.toMatchObject({ status: 404, }); }); it("parses registry notes and drops empty ones", () => { expect(manage.readReleaseNotes("Summary.\n\n### Added\n- Thing")).toEqual({ summary: "Summary.", changes: [{ type: "added", text: "Thing" }], }); expect(manage.readReleaseNotes("")).toBeUndefined(); expect(manage.readReleaseNotes(undefined)).toBeUndefined(); }); }); describe("more registries", () => { const customKey = (() => { const { publicKey } = crypto.generateKeyPairSync("ed25519"); return publicKey .export({ format: "der", type: "spki" }) .subarray(12) .toString("base64"); })(); function turnOnCommunity(): void { db.settings.set( "plugin_registries", JSON.stringify({ community: { enabled: true }, custom: [] }), ); } it("lists community plugins only when it is on, never official ids", async () => { registry.index.plugins = [registryEntry("docker", [{ version: "1.0.0" }])]; registry.others.community = { registry: "community", name: "Community", plugins: [ registryEntry("docker", [{ version: "9.0.0" }]), registryEntry("neat", [{ version: "1.0.0" }]), ], }; await boot(); let listing = await manage.listRegistry(); expect(listing.plugins.map((p) => p.id)).toEqual(["docker"]); expect(listing.registries.map((r) => r.id)).toEqual(["official"]); turnOnCommunity(); listing = await manage.listRegistry(); expect( listing.plugins.map((p) => [p.id, p.registryId, p.tier, p.latestVersion]), ).toEqual([ ["docker", "official", "official", "1.0.0"], ["neat", "community", "community", "1.0.0"], ]); expect(listing.registries.find((r) => r.id === "community")).toMatchObject({ tier: "community", plugins: 1, error: null, }); }); it("keeps listing official plugins when another registry is down", async () => { registry.index.plugins = [registryEntry("docker", [{ version: "1.0.0" }])]; turnOnCommunity(); await boot(); const listing = await manage.listRegistry(); expect(listing.plugins.map((p) => p.id)).toEqual(["docker"]); expect(listing.registries.find((r) => r.id === "community")?.error).toMatch( /offline/, ); }); it("installs from the community registry with its keys and records it", async () => { registry.others.community = { registry: "community", plugins: [registryEntry("neat", [{ version: "1.0.0" }])], }; turnOnCommunity(); await boot(); registry.download.mockImplementation(() => stageArtifact("neat")); const result = await manage.installPlugin("neat", { userId: "admin", registryId: "community", capabilities: DEFAULT_CAPS, }); expect(result.state).toBe("active"); const keys = registry.download.mock.calls[0][2] as Array<{ id: string }>; expect(keys.map((key) => key.id)).toEqual(["be5b24ea07b36624"]); expect(db.rows.get("neat")).toMatchObject({ registryId: "community", tier: "community", }); }); it("updates a plugin from the registry it came from", async () => { registry.others.community = { registry: "community", plugins: [ registryEntry("neat", [{ version: "1.1.0" }, { version: "1.0.0" }]), ], }; turnOnCommunity(); await boot(); registry.download.mockImplementation(() => stageArtifact("neat", { version: "1.0.0" }), ); await manage.installPlugin("neat", { userId: "admin", registryId: "community", version: "1.0.0", capabilities: DEFAULT_CAPS, }); await manage.setPluginOptions("neat", { pinned: false }); registry.download.mockReset(); registry.download.mockImplementation(() => stageArtifact("neat", { version: "1.1.0" }), ); const result = await manage.updatePlugin("neat", { userId: "admin" }); expect(result.version).toBe("1.1.0"); const [, release, keys] = registry.download.mock.calls[0]; expect(release.url).toContain("neat-1.1.0"); expect(keys.map((key: { id: string }) => key.id)).toEqual([ "be5b24ea07b36624", ]); expect(db.rows.get("neat")).toMatchObject({ registryId: "community" }); }); it("stops updating a community plugin once the official registry lists its id", async () => { registry.others.community = { registry: "community", plugins: [ registryEntry("neat", [{ version: "1.1.0" }, { version: "1.0.0" }]), ], }; turnOnCommunity(); await boot(); registry.download.mockImplementation(() => stageArtifact("neat")); await manage.installPlugin("neat", { userId: "admin", registryId: "community", version: "1.0.0", capabilities: DEFAULT_CAPS, }); registry.index.plugins = [registryEntry("neat", [{ version: "5.0.0" }])]; await expect( manage.updatePlugin("neat", { userId: "admin", version: "1.1.0" }), ).rejects.toMatchObject({ code: "RESERVED_ID" }); }); it("refuses an official id from another registry", async () => { registry.index.plugins = [registryEntry("docker", [{ version: "1.0.0" }])]; registry.others.community = { registry: "community", plugins: [registryEntry("docker", [{ version: "9.0.0" }])], }; turnOnCommunity(); await boot(); await expect( manage.installPlugin("docker", { userId: "admin", registryId: "community", capabilities: DEFAULT_CAPS, }), ).rejects.toMatchObject({ code: "RESERVED_ID" }); expect(registry.download).not.toHaveBeenCalled(); }); it("refuses a registry that is off", async () => { registry.others.community = { registry: "community", plugins: [registryEntry("neat", [{ version: "1.0.0" }])], }; await boot(); await expect( manage.installPlugin("neat", { userId: "admin", registryId: "community", capabilities: DEFAULT_CAPS, }), ).rejects.toMatchObject({ code: "REGISTRY_OFF" }); }); it("stops trusting a custom registry whose index names a new key", async () => { const other = crypto .generateKeyPairSync("ed25519") .publicKey.export({ format: "der", type: "spki" }) .subarray(12) .toString("base64"); db.settings.set( "plugin_registries", JSON.stringify({ community: { enabled: false }, custom: [ { id: "mine", name: "Mine", url: "https://example.com/index.json", publicKey: customKey, enabled: true, addedAt: "2026-10-11T00:00:00Z", }, ], }), ); registry.others.mine = { registry: "mine", publicKey: customKey, plugins: [registryEntry("tool", [{ version: "1.0.0" }])], }; await boot(); let listing = await manage.listRegistry(); expect(listing.plugins.find((p) => p.id === "tool")?.tier).toBe("custom"); registry.others.mine.publicKey = other; listing = await manage.listRegistry(); expect(listing.plugins.find((p) => p.id === "tool")).toBeUndefined(); expect(listing.registries.find((r) => r.id === "mine")?.error).toMatch( /different signing key/, ); await expect( manage.installPlugin("tool", { userId: "admin", registryId: "mine", capabilities: DEFAULT_CAPS, }), ).rejects.toMatchObject({ code: "KEY_CHANGED" }); }); }); it("requires exact consent before enabling a mirrored plugin with capabilities", async () => { createFixturePlugin({ id: "manual-mirror", root: path.join(data, "plugins"), capabilities: ["kv:own"], backendSource: 'export async function activate(ctx) { await ctx.kv.set("probe", "value"); }', }); await boot(); const options = { version: "1.0.0", capabilities: ["kv:own"], userId: "admin", }; await expect( manage.enablePluginWithConsent("manual-mirror", { ...options, version: "0.9.0", }), ).rejects.toMatchObject({ code: "CONSENT_MISMATCH" }); await expect( manage.enablePluginWithConsent("manual-mirror", { ...options, capabilities: [], }), ).rejects.toMatchObject({ code: "CONSENT_MISMATCH" }); await expect( manage.enablePluginWithConsent("manual-mirror", { version: "1.0.0", userId: "admin", }), ).rejects.toMatchObject({ code: "CONSENT_REQUIRED" }); expect(db.grants).toEqual([]); expect(db.rows.get("manual-mirror")?.state).toBe("disabled"); const result = await manage.enablePluginWithConsent("manual-mirror", options); expect(result.state).toBe("active"); expect(db.grants).toContainEqual( expect.objectContaining({ pluginId: "manual-mirror", capability: "kv:own", grantedBy: "admin", }), ); });