Files
multica/packages/core/api/ws-client.test.ts
Bohan Jiang 8a135d2982 fix(ws): truncate unparseable frame payload in client warn log (#2974)
The post-#2946 onmessage guard logs the raw event.data alongside the
warning. A malformed or rogue server can stream arbitrarily large
garbage and bloat the renderer / desktop main-process log buffers, so
cap the logged payload to the first 200 chars and append a
"(truncated, N chars total)" suffix when truncation occurs.

MUL-2490

Co-authored-by: multica-agent <github@multica.ai>
2026-05-21 13:37:42 +08:00

131 lines
4.2 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { WSClient } from "./ws-client";
// Capture URL passed to WebSocket so we can assert the connect-time
// query string. We don't simulate the full WS lifecycle here — only the
// upgrade URL construction, which is what carries client identity.
class FakeWebSocket {
static lastUrl: string | null = null;
static lastInstance: FakeWebSocket | null = null;
// Fields read by WSClient.connect()/disconnect(), all no-op here.
onopen: (() => void) | null = null;
onmessage: ((ev: { data: string }) => void) | null = null;
onclose: (() => void) | null = null;
onerror: (() => void) | null = null;
readyState = 0;
constructor(url: string) {
FakeWebSocket.lastUrl = url;
FakeWebSocket.lastInstance = this;
}
close() {}
send() {}
}
describe("WSClient", () => {
beforeEach(() => {
FakeWebSocket.lastUrl = null;
FakeWebSocket.lastInstance = null;
vi.stubGlobal("WebSocket", FakeWebSocket as unknown as typeof WebSocket);
});
afterEach(() => {
vi.unstubAllGlobals();
});
it("includes client identity in the upgrade URL when configured", () => {
const ws = new WSClient("ws://example.test/ws", {
identity: { platform: "desktop", version: "1.2.3", os: "macos" },
});
ws.setAuth("tok", "acme");
ws.connect();
const url = new URL(FakeWebSocket.lastUrl!);
expect(url.searchParams.get("workspace_slug")).toBe("acme");
expect(url.searchParams.get("client_platform")).toBe("desktop");
expect(url.searchParams.get("client_version")).toBe("1.2.3");
expect(url.searchParams.get("client_os")).toBe("macos");
// Token must never appear in the URL — it is delivered as the first
// WS message in token mode.
expect(url.searchParams.has("token")).toBe(false);
});
it("omits client_* params when identity is not configured", () => {
const ws = new WSClient("ws://example.test/ws");
ws.setAuth("tok", "acme");
ws.connect();
const url = new URL(FakeWebSocket.lastUrl!);
expect(url.searchParams.has("client_platform")).toBe(false);
expect(url.searchParams.has("client_version")).toBe(false);
expect(url.searchParams.has("client_os")).toBe(false);
});
it("only includes the identity fields that are set", () => {
const ws = new WSClient("ws://example.test/ws", {
identity: { platform: "cli" },
});
ws.setAuth("tok", "acme");
ws.connect();
const url = new URL(FakeWebSocket.lastUrl!);
expect(url.searchParams.get("client_platform")).toBe("cli");
expect(url.searchParams.has("client_version")).toBe(false);
expect(url.searchParams.has("client_os")).toBe(false);
});
it("truncates the logged payload when an unparseable frame is large", () => {
const logger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
};
const ws = new WSClient("ws://example.test/ws", { logger });
ws.connect();
const huge = "x".repeat(5000);
FakeWebSocket.lastInstance!.onmessage?.({ data: huge });
expect(logger.warn).toHaveBeenCalledTimes(1);
const [, summary] = logger.warn.mock.calls[0] as [string, string];
expect(summary.length).toBeLessThan(huge.length);
expect(summary).toContain("truncated");
expect(summary).toContain("5000");
expect(summary.startsWith("x".repeat(200))).toBe(true);
});
it("logs and skips malformed frames without breaking later messages", () => {
const logger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
};
const ws = new WSClient("ws://example.test/ws", { logger });
const handler = vi.fn();
ws.on("issue:updated", handler);
ws.connect();
expect(() => {
FakeWebSocket.lastInstance!.onmessage?.({ data: `{"type":"issue` });
}).not.toThrow();
FakeWebSocket.lastInstance!.onmessage?.({
data: JSON.stringify({
type: "issue:updated",
payload: { id: "issue-1" },
}),
});
expect(logger.warn).toHaveBeenCalledWith(
"ws: received unparseable message",
`{"type":"issue`,
);
expect(handler).toHaveBeenCalledWith(
{ id: "issue-1" },
undefined,
undefined,
);
});
});