Files
kandev/apps/web/lib/plugins/active-plugin.test.ts
T
Kandev Agent c53f699714 feat: add plugin system with grpc runtime and native ui plugins
- go plugin backends ship as release tarballs (per-platform binaries, checksums) that
  kandev installs from url/upload or filesystem sideload+sync, then spawns and
  supervises via hashicorp/go-plugin (unix socket / loopback tcp+automtls)
- strict typed grpc contract (kandev.plugin.v1): DeliverEvent/InvokeTool/HandleWebhook
  plus a capability-gated host service (state, secrets, emit); author sdk in
  pkg/pluginsdk; packaging cli in cmd/plugin-pack
- native frontend bundles: window.registerKandevPlugin + registry/host loader with
  dynamic routes, nav items, slots and ws handlers; plugins settings page with
  install dialog, sync button and enable/disable/uninstall
- gated behind the plugins feature flag (prod off, dev/e2e on); covered by unit and
  integration tests plus playwright e2e against a real packaged fixture plugin

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 17:55:45 +01:00

59 lines
1.9 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { toActivePlugin } from "./active-plugin";
import type { PluginRecord } from "@/lib/types/plugins";
function record(overrides: Partial<PluginRecord> = {}): PluginRecord {
return {
id: "acme-tools",
api_version: 1,
version: "1.0.0",
display_name: "Acme Tools",
description: "",
author: "",
categories: [],
capabilities: {},
status: "active",
install_path: "/home/user/.kandev/plugins/acme-tools/1.0.0",
signed: true,
installed_at: "2026-01-01T00:00:00Z",
restart_count: 0,
ui: { bundle: "/ui/bundle.js" },
...overrides,
};
}
describe("toActivePlugin", () => {
it("builds the bundle URL from the plugin id, mirroring the backend's boot payload mapping", () => {
const result = toActivePlugin(record());
expect(result).not.toBeNull();
expect(result?.bundleUrl).toBe("/api/plugins/acme-tools/bundle");
expect(result?.id).toBe("acme-tools");
expect(result?.name).toBe("Acme Tools");
});
it("maps ui.styles to /api/plugins/:id/ui/:style, mirroring pluginStyleURLs on the backend", () => {
const result = toActivePlugin(
record({ ui: { bundle: "/ui/bundle.js", styles: ["/ui/a.css", "/ui/b.css"] } }),
);
expect(result?.styleUrls).toEqual([
"/api/plugins/acme-tools/ui/ui/a.css",
"/api/plugins/acme-tools/ui/ui/b.css",
]);
});
it("omits styleUrls when the plugin declares no styles", () => {
const result = toActivePlugin(record({ ui: { bundle: "/ui/bundle.js" } }));
expect(result?.styleUrls).toBeUndefined();
});
it("returns null when the plugin declares no UI bundle", () => {
const result = toActivePlugin(record({ ui: undefined }));
expect(result).toBeNull();
});
it("returns null when ui.bundle is an empty string", () => {
const result = toActivePlugin(record({ ui: { bundle: "" } }));
expect(result).toBeNull();
});
});