Skip to content

Commit e895848

Browse files
committed
fix(webchat): scope queued sends by session
1 parent 3d7161c commit e895848

5 files changed

Lines changed: 314 additions & 72 deletions

File tree

ui/src/ui/app-chat.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ let refreshChat: typeof import("./app-chat.ts").refreshChat;
4949
let refreshChatAvatar: typeof import("./app-chat.ts").refreshChatAvatar;
5050
let clearPendingQueueItemsForRun: typeof import("./app-chat.ts").clearPendingQueueItemsForRun;
5151
let removeQueuedMessage: typeof import("./app-chat.ts").removeQueuedMessage;
52+
let markQueuedChatSendsWaitingForReconnect: typeof import("./app-chat.ts").markQueuedChatSendsWaitingForReconnect;
5253

5354
async function loadChatHelpers(): Promise<void> {
5455
({
@@ -61,6 +62,7 @@ async function loadChatHelpers(): Promise<void> {
6162
refreshChatAvatar,
6263
clearPendingQueueItemsForRun,
6364
removeQueuedMessage,
65+
markQueuedChatSendsWaitingForReconnect,
6466
} = await import("./app-chat.ts"));
6567
}
6668

@@ -132,6 +134,7 @@ function makeHost(overrides?: Partial<ChatHost>): ChatHost {
132134
chatDraftBeforeHistory: null,
133135
chatAttachments: [],
134136
chatQueue: [],
137+
chatQueueBySession: {},
135138
chatRunId: null,
136139
chatSending: false,
137140
lastError: null,
@@ -1292,6 +1295,44 @@ describe("handleSendChat", () => {
12921295
expect(userMessage.role).toBe("user");
12931296
});
12941297

1298+
it("keeps delayed chat.send ACK effects scoped to the submitted session", async () => {
1299+
const sent = createDeferred<unknown>();
1300+
const request = vi.fn((method: string) => {
1301+
if (method === "chat.send") {
1302+
return sent.promise;
1303+
}
1304+
throw new Error(`Unexpected request: ${method}`);
1305+
});
1306+
const host = makeHost({
1307+
client: { request } as unknown as ChatHost["client"],
1308+
chatMessage: "stay with session A",
1309+
sessionKey: "agent:a",
1310+
});
1311+
1312+
const send = handleSendChat(host);
1313+
await Promise.resolve();
1314+
1315+
const queuedRunId = host.chatQueue[0]?.sendRunId;
1316+
expect(queuedRunId).toEqual(expect.any(String));
1317+
1318+
host.chatQueueBySession = { "agent:a": [...host.chatQueue] };
1319+
host.chatQueue = [];
1320+
host.sessionKey = "agent:b";
1321+
host.chatMessages = [];
1322+
host.chatRunId = null;
1323+
host.chatStream = null;
1324+
1325+
sent.resolve({ runId: queuedRunId, status: "started" });
1326+
await send;
1327+
1328+
expect(host.sessionKey).toBe("agent:b");
1329+
expect(host.chatMessages).toStrictEqual([]);
1330+
expect(host.chatRunId).toBeNull();
1331+
expect(host.chatStream).toBeNull();
1332+
expect(host.chatQueue).toStrictEqual([]);
1333+
expect(host.chatQueueBySession?.["agent:a"]).toBeUndefined();
1334+
});
1335+
12951336
it("keeps a pre-ack socket close recoverable with the same run id", async () => {
12961337
const request = vi.fn((method: string) => {
12971338
if (method === "chat.send") {
@@ -1335,6 +1376,31 @@ describe("handleSendChat", () => {
13351376
expect(host.chatQueue[0]?.sendRunId).toEqual(expect.any(String));
13361377
});
13371378

1379+
it("marks saved session queued sends waiting after a disconnect", () => {
1380+
const host = makeHost({
1381+
chatQueue: [],
1382+
chatQueueBySession: {
1383+
"agent:a": [
1384+
{
1385+
id: "pending-send-a",
1386+
text: "pending",
1387+
createdAt: 1,
1388+
sendRunId: "run-a",
1389+
sendState: "sending",
1390+
sessionKey: "agent:a",
1391+
},
1392+
],
1393+
},
1394+
});
1395+
1396+
markQueuedChatSendsWaitingForReconnect(host);
1397+
1398+
expect(host.chatQueueBySession?.["agent:a"]?.[0]).toMatchObject({
1399+
sendRunId: "run-a",
1400+
sendState: "waiting-reconnect",
1401+
});
1402+
});
1403+
13381404
it("marks validation failures visible and restores the composer", async () => {
13391405
const request = vi.fn((method: string) => {
13401406
if (method === "chat.send") {

0 commit comments

Comments
 (0)