import { beforeEach, describe, expect, it, vi } from "vitest"; import { __testing, getSessionBindingService, isSessionBindingError, registerSessionBindingAdapter, type SessionBindingBindInput, type SessionBindingRecord, } from "./session-binding-service.js"; function createRecord(input: SessionBindingBindInput): SessionBindingRecord { const conversationId = input.placement === "child" ? "thread-created" : input.conversation.conversationId.trim() || "thread-current"; return { bindingId: `default:${conversationId}`, targetSessionKey: input.targetSessionKey, targetKind: input.targetKind, conversation: { channel: "discord", accountId: "default", conversationId, parentConversationId: input.conversation.parentConversationId?.trim() || undefined, }, status: "active", boundAt: 1, }; } describe("session binding service", () => { beforeEach(() => { __testing.resetSessionBindingAdaptersForTests(); }); it("normalizes conversation refs and infers current placement", async () => { const bind = vi.fn(async (input: SessionBindingBindInput) => createRecord(input)); registerSessionBindingAdapter({ channel: "discord", accountId: "default", bind, listBySession: () => [], resolveByConversation: () => null, }); const result = await getSessionBindingService().bind({ targetSessionKey: "agent:main:subagent:child-1", targetKind: "subagent", conversation: { channel: "Discord", accountId: "DEFAULT", conversationId: " thread-1 ", }, }); expect(result.conversation.channel).toBe("discord"); expect(result.conversation.accountId).toBe("default"); expect(bind).toHaveBeenCalledWith( expect.objectContaining({ placement: "current", conversation: expect.objectContaining({ channel: "discord", accountId: "default", conversationId: "thread-1", }), }), ); }); it("supports explicit child placement when adapter advertises it", async () => { registerSessionBindingAdapter({ channel: "discord", accountId: "default", capabilities: { placements: ["child"] }, bind: async (input) => createRecord(input), listBySession: () => [], resolveByConversation: () => null, }); const result = await getSessionBindingService().bind({ targetSessionKey: "agent:codex:acp:1", targetKind: "session", conversation: { channel: "discord", accountId: "default", conversationId: "thread-1", }, placement: "child", }); expect(result.conversation.conversationId).toBe("thread-created"); }); it("returns structured errors when adapter is unavailable", async () => { await expect( getSessionBindingService().bind({ targetSessionKey: "agent:main:subagent:child-1", targetKind: "subagent", conversation: { channel: "discord", accountId: "default", conversationId: "thread-1", }, }), ).rejects.toMatchObject({ code: "BINDING_ADAPTER_UNAVAILABLE", }); }); it("returns structured errors for unsupported placement", async () => { registerSessionBindingAdapter({ channel: "discord", accountId: "default", capabilities: { placements: ["current"] }, bind: async (input) => createRecord(input), listBySession: () => [], resolveByConversation: () => null, }); const rejected = await getSessionBindingService() .bind({ targetSessionKey: "agent:codex:acp:1", targetKind: "session", conversation: { channel: "discord", accountId: "default", conversationId: "thread-1", }, placement: "child", }) .catch((error) => error); expect(isSessionBindingError(rejected)).toBe(true); expect(rejected).toMatchObject({ code: "BINDING_CAPABILITY_UNSUPPORTED", details: { placement: "child", }, }); }); it("returns structured errors when adapter bind fails", async () => { registerSessionBindingAdapter({ channel: "discord", accountId: "default", bind: async () => null, listBySession: () => [], resolveByConversation: () => null, }); await expect( getSessionBindingService().bind({ targetSessionKey: "agent:main:subagent:child-1", targetKind: "subagent", conversation: { channel: "discord", accountId: "default", conversationId: "thread-1", }, }), ).rejects.toMatchObject({ code: "BINDING_CREATE_FAILED", }); }); it("reports adapter capabilities for command preflight messaging", () => { registerSessionBindingAdapter({ channel: "discord", accountId: "default", capabilities: { placements: ["current", "child"], }, bind: async (input) => createRecord(input), listBySession: () => [], resolveByConversation: () => null, unbind: async () => [], }); const known = getSessionBindingService().getCapabilities({ channel: "discord", accountId: "default", }); const unknown = getSessionBindingService().getCapabilities({ channel: "discord", accountId: "other", }); expect(known).toEqual({ adapterAvailable: true, bindSupported: true, unbindSupported: true, placements: ["current", "child"], }); expect(unknown).toEqual({ adapterAvailable: false, bindSupported: false, unbindSupported: false, placements: [], }); }); });