diff --git a/src/secrets/channel-contract-api.fast-path.test.ts b/src/secrets/channel-contract-api.fast-path.test.ts new file mode 100644 index 00000000000..5954278bf85 --- /dev/null +++ b/src/secrets/channel-contract-api.fast-path.test.ts @@ -0,0 +1,50 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const { loadPluginManifestRegistryMock } = vi.hoisted(() => ({ + loadPluginManifestRegistryMock: vi.fn(() => { + throw new Error("manifest registry should stay off the explicit bundled channel fast path"); + }), +})); + +vi.mock("../plugins/manifest-registry.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + loadPluginManifestRegistry: loadPluginManifestRegistryMock, + }; +}); + +import { + loadBundledChannelSecretContractApi, + loadBundledChannelSecurityContractApi, +} from "./channel-contract-api.js"; + +describe("channel contract api explicit fast path", () => { + beforeEach(() => { + loadPluginManifestRegistryMock.mockClear(); + }); + + it("resolves bundled channel secret contracts by explicit channel id without manifest scans", () => { + const api = loadBundledChannelSecretContractApi("bluebubbles"); + + expect(api?.collectRuntimeConfigAssignments).toBeTypeOf("function"); + expect(api?.secretTargetRegistryEntries).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "channels.bluebubbles.accounts.*.password", + }), + ]), + ); + expect(loadPluginManifestRegistryMock).not.toHaveBeenCalled(); + }); + + it("resolves bundled channel security contracts by explicit channel id without manifest scans", () => { + const api = loadBundledChannelSecurityContractApi("whatsapp"); + + expect(api?.unsupportedSecretRefSurfacePatterns).toEqual( + expect.arrayContaining(["channels.whatsapp.creds.json"]), + ); + expect(api?.collectUnsupportedSecretRefConfigCandidates).toBeTypeOf("function"); + expect(loadPluginManifestRegistryMock).not.toHaveBeenCalled(); + }); +}); diff --git a/src/secrets/channel-contract-api.ts b/src/secrets/channel-contract-api.ts index e873288211c..5d5fe949e5c 100644 --- a/src/secrets/channel-contract-api.ts +++ b/src/secrets/channel-contract-api.ts @@ -50,31 +50,45 @@ function loadBundledChannelPublicArtifact( channelId: string, artifactBasenames: readonly string[], ): BundledChannelContractApi | undefined { - const dirName = getBundledChannelDirName(channelId); - if (!dirName) { - return undefined; - } + const triedDirNames = new Set(); + const tryDirName = (dirName: string | undefined): BundledChannelContractApi | undefined => { + if (typeof dirName !== "string" || dirName.trim().length === 0 || triedDirNames.has(dirName)) { + return undefined; + } + triedDirNames.add(dirName); - for (const artifactBasename of artifactBasenames) { - try { - return loadBundledPluginPublicArtifactModuleSync({ - dirName, - artifactBasename, - }); - } catch (error) { - if ( - error instanceof Error && - error.message.startsWith("Unable to resolve bundled plugin public surface ") - ) { - continue; - } - if (process.env.OPENCLAW_DEBUG_CHANNEL_CONTRACT_API === "1") { - const detail = formatErrorMessage(error); - process.stderr.write( - `[channel-contract-api] failed to load ${channelId} via ${dirName}/${artifactBasename}: ${detail}\n`, - ); + for (const artifactBasename of artifactBasenames) { + try { + return loadBundledPluginPublicArtifactModuleSync({ + dirName, + artifactBasename, + }); + } catch (error) { + if ( + error instanceof Error && + error.message.startsWith("Unable to resolve bundled plugin public surface ") + ) { + continue; + } + if (process.env.OPENCLAW_DEBUG_CHANNEL_CONTRACT_API === "1") { + const detail = formatErrorMessage(error); + process.stderr.write( + `[channel-contract-api] failed to load ${channelId} via ${dirName}/${artifactBasename}: ${detail}\n`, + ); + } } } + return undefined; + }; + + const direct = tryDirName(channelId); + if (direct) { + return direct; + } + + const fallback = tryDirName(getBundledChannelDirName(channelId)); + if (fallback) { + return fallback; } return undefined;