c53f699714
- 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>
53 lines
1.9 KiB
TypeScript
53 lines
1.9 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { pluginRegistry } from "@/lib/plugins/registry";
|
|
import { dispatchToPluginWsHandlers } from "./plugin-bridge";
|
|
|
|
const ACTION = "task.created";
|
|
|
|
function cleanupPlugins(...pluginIds: string[]) {
|
|
pluginIds.forEach((id) => pluginRegistry.unregisterPlugin(id));
|
|
}
|
|
|
|
describe("dispatchToPluginWsHandlers", () => {
|
|
afterEach(() => cleanupPlugins("plugin-a", "plugin-b"));
|
|
|
|
it("does nothing when no plugin registered a handler for the action", () => {
|
|
expect(() => dispatchToPluginWsHandlers(ACTION, { foo: 1 })).not.toThrow();
|
|
});
|
|
|
|
it("forwards the payload to every handler registered for the action", () => {
|
|
const handlerA = vi.fn();
|
|
const handlerB = vi.fn();
|
|
pluginRegistry.forPlugin("plugin-a").registerWsHandler(ACTION, handlerA);
|
|
pluginRegistry.forPlugin("plugin-b").registerWsHandler(ACTION, handlerB);
|
|
|
|
dispatchToPluginWsHandlers(ACTION, { taskId: "t-1" });
|
|
|
|
expect(handlerA).toHaveBeenCalledWith({ taskId: "t-1" });
|
|
expect(handlerB).toHaveBeenCalledWith({ taskId: "t-1" });
|
|
});
|
|
|
|
it("does not forward to a handler registered for a different action", () => {
|
|
const handler = vi.fn();
|
|
pluginRegistry.forPlugin("plugin-a").registerWsHandler("task.deleted", handler);
|
|
|
|
dispatchToPluginWsHandlers(ACTION, { taskId: "t-1" });
|
|
|
|
expect(handler).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("isolates a throwing handler so a sibling handler still runs", () => {
|
|
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
const handlerB = vi.fn();
|
|
pluginRegistry.forPlugin("plugin-a").registerWsHandler(ACTION, () => {
|
|
throw new Error("boom");
|
|
});
|
|
pluginRegistry.forPlugin("plugin-b").registerWsHandler(ACTION, handlerB);
|
|
|
|
expect(() => dispatchToPluginWsHandlers(ACTION, {})).not.toThrow();
|
|
expect(handlerB).toHaveBeenCalled();
|
|
|
|
consoleError.mockRestore();
|
|
});
|
|
});
|