openclaw / src /channels /plugins /binding-routing.test.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
5cb63c1 verified
Raw History Blame Contribute Delete
7.82 kB
// Binding routing tests cover channel binding selection and message routing behavior.
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
testing,
registerSessionBindingAdapter,
type SessionBindingAdapter,
type SessionBindingRecord,
} from "../../infra/outbound/session-binding-service.js";
import type { ResolvedAgentRoute } from "../../routing/resolve-route.js";
import {
ensureConfiguredBindingRouteReady,
resolveRuntimeConversationBindingRoute,
type RuntimeConversationBindingRouteResult,
} from "./binding-routing.js";
import { registerStatefulBindingTargetDriver } from "./stateful-target-drivers.js";
function createRoute(): ResolvedAgentRoute {
return {
agentId: "main",
channel: "demo",
accountId: "default",
sessionKey: "agent:main:main",
mainSessionKey: "agent:main:main",
lastRoutePolicy: "main",
matchedBy: "default",
};
}
function createBinding(overrides?: Partial<SessionBindingRecord>): SessionBindingRecord {
return {
bindingId: "binding-1",
targetSessionKey: "agent:review:acp:session-1",
targetKind: "session",
conversation: {
channel: "demo",
accountId: "default",
conversationId: "room-1",
},
status: "active",
boundAt: 1,
...overrides,
};
}
function registerAdapter(record: SessionBindingRecord | null): {
resolveByConversation: ReturnType<typeof vi.fn>;
touch: ReturnType<typeof vi.fn>;
} {
const resolveByConversation = vi.fn<SessionBindingAdapter["resolveByConversation"]>(() => record);
const touch = vi.fn<NonNullable<SessionBindingAdapter["touch"]>>();
registerSessionBindingAdapter({
channel: record?.conversation.channel ?? "demo",
accountId: record?.conversation.accountId ?? "default",
listBySession: () => [],
resolveByConversation,
touch,
});
return { resolveByConversation, touch };
}
describe("runtime conversation binding route", () => {
beforeEach(() => {
testing.resetSessionBindingAdaptersForTests();
});
it("keeps the stable runtime-route result structurally assignable", () => {
const result: RuntimeConversationBindingRouteResult = {
bindingRecord: null,
route: createRoute(),
};
expect(result.bindingOwnerAvailable).toBeUndefined();
});
it("rewrites the route and touches only the owning channel account's binding", () => {
const binding = createBinding();
const { resolveByConversation, touch } = registerAdapter(binding);
const siblingTouches = [
{ channel: "other", accountId: "default" },
{ channel: "demo", accountId: "other" },
].map(
(scope) =>
registerAdapter(createBinding({ conversation: { ...binding.conversation, ...scope } }))
.touch,
);
const result = resolveRuntimeConversationBindingRoute({
route: createRoute(),
conversation: {
channel: "demo",
accountId: "default",
conversationId: "room-1",
},
});
expect(resolveByConversation).toHaveBeenCalledWith({
channel: "demo",
accountId: "default",
conversationId: "room-1",
});
expect(touch).toHaveBeenCalledWith("binding-1", undefined);
for (const siblingTouch of siblingTouches) {
expect(siblingTouch).not.toHaveBeenCalled();
}
expect(result.boundSessionKey).toBe("agent:review:acp:session-1");
expect(result.boundAgentId).toBe("review");
expect(result.route).toEqual({
agentId: "review",
accountId: "default",
channel: "demo",
sessionKey: "agent:review:acp:session-1",
mainSessionKey: "agent:main:main",
lastRoutePolicy: "session",
matchedBy: "binding.channel",
});
});
it("touches plugin-owned bindings without rewriting the channel route", () => {
const route = createRoute();
const binding = createBinding({
metadata: {
pluginBindingOwner: "plugin",
pluginId: "demo-plugin",
pluginRoot: "/tmp/demo-plugin",
},
});
const { touch } = registerAdapter(binding);
const result = resolveRuntimeConversationBindingRoute({
route,
conversation: {
channel: "demo",
accountId: "default",
conversationId: "room-1",
},
});
expect(touch).toHaveBeenCalledWith("binding-1", undefined);
expect(result.bindingRecord).toBe(binding);
expect(result.boundSessionKey).toBeUndefined();
expect(result.route).toBe(route);
});
it.each([
{ targetSessionKey: "global", metadata: { agentId: "review" }, agentId: "review" },
{ targetSessionKey: "global", metadata: undefined, agentId: "main" },
{
targetSessionKey: "agent:review:session-1",
metadata: { agentId: "other" },
agentId: "review",
},
])("resolves $targetSessionKey to owner $agentId", ({ targetSessionKey, metadata, agentId }) => {
const binding = createBinding({ targetSessionKey, metadata });
registerAdapter(binding);
const result = resolveRuntimeConversationBindingRoute({
route: createRoute(),
conversation: binding.conversation,
});
expect(result.route).toMatchObject({ sessionKey: targetSessionKey, agentId });
expect(result.boundAgentId).toBe(agentId);
});
it("rejects an opaque target when its plugin ownership metadata is missing", () => {
const binding = createBinding({
targetSessionKey: "plugin-thread-1",
metadata: { agentId: "review" },
});
registerAdapter(binding);
expect(() =>
resolveRuntimeConversationBindingRoute({
route: createRoute(),
conversation: binding.conversation,
}),
).toThrow();
});
it("inspects a runtime-bound route without touching the binding", () => {
const { touch } = registerAdapter(createBinding());
const result = resolveRuntimeConversationBindingRoute({
route: createRoute(),
touchBinding: false,
conversation: {
channel: "demo",
accountId: "default",
conversationId: "room-1",
},
});
expect(touch).not.toHaveBeenCalled();
expect(result.bindingOwnerAvailable).toBe(true);
expect(result.boundSessionKey).toBe("agent:review:acp:session-1");
});
it("ignores runtime bindings that target isolated cron run sessions", () => {
const route = createRoute();
const binding = createBinding({
targetSessionKey: "agent:youtube:cron:monthly-report:run:closed-run-1",
});
const { touch } = registerAdapter(binding);
const result = resolveRuntimeConversationBindingRoute({
route,
conversation: {
channel: "demo",
accountId: "default",
conversationId: "room-1",
},
});
expect(touch).not.toHaveBeenCalled();
expect(result.bindingRecord).toBeNull();
expect(result.boundSessionKey).toBeUndefined();
expect(result.route).toBe(route);
});
});
describe("ensureConfiguredBindingRouteReady", () => {
let unregisterDriver: (() => void) | undefined;
afterEach(() => {
vi.useRealTimers();
unregisterDriver?.();
});
it("returns a bounded failure when target readiness never settles", async () => {
vi.useFakeTimers();
unregisterDriver = registerStatefulBindingTargetDriver({
id: "slow",
ensureReady: async () => await new Promise<never>(() => {}),
ensureSession: async () => ({
ok: false,
sessionKey: "agent:slow:binding",
error: "not used",
}),
});
const resultPromise = ensureConfiguredBindingRouteReady({
cfg: {} as never,
bindingResolution: { statefulTarget: { driverId: "slow" } } as never,
});
await vi.advanceTimersByTimeAsync(30_000);
await expect(resultPromise).resolves.toEqual({
ok: false,
error: "Configured binding route ready check timed out",
});
});
});