diff --git a/src/channels/plugins/contracts/registry.contract.test.ts b/src/channels/plugins/contracts/registry.contract.test.ts new file mode 100644 index 00000000000..69ff11d8e68 --- /dev/null +++ b/src/channels/plugins/contracts/registry.contract.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, it } from "vitest"; +import { + actionContractRegistry, + pluginContractRegistry, + setupContractRegistry, + statusContractRegistry, + surfaceContractRegistry, + type ChannelPluginSurface, +} from "./registry.js"; + +const orderedSurfaceKeys = [ + "actions", + "setup", + "status", + "outbound", + "messaging", + "threading", + "directory", + "gateway", +] as const satisfies readonly ChannelPluginSurface[]; + +describe("channel contract registry", () => { + it("does not duplicate channel plugin ids", () => { + const ids = pluginContractRegistry.map((entry) => entry.id); + expect(ids).toEqual([...new Set(ids)]); + }); + + it("keeps the surface registry aligned with the plugin registry", () => { + expect(surfaceContractRegistry.map((entry) => entry.id).toSorted()).toEqual( + pluginContractRegistry.map((entry) => entry.id).toSorted(), + ); + }); + + it("declares the actual owned channel plugin surfaces explicitly", () => { + for (const entry of surfaceContractRegistry) { + const actual = orderedSurfaceKeys.filter((surface) => Boolean(entry.plugin[surface])); + expect([...entry.surfaces].toSorted()).toEqual(actual.toSorted()); + } + }); + + it("only installs deep action coverage for plugins that declare actions", () => { + const actionSurfaceIds = new Set( + surfaceContractRegistry + .filter((entry) => entry.surfaces.includes("actions")) + .map((entry) => entry.id), + ); + for (const entry of actionContractRegistry) { + expect(actionSurfaceIds.has(entry.id)).toBe(true); + } + }); + + it("only installs deep setup coverage for plugins that declare setup", () => { + const setupSurfaceIds = new Set( + surfaceContractRegistry + .filter((entry) => entry.surfaces.includes("setup")) + .map((entry) => entry.id), + ); + for (const entry of setupContractRegistry) { + expect(setupSurfaceIds.has(entry.id)).toBe(true); + } + }); + + it("only installs deep status coverage for plugins that declare status", () => { + const statusSurfaceIds = new Set( + surfaceContractRegistry + .filter((entry) => entry.surfaces.includes("status")) + .map((entry) => entry.id), + ); + for (const entry of statusContractRegistry) { + expect(statusSurfaceIds.has(entry.id)).toBe(true); + } + }); +}); diff --git a/src/channels/plugins/contracts/registry.ts b/src/channels/plugins/contracts/registry.ts index 567181cef46..77bf23b335c 100644 --- a/src/channels/plugins/contracts/registry.ts +++ b/src/channels/plugins/contracts/registry.ts @@ -57,6 +57,33 @@ type StatusContractEntry = { }>; }; +export type ChannelPluginSurface = + | "actions" + | "setup" + | "status" + | "outbound" + | "messaging" + | "threading" + | "directory" + | "gateway"; + +type SurfaceContractEntry = { + id: string; + plugin: Pick< + ChannelPlugin, + | "id" + | "actions" + | "setup" + | "status" + | "outbound" + | "messaging" + | "threading" + | "directory" + | "gateway" + >; + surfaces: readonly ChannelPluginSurface[]; +}; + const telegramListActionsMock = vi.fn(); const telegramGetCapabilitiesMock = vi.fn(); const discordListActionsMock = vi.fn(); @@ -461,3 +488,187 @@ export const statusContractRegistry: StatusContractEntry[] = [ ], }, ]; + +export const surfaceContractRegistry: SurfaceContractEntry[] = [ + { + id: "bluebubbles", + plugin: bluebubblesPlugin, + surfaces: ["actions", "setup", "status", "outbound", "messaging", "threading", "gateway"], + }, + { + id: "discord", + plugin: discordPlugin, + surfaces: [ + "actions", + "setup", + "status", + "outbound", + "messaging", + "threading", + "directory", + "gateway", + ], + }, + { + id: "feishu", + plugin: feishuPlugin, + surfaces: ["actions", "setup", "status", "outbound", "messaging", "directory", "gateway"], + }, + { + id: "googlechat", + plugin: googlechatPlugin, + surfaces: [ + "actions", + "setup", + "status", + "outbound", + "messaging", + "threading", + "directory", + "gateway", + ], + }, + { + id: "imessage", + plugin: imessagePlugin, + surfaces: ["setup", "status", "outbound", "messaging", "gateway"], + }, + { + id: "irc", + plugin: ircPlugin, + surfaces: ["setup", "status", "outbound", "messaging", "directory", "gateway"], + }, + { + id: "line", + plugin: linePlugin, + surfaces: ["setup", "status", "outbound", "messaging", "directory", "gateway"], + }, + { + id: "matrix", + plugin: matrixPlugin, + surfaces: [ + "actions", + "setup", + "status", + "outbound", + "messaging", + "threading", + "directory", + "gateway", + ], + }, + { + id: "mattermost", + plugin: mattermostPlugin, + surfaces: [ + "actions", + "setup", + "status", + "outbound", + "messaging", + "threading", + "directory", + "gateway", + ], + }, + { + id: "msteams", + plugin: msteamsPlugin, + surfaces: [ + "actions", + "setup", + "status", + "outbound", + "messaging", + "threading", + "directory", + "gateway", + ], + }, + { + id: "nextcloud-talk", + plugin: nextcloudTalkPlugin, + surfaces: ["setup", "status", "outbound", "messaging", "gateway"], + }, + { + id: "nostr", + plugin: nostrPlugin, + surfaces: ["setup", "status", "outbound", "messaging", "gateway"], + }, + { + id: "signal", + plugin: signalPlugin, + surfaces: ["actions", "setup", "status", "outbound", "messaging", "gateway"], + }, + { + id: "slack", + plugin: slackPlugin, + surfaces: [ + "actions", + "setup", + "status", + "outbound", + "messaging", + "threading", + "directory", + "gateway", + ], + }, + { + id: "synology-chat", + plugin: synologyChatPlugin, + surfaces: ["setup", "outbound", "messaging", "directory", "gateway"], + }, + { + id: "telegram", + plugin: telegramPlugin, + surfaces: [ + "actions", + "setup", + "status", + "outbound", + "messaging", + "threading", + "directory", + "gateway", + ], + }, + { + id: "tlon", + plugin: tlonPlugin, + surfaces: ["setup", "status", "outbound", "messaging", "gateway"], + }, + { + id: "whatsapp", + plugin: whatsappPlugin, + surfaces: ["actions", "setup", "status", "outbound", "messaging", "directory", "gateway"], + }, + { + id: "zalo", + plugin: zaloPlugin, + surfaces: [ + "actions", + "setup", + "status", + "outbound", + "messaging", + "threading", + "directory", + "gateway", + ], + }, + { + id: "zalouser", + plugin: zalouserPlugin, + surfaces: [ + "actions", + "setup", + "status", + "outbound", + "messaging", + "threading", + "directory", + "gateway", + ], + }, +]; diff --git a/src/channels/plugins/contracts/surface.contract.test.ts b/src/channels/plugins/contracts/surface.contract.test.ts new file mode 100644 index 00000000000..6e657bd19fc --- /dev/null +++ b/src/channels/plugins/contracts/surface.contract.test.ts @@ -0,0 +1,14 @@ +import { describe } from "vitest"; +import { surfaceContractRegistry } from "./registry.js"; +import { installChannelSurfaceContractSuite } from "./suites.js"; + +for (const entry of surfaceContractRegistry) { + for (const surface of entry.surfaces) { + describe(`${entry.id} ${surface} surface contract`, () => { + installChannelSurfaceContractSuite({ + plugin: entry.plugin, + surface, + }); + }); + } +}