diff --git a/src/mcp/channel-server.test.ts b/src/mcp/channel-server.test.ts index 472fdab61b5..5d5339edba7 100644 --- a/src/mcp/channel-server.test.ts +++ b/src/mcp/channel-server.test.ts @@ -3,6 +3,7 @@ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; import { describe, expect, test, vi } from "vitest"; import { z } from "zod"; import { createOpenClawChannelMcpServer, OpenClawChannelBridge } from "./channel-server.js"; +import { extractAttachmentsFromMessage } from "./channel-shared.js"; const ClaudeChannelNotificationSchema = z.object({ method: z.literal("notifications/claude/channel"), @@ -116,86 +117,41 @@ describe("openclaw channel mcp server", () => { } throw new Error(`unexpected gateway method ${method}`); }); - let mcp: Awaited> | null = null; - try { - mcp = await connectMcpWithoutGateway({ - claudeChannelMode: "off", - }); - const connectedMcp = mcp; - ( - connectedMcp.bridge as unknown as { - gateway: { request: typeof gatewayRequest; stopAndWait: () => Promise }; - readySettled: boolean; - resolveReady: () => void; - } - ).gateway = { - request: gatewayRequest, - stopAndWait: async () => {}, - }; - ( - connectedMcp.bridge as unknown as { - readySettled: boolean; - resolveReady: () => void; - } - ).readySettled = true; - ( - connectedMcp.bridge as unknown as { - resolveReady: () => void; - } - ).resolveReady(); + const bridge = new OpenClawChannelBridge({} as never, { + claudeChannelMode: "off", + verbose: false, + }); + attachReadyGateway(bridge, gatewayRequest); - const listed = (await connectedMcp.client.callTool({ - name: "conversations_list", - arguments: {}, - })) as { - structuredContent?: { conversations?: Array> }; - }; - expect(listed.structuredContent?.conversations).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - sessionKey, - channel: "telegram", - to: "-100123", - accountId: "acct-1", - threadId: 42, - }), - ]), - ); + await expect(bridge.listConversations()).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + sessionKey, + channel: "telegram", + to: "-100123", + accountId: "acct-1", + threadId: 42, + }), + ]), + ); - const read = (await connectedMcp.client.callTool({ - name: "messages_read", - arguments: { session_key: sessionKey, limit: 5 }, - })) as { - structuredContent?: { messages?: Array> }; - }; - expect(read.structuredContent?.messages?.[0]).toMatchObject({ - role: "assistant", - content: [{ type: "text", text: "hello from transcript" }], - }); - expect(read.structuredContent?.messages?.[1]).toMatchObject({ - __openclaw: { - id: "msg-attachment", - }, - }); - - const attachments = (await connectedMcp.client.callTool({ - name: "attachments_fetch", - arguments: { session_key: sessionKey, message_id: "msg-attachment" }, - })) as { - structuredContent?: { attachments?: Array> }; - isError?: boolean; - }; - expect(attachments.isError).not.toBe(true); - expect(attachments.structuredContent?.attachments).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - type: "image", - }), - ]), - ); - } finally { - await mcp?.close(); - } + const messages = await bridge.readMessages(sessionKey, 5); + expect(messages[0]).toMatchObject({ + role: "assistant", + content: [{ type: "text", text: "hello from transcript" }], + }); + expect(messages[1]).toMatchObject({ + __openclaw: { + id: "msg-attachment", + }, + }); + expect(extractAttachmentsFromMessage(messages[1])).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: "image", + }), + ]), + ); }); test("emits Claude channel and permission notifications", async () => { diff --git a/src/pairing/pairing-store.test.ts b/src/pairing/pairing-store.test.ts index c7a3400e860..c2eacb43010 100644 --- a/src/pairing/pairing-store.test.ts +++ b/src/pairing/pairing-store.test.ts @@ -64,6 +64,8 @@ import { let fixtureRoot = ""; let caseId = 0; +let randomIntSpy: ReturnType>; +let nextRandomInt = 0; beforeAll(async () => { fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-pairing-")); @@ -77,8 +79,24 @@ afterAll(async () => { beforeEach(() => { clearPairingAllowFromReadCacheForTest(); + nextRandomInt = 0; + randomIntSpy ??= vi.spyOn(crypto, "randomInt") as unknown as typeof randomIntSpy; + setDefaultRandomIntMock(); }); +afterAll(() => { + randomIntSpy?.mockRestore(); +}); + +function setDefaultRandomIntMock() { + randomIntSpy.mockImplementation(((minOrMax: number, max?: number) => { + const min = max === undefined ? 0 : minOrMax; + const upper = max === undefined ? minOrMax : max; + const span = Math.max(upper - min, 1); + return min + (nextRandomInt++ % span); + }) as typeof crypto.randomInt); +} + async function withTempStateDir(fn: (stateDir: string) => Promise) { const dir = path.join(fixtureRoot, `case-${caseId++}`); await fs.mkdir(dir, { recursive: true }); @@ -215,25 +233,21 @@ async function withMockRandomInt(params: { fallbackValue?: number; run: () => Promise; }) { - const spy = vi.spyOn(crypto, "randomInt") as unknown as { - mockReturnValue: (value: number) => void; - mockImplementation: (fn: () => number) => void; - mockRestore: () => void; - }; - try { if (params.initialValue !== undefined) { - spy.mockReturnValue(params.initialValue); + randomIntSpy.mockReturnValue(params.initialValue); } if (params.sequence) { let idx = 0; - spy.mockImplementation(() => params.sequence?.[idx++] ?? params.fallbackValue ?? 1); + randomIntSpy.mockImplementation( + (() => params.sequence?.[idx++] ?? params.fallbackValue ?? 1) as typeof crypto.randomInt, + ); } await params.run(); } finally { - spy.mockRestore(); + setDefaultRandomIntMock(); } } diff --git a/src/proxy-capture/runtime.test.ts b/src/proxy-capture/runtime.test.ts index 1b60b274e9d..e62d14f11e3 100644 --- a/src/proxy-capture/runtime.test.ts +++ b/src/proxy-capture/runtime.test.ts @@ -66,9 +66,7 @@ describe("debug proxy runtime", () => { }); it("captures ambient global fetch calls when debug proxy mode is enabled", async () => { - globalThis.fetch = vi.fn( - async () => new Response('{"ok":true}', { status: 200 }), - ) as typeof fetch; + globalThis.fetch = vi.fn(async () => ({ status: 200 }) as Response) as typeof fetch; const runtime = await import("./runtime.js"); runtime.initializeDebugProxyCapture("test"); @@ -77,9 +75,6 @@ describe("debug proxy runtime", () => { headers: { "content-type": "application/json" }, body: '{"input":"hello"}', }); - for (let index = 0; index < 4; index += 1) { - await Promise.resolve(); - } runtime.finalizeDebugProxyCapture(); const events = storeState.events.filter((event) => event.sessionId === "runtime-test-session"); diff --git a/ui/src/ui/views/chat.test.ts b/ui/src/ui/views/chat.test.ts index ea751050dce..245cd4c7b4d 100644 --- a/ui/src/ui/views/chat.test.ts +++ b/ui/src/ui/views/chat.test.ts @@ -175,28 +175,6 @@ describe("chat view", () => { expect(welcomeImage).not.toBeNull(); expect(welcomeImage?.getAttribute("src")).toBe("/avatar/main"); - render( - renderChat( - createProps({ - assistantName: "Assistant", - assistantAvatar: "A", - assistantAvatarUrl: null, - basePath: "/openclaw/", - }), - ), - container, - ); - const logoImage = container.querySelector( - ".agent-chat__welcome .agent-chat__avatar--logo img", - ); - expect(container.querySelector(".agent-chat__welcome > img")).toBeNull(); - expect(logoImage).not.toBeNull(); - expect( - container - .querySelector(".agent-chat__welcome .agent-chat__avatar--logo img") - ?.getAttribute("src"), - ).toBe("/openclaw/favicon.svg"); - renderAssistantMessage( container, { @@ -1627,72 +1605,50 @@ describe("chat view", () => { it("lets a tool call collapse while keeping matching tool output visible", async () => { const container = document.createElement("div"); - - const renderCase = (params: { outputInToolMessages: boolean; id: string }) => { - const props = createProps({ - autoExpandToolCalls: true, - messages: [ - { - id: `assistant-${params.id}`, - role: "assistant", - toolCallId: `call-${params.id}`, - content: [ - { - type: "toolcall", - id: `call-${params.id}`, - name: "sessions_spawn", - arguments: { mode: "session", thread: true }, - }, - ], - timestamp: Date.now(), - }, - ...(params.outputInToolMessages - ? [] - : [ - { - id: `tool-${params.id}`, - role: "tool" as const, - toolCallId: `call-${params.id}`, - toolName: "sessions_spawn", - content: JSON.stringify({ status: "error" }, null, 2), - timestamp: Date.now() + 1, - }, - ]), - ], - toolMessages: params.outputInToolMessages - ? [ - { - id: `tool-${params.id}`, - role: "tool", - toolCallId: `call-${params.id}`, - toolName: "sessions_spawn", - content: JSON.stringify({ status: "error" }, null, 2), - timestamp: Date.now() + 1, - }, - ] - : [], - }); - const rerender = () => { - render(renderChat({ ...props, onRequestUpdate: rerender }), container); - }; - rerender(); + const props = createProps({ + autoExpandToolCalls: true, + messages: [ + { + id: "assistant-tool-messages", + role: "assistant", + toolCallId: "call-tool-messages", + content: [ + { + type: "toolcall", + id: "call-tool-messages", + name: "sessions_spawn", + arguments: { mode: "session", thread: true }, + }, + ], + timestamp: Date.now(), + }, + ], + toolMessages: [ + { + id: "tool-tool-messages", + role: "tool", + toolCallId: "call-tool-messages", + toolName: "sessions_spawn", + content: JSON.stringify({ status: "error" }, null, 2), + timestamp: Date.now() + 1, + }, + ], + }); + const rerender = () => { + render(renderChat({ ...props, onRequestUpdate: rerender }), container); }; + rerender(); - for (const outputInToolMessages of [false, true]) { - renderCase({ id: outputInToolMessages ? "tool-messages" : "split", outputInToolMessages }); - expect(container.textContent).toContain("Tool input"); - expect(container.textContent).toContain('"thread": true'); - expect(container.textContent).toContain('"status": "error"'); + expect(container.textContent).toContain("Tool input"); + expect(container.textContent).toContain('"thread": true'); + expect(container.textContent).toContain('"status": "error"'); - const summaries = container.querySelectorAll(".chat-tool-msg-summary"); - if (outputInToolMessages) { - expect(summaries.length).toBeGreaterThan(1); - } - summaries[0]?.dispatchEvent(new MouseEvent("click", { bubbles: true })); - await flushTasks(); + const summaries = container.querySelectorAll(".chat-tool-msg-summary"); + expect(summaries.length).toBeGreaterThan(1); + summaries[0]?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + await flushTasks(); - expect(container.textContent).not.toContain("Tool input"); - expect(container.textContent).toContain('"status": "error"'); - } + expect(container.textContent).not.toContain("Tool input"); + expect(container.textContent).toContain('"status": "error"'); }); });