Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions packages/chat/src/chat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof vi.fn>).mock
.invocationCallOrder[0];
const stateDisconnectCall = (mockState.disconnect as ReturnType<typeof vi.fn>)
.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");
Expand Down
11 changes: 11 additions & 0 deletions packages/chat/src/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,17 @@ export class Chat<
*/
async shutdown(): Promise<void> {
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;
Expand Down
1 change: 1 addition & 0 deletions packages/chat/src/mock-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
3 changes: 3 additions & 0 deletions packages/chat/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,9 @@ export interface Adapter<TThreadId = unknown, TRawMessage = unknown> {
/** Called when Chat instance is created (internal use) */
initialize(chat: ChatInstance): Promise<void>;

/** Cleanup hook called when Chat instance is shutdown */
disconnect?(): Promise<void>;

/**
* Check if a thread is a direct message conversation.
*
Expand Down