Files
openclaw/src/channels/plugins/acp-bindings.test.ts
Bob ea15819ecf ACP: harden startup and move configured routing behind plugin seams (#48197)
* ACPX: keep plugin-local runtime installs out of dist

* Gateway: harden ACP startup and service PATH

* ACP: reinitialize error-state configured bindings

* ACP: classify pre-turn runtime failures as session init failures

* Plugins: move configured ACP routing behind channel seams

* Telegram tests: align startup probe assertions after rebase

* Discord: harden ACP configured binding recovery

* ACP: recover Discord bindings after stale runtime exits

* ACPX: replace dead sessions during ensure

* Discord: harden ACP binding recovery

* Discord: fix review follow-ups

* ACP bindings: load channel snapshots across workspaces

* ACP bindings: cache snapshot channel plugin resolution

* Experiments: add ACP pluginification holy grail plan

* Experiments: rename ACP pluginification plan doc

* Experiments: drop old ACP pluginification doc path

* ACP: move configured bindings behind plugin services

* Experiments: update bindings capability architecture plan

* Bindings: isolate configured binding routing and targets

* Discord tests: fix runtime env helper path

* Tests: fix channel binding CI regressions

* Tests: normalize ACP workspace assertion on Windows

* Bindings: isolate configured binding registry

* Bindings: finish configured binding cleanup

* Bindings: finish generic cleanup

* Bindings: align runtime approval callbacks

* ACP: delete residual bindings barrel

* Bindings: restore legacy compatibility

* Revert "Bindings: restore legacy compatibility"

This reverts commit ac2ed68fa2426ecc874d68278c71c71ad363fcfe.

* Tests: drop ACP route legacy helper names

* Discord/ACP: fix binding regressions

---------

Co-authored-by: Onur <2453968+osolmaz@users.noreply.github.com>
2026-03-17 17:27:52 +01:00

253 lines
8.5 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import { buildConfiguredAcpSessionKey } from "../../acp/persistent-bindings.types.js";
const resolveAgentConfigMock = vi.hoisted(() => vi.fn());
const resolveDefaultAgentIdMock = vi.hoisted(() => vi.fn());
const resolveAgentWorkspaceDirMock = vi.hoisted(() => vi.fn());
const getChannelPluginMock = vi.hoisted(() => vi.fn());
const getActivePluginRegistryMock = vi.hoisted(() => vi.fn());
const getActivePluginRegistryVersionMock = vi.hoisted(() => vi.fn());
vi.mock("../../agents/agent-scope.js", () => ({
resolveAgentConfig: (...args: unknown[]) => resolveAgentConfigMock(...args),
resolveDefaultAgentId: (...args: unknown[]) => resolveDefaultAgentIdMock(...args),
resolveAgentWorkspaceDir: (...args: unknown[]) => resolveAgentWorkspaceDirMock(...args),
}));
vi.mock("./index.js", () => ({
getChannelPlugin: (...args: unknown[]) => getChannelPluginMock(...args),
}));
vi.mock("../../plugins/runtime.js", () => ({
getActivePluginRegistry: (...args: unknown[]) => getActivePluginRegistryMock(...args),
getActivePluginRegistryVersion: (...args: unknown[]) =>
getActivePluginRegistryVersionMock(...args),
}));
async function importConfiguredBindings() {
const builtins = await import("./configured-binding-builtins.js");
builtins.ensureConfiguredBindingBuiltinsRegistered();
return await import("./configured-binding-registry.js");
}
function createConfig(options?: { bindingAgentId?: string; accountId?: string }) {
return {
agents: {
list: [{ id: "main" }, { id: "codex" }],
},
bindings: [
{
type: "acp",
agentId: options?.bindingAgentId ?? "codex",
match: {
channel: "discord",
accountId: options?.accountId ?? "default",
peer: {
kind: "channel",
id: "1479098716916023408",
},
},
acp: {
backend: "acpx",
},
},
],
};
}
function createDiscordAcpPlugin(overrides?: {
compileConfiguredBinding?: ReturnType<typeof vi.fn>;
matchInboundConversation?: ReturnType<typeof vi.fn>;
}) {
const compileConfiguredBinding =
overrides?.compileConfiguredBinding ??
vi.fn(({ conversationId }: { conversationId: string }) => ({
conversationId,
}));
const matchInboundConversation =
overrides?.matchInboundConversation ??
vi.fn(
({
compiledBinding,
conversationId,
parentConversationId,
}: {
compiledBinding: { conversationId: string };
conversationId: string;
parentConversationId?: string;
}) => {
if (compiledBinding.conversationId === conversationId) {
return { conversationId, matchPriority: 2 };
}
if (parentConversationId && compiledBinding.conversationId === parentConversationId) {
return { conversationId: parentConversationId, matchPriority: 1 };
}
return null;
},
);
return {
id: "discord",
bindings: {
compileConfiguredBinding,
matchInboundConversation,
},
};
}
describe("configured binding registry", () => {
beforeEach(() => {
vi.resetModules();
resolveAgentConfigMock.mockReset().mockReturnValue(undefined);
resolveDefaultAgentIdMock.mockReset().mockReturnValue("main");
resolveAgentWorkspaceDirMock.mockReset().mockReturnValue("/tmp/workspace");
getChannelPluginMock.mockReset();
getActivePluginRegistryMock.mockReset().mockReturnValue({ channels: [] });
getActivePluginRegistryVersionMock.mockReset().mockReturnValue(1);
});
it("resolves configured ACP bindings from an already loaded channel plugin", async () => {
const plugin = createDiscordAcpPlugin();
getChannelPluginMock.mockReturnValue(plugin);
const bindingRegistry = await importConfiguredBindings();
const resolved = bindingRegistry.resolveConfiguredBindingRecord({
cfg: createConfig() as never,
channel: "discord",
accountId: "default",
conversationId: "1479098716916023408",
});
expect(resolved?.record.conversation.channel).toBe("discord");
expect(resolved?.record.metadata?.backend).toBe("acpx");
expect(plugin.bindings?.compileConfiguredBinding).toHaveBeenCalledTimes(1);
});
it("resolves configured ACP bindings from canonical conversation refs", async () => {
const plugin = createDiscordAcpPlugin();
getChannelPluginMock.mockReturnValue(plugin);
const bindingRegistry = await importConfiguredBindings();
const resolved = bindingRegistry.resolveConfiguredBinding({
cfg: createConfig() as never,
conversation: {
channel: "discord",
accountId: "default",
conversationId: "1479098716916023408",
},
});
expect(resolved?.conversation).toEqual({
channel: "discord",
accountId: "default",
conversationId: "1479098716916023408",
});
expect(resolved?.record.conversation.channel).toBe("discord");
expect(resolved?.statefulTarget).toEqual({
kind: "stateful",
driverId: "acp",
sessionKey: resolved?.record.targetSessionKey,
agentId: "codex",
label: undefined,
});
});
it("primes compiled ACP bindings from the already loaded active registry once", async () => {
const plugin = createDiscordAcpPlugin();
const cfg = createConfig({ bindingAgentId: "codex" });
getChannelPluginMock.mockReturnValue(undefined);
getActivePluginRegistryMock.mockReturnValue({
channels: [{ plugin }],
});
const bindingRegistry = await importConfiguredBindings();
const primed = bindingRegistry.primeConfiguredBindingRegistry({
cfg: cfg as never,
});
const resolved = bindingRegistry.resolveConfiguredBindingRecord({
cfg: cfg as never,
channel: "discord",
accountId: "default",
conversationId: "1479098716916023408",
});
expect(primed).toEqual({ bindingCount: 1, channelCount: 1 });
expect(resolved?.statefulTarget.agentId).toBe("codex");
expect(plugin.bindings?.compileConfiguredBinding).toHaveBeenCalledTimes(1);
const second = bindingRegistry.resolveConfiguredBindingRecord({
cfg: cfg as never,
channel: "discord",
accountId: "default",
conversationId: "1479098716916023408",
});
expect(second?.statefulTarget.agentId).toBe("codex");
});
it("resolves wildcard binding session keys from the compiled registry", async () => {
const plugin = createDiscordAcpPlugin();
getChannelPluginMock.mockReturnValue(plugin);
const bindingRegistry = await importConfiguredBindings();
const resolved = bindingRegistry.resolveConfiguredBindingRecordBySessionKey({
cfg: createConfig({ accountId: "*" }) as never,
sessionKey: buildConfiguredAcpSessionKey({
channel: "discord",
accountId: "work",
conversationId: "1479098716916023408",
agentId: "codex",
mode: "persistent",
backend: "acpx",
}),
});
expect(resolved?.record.conversation.channel).toBe("discord");
expect(resolved?.record.conversation.accountId).toBe("work");
expect(resolved?.record.metadata?.backend).toBe("acpx");
});
it("does not perform late plugin discovery when a channel plugin is unavailable", async () => {
const bindingRegistry = await importConfiguredBindings();
const resolved = bindingRegistry.resolveConfiguredBindingRecord({
cfg: createConfig() as never,
channel: "discord",
accountId: "default",
conversationId: "1479098716916023408",
});
expect(resolved).toBeNull();
});
it("rebuilds the compiled registry when the active plugin registry version changes", async () => {
const plugin = createDiscordAcpPlugin();
getChannelPluginMock.mockReturnValue(plugin);
getActivePluginRegistryVersionMock.mockReturnValue(10);
const cfg = createConfig();
const bindingRegistry = await importConfiguredBindings();
bindingRegistry.resolveConfiguredBindingRecord({
cfg: cfg as never,
channel: "discord",
accountId: "default",
conversationId: "1479098716916023408",
});
bindingRegistry.resolveConfiguredBindingRecord({
cfg: cfg as never,
channel: "discord",
accountId: "default",
conversationId: "1479098716916023408",
});
getActivePluginRegistryVersionMock.mockReturnValue(11);
bindingRegistry.resolveConfiguredBindingRecord({
cfg: cfg as never,
channel: "discord",
accountId: "default",
conversationId: "1479098716916023408",
});
expect(plugin.bindings?.compileConfiguredBinding).toHaveBeenCalledTimes(2);
});
});