mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:20:43 +00:00
perf(test): trim hotspot integration paths
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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"');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user