// 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 { 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; touch: ReturnType; } { const resolveByConversation = vi.fn(() => record); const touch = vi.fn>(); 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(() => {}), 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", }); }); });