test: clear app chat broad matchers

This commit is contained in:
Peter Steinberger
2026-05-10 10:50:41 +01:00
parent c7a281aaad
commit 31a87584d0

View File

@@ -72,6 +72,38 @@ function requestUrl(input: string | URL | Request): string {
return input.url;
}
type MockCallSource = {
mock: {
calls: ArrayLike<ReadonlyArray<unknown>>;
};
};
function requireRecord(value: unknown, label: string): Record<string, unknown> {
expect(value, label).toBeTypeOf("object");
expect(value, label).not.toBeNull();
return value as Record<string, unknown>;
}
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>): 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<Record<string, unknown>>;
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<Record<string, unknown>>;
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<Record<string, unknown>>;
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 () => {