From 31a87584d0fb48c2c20710d64db2533ecfbfdbd6 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 10 May 2026 10:50:41 +0100 Subject: [PATCH] test: clear app chat broad matchers --- ui/src/ui/app-chat.test.ts | 343 ++++++++++++++++++------------------- 1 file changed, 169 insertions(+), 174 deletions(-) diff --git a/ui/src/ui/app-chat.test.ts b/ui/src/ui/app-chat.test.ts index d98beb3b357..846dc65efaf 100644 --- a/ui/src/ui/app-chat.test.ts +++ b/ui/src/ui/app-chat.test.ts @@ -72,6 +72,38 @@ function requestUrl(input: string | URL | Request): string { return input.url; } +type MockCallSource = { + mock: { + calls: ArrayLike>; + }; +}; + +function requireRecord(value: unknown, label: string): Record { + expect(value, label).toBeTypeOf("object"); + expect(value, label).not.toBeNull(); + return value as Record; +} + +function mockArg(source: MockCallSource, callIndex: number, argIndex: number, label: string) { + const call = source.mock.calls[callIndex]; + if (!call) { + throw new Error(`expected mock call: ${label}`); + } + return call[argIndex]; +} + +function findRequestPayload(source: MockCallSource, method: string, label: string) { + const call = Array.from(source.mock.calls).find((candidate) => candidate[0] === method); + if (!call) { + throw new Error(`expected request call: ${label}`); + } + return requireRecord(call[1], label); +} + +function fetchInit(source: MockCallSource, callIndex: number) { + return requireRecord(mockArg(source, callIndex, 1, `fetch init ${callIndex}`), "fetch init"); +} + function makeHost(overrides?: Partial): ChatHost { const host = { client: null, @@ -191,14 +223,14 @@ describe("refreshChat", () => { maxChars: 4000, }); expect(request).toHaveBeenCalledWith("models.list", { view: "configured" }); - expect(request).toHaveBeenCalledWith( + const sessionsListPayload = findRequestPayload( + request as unknown as MockCallSource, "sessions.list", - expect.objectContaining({ - agentId: "main", - includeGlobal: true, - includeUnknown: true, - }), + "sessions list payload", ); + expect(sessionsListPayload.agentId).toBe("main"); + expect(sessionsListPayload.includeGlobal).toBe(true); + expect(sessionsListPayload.includeUnknown).toBe(true); expect(request).toHaveBeenCalledWith("commands.list", { agentId: "main", includeArgs: true, @@ -280,14 +312,10 @@ describe("refreshChatAvatar", () => { const host = makeHost({ basePath: "", sessionKey: "agent:main" }); await refreshChatAvatar(host); - expect(fetchMock).toHaveBeenCalledWith( - "/avatar/main?meta=1", - expect.objectContaining({ method: "GET" }), - ); - expect(fetchMock).toHaveBeenCalledWith( - "/avatar/main", - expect.objectContaining({ method: "GET" }), - ); + expect(fetchMock.mock.calls[0]?.[0]).toBe("/avatar/main?meta=1"); + expect(fetchInit(fetchMock as unknown as MockCallSource, 0).method).toBe("GET"); + expect(fetchMock.mock.calls[1]?.[0]).toBe("/avatar/main"); + expect(fetchInit(fetchMock as unknown as MockCallSource, 1).method).toBe("GET"); const avatarFetchInit = ( fetchMock.mock.calls as Array<[string | URL | Request, RequestInit?]> )[1]?.[1]; @@ -334,20 +362,16 @@ describe("refreshChatAvatar", () => { }); await refreshChatAvatar(host); - expect(fetchMock).toHaveBeenCalledWith( - "/openclaw/avatar/main?meta=1", - expect.objectContaining({ - method: "GET", - headers: { Authorization: "Bearer device-token" }, - }), - ); - expect(fetchMock).toHaveBeenCalledWith( - "/avatar/main", - expect.objectContaining({ - method: "GET", - headers: { Authorization: "Bearer device-token" }, - }), - ); + expect(fetchMock.mock.calls[0]?.[0]).toBe("/openclaw/avatar/main?meta=1"); + expect(fetchInit(fetchMock as unknown as MockCallSource, 0).method).toBe("GET"); + expect(fetchInit(fetchMock as unknown as MockCallSource, 0).headers).toEqual({ + Authorization: "Bearer device-token", + }); + expect(fetchMock.mock.calls[1]?.[0]).toBe("/avatar/main"); + expect(fetchInit(fetchMock as unknown as MockCallSource, 1).method).toBe("GET"); + expect(fetchInit(fetchMock as unknown as MockCallSource, 1).headers).toEqual({ + Authorization: "Bearer device-token", + }); expect(createObjectURL).toHaveBeenCalledTimes(1); expect(revokeObjectURL).not.toHaveBeenCalled(); expect(host.chatAvatarUrl).toBe("blob:device-avatar"); @@ -388,20 +412,16 @@ describe("refreshChatAvatar", () => { }); await refreshChatAvatar(host); - expect(fetchMock).toHaveBeenCalledWith( - "/openclaw/avatar/main?meta=1", - expect.objectContaining({ - method: "GET", - headers: { Authorization: "Bearer session-token" }, - }), - ); - expect(fetchMock).toHaveBeenCalledWith( - "/avatar/main", - expect.objectContaining({ - method: "GET", - headers: { Authorization: "Bearer session-token" }, - }), - ); + expect(fetchMock.mock.calls[0]?.[0]).toBe("/openclaw/avatar/main?meta=1"); + expect(fetchInit(fetchMock as unknown as MockCallSource, 0).method).toBe("GET"); + expect(fetchInit(fetchMock as unknown as MockCallSource, 0).headers).toEqual({ + Authorization: "Bearer session-token", + }); + expect(fetchMock.mock.calls[1]?.[0]).toBe("/avatar/main"); + expect(fetchInit(fetchMock as unknown as MockCallSource, 1).method).toBe("GET"); + expect(fetchInit(fetchMock as unknown as MockCallSource, 1).headers).toEqual({ + Authorization: "Bearer session-token", + }); expect(createObjectURL).toHaveBeenCalledTimes(1); expect(revokeObjectURL).not.toHaveBeenCalled(); expect(host.chatAvatarUrl).toBe("blob:session-avatar"); @@ -417,10 +437,8 @@ describe("refreshChatAvatar", () => { const host = makeHost({ basePath: "/openclaw/", sessionKey: "agent:ops:main" }); await refreshChatAvatar(host); - expect(fetchMock).toHaveBeenCalledWith( - "/openclaw/avatar/ops?meta=1", - expect.objectContaining({ method: "GET" }), - ); + expect(fetchMock.mock.calls[0]?.[0]).toBe("/openclaw/avatar/ops?meta=1"); + expect(fetchInit(fetchMock as unknown as MockCallSource, 0).method).toBe("GET"); expect(host.chatAvatarUrl).toBeNull(); }); @@ -516,21 +534,12 @@ describe("refreshChatAvatar", () => { expect(createObjectURL).toHaveBeenCalledTimes(1); expect(host.chatAvatarUrl).toBe("blob:ops-avatar"); - expect(fetchMock).toHaveBeenNthCalledWith( - 1, - "/avatar/main?meta=1", - expect.objectContaining({ method: "GET" }), - ); - expect(fetchMock).toHaveBeenNthCalledWith( - 2, - "/avatar/ops?meta=1", - expect.objectContaining({ method: "GET" }), - ); - expect(fetchMock).toHaveBeenNthCalledWith( - 3, - "/avatar/ops", - expect.objectContaining({ method: "GET" }), - ); + expect(fetchMock.mock.calls[0]?.[0]).toBe("/avatar/main?meta=1"); + expect(fetchInit(fetchMock as unknown as MockCallSource, 0).method).toBe("GET"); + expect(fetchMock.mock.calls[1]?.[0]).toBe("/avatar/ops?meta=1"); + expect(fetchInit(fetchMock as unknown as MockCallSource, 1).method).toBe("GET"); + expect(fetchMock.mock.calls[2]?.[0]).toBe("/avatar/ops"); + expect(fetchInit(fetchMock as unknown as MockCallSource, 2).method).toBe("GET"); }); }); @@ -562,19 +571,22 @@ describe("refreshChat", () => { expect(host.chatMessages).toEqual([ { role: "assistant", content: [{ type: "text", text: "ready" }] }, ]); - expect(request).toHaveBeenCalledWith( + const sessionsListPayload = findRequestPayload( + request as unknown as MockCallSource, "sessions.list", - expect.objectContaining({ - agentId: "main", - includeGlobal: true, - includeUnknown: true, - }), + "sessions list payload", ); + expect(sessionsListPayload.agentId).toBe("main"); + expect(sessionsListPayload.includeGlobal).toBe(true); + expect(sessionsListPayload.includeUnknown).toBe(true); expect(request).toHaveBeenCalledWith("models.list", { view: "configured" }); - expect(request).toHaveBeenCalledWith( + const commandsListPayload = findRequestPayload( + request as unknown as MockCallSource, "commands.list", - expect.objectContaining({ includeArgs: true, scope: "text" }), + "commands list payload", ); + expect(commandsListPayload.includeArgs).toBe(true); + expect(commandsListPayload.scope).toBe("text"); } finally { globalThis.fetch = previousFetch; } @@ -718,13 +730,13 @@ describe("handleSendChat", () => { await handleSendChat(host); expect(confirm).not.toHaveBeenCalled(); - expect(request).toHaveBeenCalledWith( + const payload = findRequestPayload( + request as unknown as MockCallSource, "chat.send", - expect.objectContaining({ - sessionKey: "agent:main", - message: "/reset", - }), + "chat send payload", ); + expect(payload.sessionKey).toBe("agent:main"); + expect(payload.message).toBe("/reset"); expect(host.chatMessage).toBe(""); }); @@ -751,13 +763,13 @@ describe("handleSendChat", () => { switchUpdate.resolve(true); await send; - expect(request).toHaveBeenCalledWith( + const payload = findRequestPayload( + request as unknown as MockCallSource, "chat.send", - expect.objectContaining({ - sessionKey: "agent:main", - message: "use the newly selected model", - }), + "chat send payload", ); + expect(payload.sessionKey).toBe("agent:main"); + expect(payload.message).toBe("use the newly selected model"); expect(host.chatMessage).toBe(""); }); @@ -782,13 +794,13 @@ describe("handleSendChat", () => { switchUpdate.resolve(true); await send; - expect(request).toHaveBeenCalledWith( + const payload = findRequestPayload( + request as unknown as MockCallSource, "chat.send", - expect.objectContaining({ - sessionKey: "agent:main", - message: "send this", - }), + "chat send payload", ); + expect(payload.sessionKey).toBe("agent:main"); + expect(payload.message).toBe("send this"); expect(host.chatMessage).toBe("keep typing"); }); @@ -825,20 +837,18 @@ describe("handleSendChat", () => { switchUpdate.resolve(true); await send; - expect(request).toHaveBeenCalledWith( + const payload = findRequestPayload( + request as unknown as MockCallSource, "chat.send", - expect.objectContaining({ - attachments: [ - expect.objectContaining({ - content: "JVBERi0xLjQK", - fileName: "brief.pdf", - mimeType: "application/pdf", - type: "file", - }), - ], - message: "send this", - }), + "chat send payload", ); + expect(payload.message).toBe("send this"); + const attachments = payload.attachments as Array>; + expect(attachments).toHaveLength(1); + expect(attachments[0]?.content).toBe("JVBERi0xLjQK"); + expect(attachments[0]?.fileName).toBe("brief.pdf"); + expect(attachments[0]?.mimeType).toBe("application/pdf"); + expect(attachments[0]?.type).toBe("file"); expect(host.chatMessage).toBe("keep typing with the attachment"); expect(host.chatAttachments).toEqual([attachment]); expect(getChatAttachmentDataUrl(attachment)).toBe("data:application/pdf;base64,JVBERi0xLjQK"); @@ -888,20 +898,18 @@ describe("handleSendChat", () => { switchUpdate.resolve(true); await send; - expect(request).toHaveBeenCalledWith( + const payload = findRequestPayload( + request as unknown as MockCallSource, "chat.send", - expect.objectContaining({ - attachments: [ - expect.objectContaining({ - content: "b3JpZ2luYWw=", - fileName: "original.pdf", - mimeType: "application/pdf", - type: "file", - }), - ], - message: "send this", - }), + "chat send payload", ); + expect(payload.message).toBe("send this"); + const attachments = payload.attachments as Array>; + expect(attachments).toHaveLength(1); + expect(attachments[0]?.content).toBe("b3JpZ2luYWw="); + expect(attachments[0]?.fileName).toBe("original.pdf"); + expect(attachments[0]?.mimeType).toBe("application/pdf"); + expect(attachments[0]?.type).toBe("file"); expect(host.chatMessage).toBe("send this"); expect(host.chatAttachments).toEqual([editedAttachment]); expect(getChatAttachmentDataUrl(originalAttachment)).toBeNull(); @@ -942,20 +950,18 @@ describe("handleSendChat", () => { switchUpdate.resolve(true); await send; - expect(request).toHaveBeenCalledWith( + const payload = findRequestPayload( + request as unknown as MockCallSource, "chat.send", - expect.objectContaining({ - attachments: [ - expect.objectContaining({ - content: "b3JpZ2luYWw=", - fileName: "original.pdf", - mimeType: "application/pdf", - type: "file", - }), - ], - message: "send this", - }), + "chat send payload", ); + expect(payload.message).toBe("send this"); + const attachments = payload.attachments as Array>; + expect(attachments).toHaveLength(1); + expect(attachments[0]?.content).toBe("b3JpZ2luYWw="); + expect(attachments[0]?.fileName).toBe("original.pdf"); + expect(attachments[0]?.mimeType).toBe("application/pdf"); + expect(attachments[0]?.type).toBe("file"); expect(host.chatMessage).toBe("send this"); expect(host.chatAttachments).toStrictEqual([]); expect(getChatAttachmentDataUrl(attachment)).toBeNull(); @@ -978,13 +984,13 @@ describe("handleSendChat", () => { await handleSendChat(host); - expect(request).toHaveBeenCalledWith( + const payload = findRequestPayload( + request as unknown as MockCallSource, "chat.send", - expect.objectContaining({ - sessionKey: "agent:other", - message: "send in other session", - }), + "chat send payload", ); + expect(payload.sessionKey).toBe("agent:other"); + expect(payload.message).toBe("send in other session"); otherSessionSwitch.resolve(false); }); @@ -1077,12 +1083,12 @@ describe("handleSendChat", () => { await handleSendChat(host); expect(host.chatMessage).toBe(""); - expect(host.chatMessages).toEqual([ - expect.objectContaining({ - role: "system", - content: "Cannot run `/think`: Control UI is not connected to the Gateway.", - }), - ]); + expect(host.chatMessages).toHaveLength(1); + const feedback = requireRecord(host.chatMessages[0], "feedback message"); + expect(feedback.role).toBe("system"); + expect(feedback.content).toBe( + "Cannot run `/think`: Control UI is not connected to the Gateway.", + ); }); it("shows local slash-command feedback when dispatch fails unexpectedly", async () => { @@ -1101,12 +1107,10 @@ describe("handleSendChat", () => { expect(executeSlashCommandMock).toHaveBeenCalledTimes(1); expect(host.chatMessage).toBe(""); expect(host.lastError).toBe("Error: dispatch failed"); - expect(host.chatMessages).toEqual([ - expect.objectContaining({ - role: "system", - content: "Command `/think` failed unexpectedly.", - }), - ]); + expect(host.chatMessages).toHaveLength(1); + const feedback = requireRecord(host.chatMessages[0], "feedback message"); + expect(feedback.role).toBe("system"); + expect(feedback.content).toBe("Command `/think` failed unexpectedly."); }); it("sends /btw immediately while a main run is active without queueing it", async () => { @@ -1125,15 +1129,15 @@ describe("handleSendChat", () => { await handleSendChat(host); - expect(request).toHaveBeenCalledWith( + const payload = findRequestPayload( + request as unknown as MockCallSource, "chat.send", - expect.objectContaining({ - sessionKey: "agent:main", - message: "/btw what changed?", - deliver: false, - idempotencyKey: expect.stringMatching(uuidPattern), - }), + "chat send payload", ); + expect(payload.sessionKey).toBe("agent:main"); + expect(payload.message).toBe("/btw what changed?"); + expect(payload.deliver).toBe(false); + expect(payload.idempotencyKey).toEqual(expect.stringMatching(uuidPattern)); expect(host.chatQueue).toStrictEqual([]); expect(host.chatRunId).toBe("run-main"); expect(host.chatStream).toBe("Working..."); @@ -1159,13 +1163,13 @@ describe("handleSendChat", () => { await handleSendChat(host); - expect(request).toHaveBeenCalledWith( + const payload = findRequestPayload( + request as unknown as MockCallSource, "chat.send", - expect.objectContaining({ - message: "/side what changed?", - deliver: false, - }), + "chat send payload", ); + expect(payload.message).toBe("/side what changed?"); + expect(payload.deliver).toBe(false); expect(host.chatQueue).toStrictEqual([]); expect(host.chatRunId).toBe("run-main"); }); @@ -1184,13 +1188,13 @@ describe("handleSendChat", () => { await handleSendChat(host); - expect(request).toHaveBeenCalledWith( + const payload = findRequestPayload( + request as unknown as MockCallSource, "chat.send", - expect.objectContaining({ - message: "/btw summarize this", - deliver: false, - }), + "chat send payload", ); + expect(payload.message).toBe("/btw summarize this"); + expect(payload.deliver).toBe(false); expect(host.chatRunId).toBeNull(); expect(host.chatMessages).toStrictEqual([]); expect(host.chatMessage).toBe(""); @@ -1318,13 +1322,10 @@ describe("handleSendChat", () => { await handleSendChat(host); - expect(host.chatQueue).toEqual([ - expect.objectContaining({ - text: "/steer tighten the plan", - kind: "steered", - pendingRunId: "run-1", - }), - ]); + expect(host.chatQueue).toHaveLength(1); + expect(host.chatQueue[0]?.text).toBe("/steer tighten the plan"); + expect(host.chatQueue[0]?.kind).toBe("steered"); + expect(host.chatQueue[0]?.pendingRunId).toBe("run-1"); }); it("steers a queued message into the active run without replacing run tracking", async () => { @@ -1353,13 +1354,10 @@ describe("handleSendChat", () => { }); expect(host.chatRunId).toBe("run-1"); expect(host.chatStream).toBe("Working..."); - expect(host.chatQueue).toEqual([ - expect.objectContaining({ - text: "tighten the plan", - kind: "steered", - pendingRunId: "run-1", - }), - ]); + expect(host.chatQueue).toHaveLength(1); + expect(host.chatQueue[0]?.text).toBe("tighten the plan"); + expect(host.chatQueue[0]?.kind).toBe("steered"); + expect(host.chatQueue[0]?.pendingRunId).toBe("run-1"); }); it("removes pending steer indicators when the run finishes", () => { @@ -1381,12 +1379,9 @@ describe("handleSendChat", () => { clearPendingQueueItemsForRun(host, "run-1"); - expect(host.chatQueue).toEqual([ - expect.objectContaining({ - id: "queued", - text: "follow up", - }), - ]); + expect(host.chatQueue).toHaveLength(1); + expect(host.chatQueue[0]?.id).toBe("queued"); + expect(host.chatQueue[0]?.text).toBe("follow up"); }); it("drops sent attachment payload bytes while keeping the optimistic preview URL", async () => {