diff --git a/packages/chat/src/chat.test.ts b/packages/chat/src/chat.test.ts index ce69e522..b395ab05 100644 --- a/packages/chat/src/chat.test.ts +++ b/packages/chat/src/chat.test.ts @@ -49,6 +49,77 @@ describe("Chat", () => { expect(mockState.connect).toHaveBeenCalled(); }); + it("should disconnect adapters during shutdown", async () => { + await chat.shutdown(); + + if (!mockAdapter.disconnect) { + throw new Error("Expected mock adapter disconnect to be defined"); + } + + expect(mockAdapter.disconnect).toHaveBeenCalledTimes(1); + expect(mockState.disconnect).toHaveBeenCalledTimes(1); + }); + + it("should disconnect adapter before state adapter during shutdown", async () => { + await chat.shutdown(); + + if (!mockAdapter.disconnect) { + throw new Error("Expected mock adapter disconnect to be defined"); + } + + const adapterDisconnectCall = + (mockAdapter.disconnect as ReturnType).mock + .invocationCallOrder[0]; + const stateDisconnectCall = (mockState.disconnect as ReturnType) + .mock.invocationCallOrder[0]; + expect(adapterDisconnectCall).toBeLessThan(stateDisconnectCall); + }); + + it("should allow adapters without disconnect during shutdown", async () => { + const adapterWithoutDisconnect: Adapter = { + ...createMockAdapter("slack"), + disconnect: undefined, + }; + const state = createMockState(); + const localChat = new Chat({ + userName: "testbot", + adapters: { slack: adapterWithoutDisconnect }, + state, + logger: mockLogger, + }); + + await localChat.webhooks.slack( + new Request("http://test.com", { method: "POST" }) + ); + await expect(localChat.shutdown()).resolves.toBeUndefined(); + expect(state.disconnect).toHaveBeenCalledTimes(1); + }); + + it("should disconnect all adapters during shutdown", async () => { + const slackAdapter = createMockAdapter("slack"); + const discordAdapter = createMockAdapter("discord"); + const state = createMockState(); + const multiAdapterChat = new Chat({ + userName: "testbot", + adapters: { slack: slackAdapter, discord: discordAdapter }, + state, + logger: mockLogger, + }); + + await multiAdapterChat.webhooks.slack( + new Request("http://test.com", { method: "POST" }) + ); + await multiAdapterChat.shutdown(); + + if (!slackAdapter.disconnect || !discordAdapter.disconnect) { + throw new Error("Expected mock adapter disconnect to be defined"); + } + + expect(slackAdapter.disconnect).toHaveBeenCalledTimes(1); + expect(discordAdapter.disconnect).toHaveBeenCalledTimes(1); + expect(state.disconnect).toHaveBeenCalledTimes(1); + }); + it("should register webhook handlers", () => { expect(chat.webhooks.slack).toBeDefined(); expect(typeof chat.webhooks.slack).toBe("function"); diff --git a/packages/chat/src/chat.ts b/packages/chat/src/chat.ts index 2b8d50ea..9c899b75 100644 --- a/packages/chat/src/chat.ts +++ b/packages/chat/src/chat.ts @@ -319,6 +319,17 @@ export class Chat< */ async shutdown(): Promise { this.logger.info("Shutting down chat instance..."); + const shutdownPromises = Array.from(this.adapters.values()).map( + async (adapter) => { + if (!adapter.disconnect) { + return; + } + this.logger.debug("Disconnecting adapter", adapter.name); + await adapter.disconnect(); + this.logger.debug("Adapter disconnected", adapter.name); + } + ); + await Promise.all(shutdownPromises); await this._stateAdapter.disconnect(); this.initialized = false; this.initPromise = null; diff --git a/packages/chat/src/mock-adapter.ts b/packages/chat/src/mock-adapter.ts index bf3a8d00..dece27fb 100644 --- a/packages/chat/src/mock-adapter.ts +++ b/packages/chat/src/mock-adapter.ts @@ -32,6 +32,7 @@ export function createMockAdapter(name = "slack"): Adapter { name, userName: `${name}-bot`, initialize: vi.fn().mockResolvedValue(undefined), + disconnect: vi.fn().mockResolvedValue(undefined), handleWebhook: vi.fn().mockResolvedValue(new Response("ok")), postMessage: vi .fn() diff --git a/packages/chat/src/types.ts b/packages/chat/src/types.ts index 21647219..e3af53e0 100644 --- a/packages/chat/src/types.ts +++ b/packages/chat/src/types.ts @@ -229,6 +229,9 @@ export interface Adapter { /** Called when Chat instance is created (internal use) */ initialize(chat: ChatInstance): Promise; + /** Cleanup hook called when Chat instance is shutdown */ + disconnect?(): Promise; + /** * Check if a thread is a direct message conversation. *