mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-19 14:00:51 +00:00
* 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>
253 lines
8.5 KiB
TypeScript
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);
|
|
});
|
|
});
|