perf(test): trim hotspot integration paths

This commit is contained in:
Peter Steinberger
2026-04-20 20:41:01 +01:00
parent e3edd408aa
commit a6aa028626
4 changed files with 99 additions and 178 deletions

View File

@@ -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<ReturnType<typeof connectMcpWithoutGateway>> | null = null;
try {
mcp = await connectMcpWithoutGateway({
claudeChannelMode: "off",
});
const connectedMcp = mcp;
(
connectedMcp.bridge as unknown as {
gateway: { request: typeof gatewayRequest; stopAndWait: () => Promise<void> };
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<Record<string, unknown>> };
};
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<Record<string, unknown>> };
};
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<Record<string, unknown>> };
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 () => {

View File

@@ -64,6 +64,8 @@ import {
let fixtureRoot = "";
let caseId = 0;
let randomIntSpy: ReturnType<typeof vi.spyOn<typeof crypto, "randomInt">>;
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<T>(fn: (stateDir: string) => Promise<T>) {
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<void>;
}) {
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();
}
}

View File

@@ -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");

View File

@@ -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<HTMLImageElement>(
".agent-chat__welcome .agent-chat__avatar--logo img",
);
expect(container.querySelector<HTMLImageElement>(".agent-chat__welcome > img")).toBeNull();
expect(logoImage).not.toBeNull();
expect(
container
.querySelector<HTMLImageElement>(".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<HTMLElement>(".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<HTMLElement>(".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"');
});
});