From 1406b28469a3418a79ab57f7fbc1b3b7b0b87215 Mon Sep 17 00:00:00 2001 From: cpojer Date: Tue, 17 Feb 2026 10:50:22 +0900 Subject: [PATCH] chore: Fix types in tests 3/N. --- ...r.agent.gateway-server-agent-b.e2e.test.ts | 15 ++-- ...erver.chat.gateway-server-chat.e2e.test.ts | 37 +++++---- src/gateway/server.health.e2e.test.ts | 78 +++++++++++++------ src/gateway/test-helpers.server.ts | 19 ++++- src/slack/monitor.test-helpers.ts | 30 +++++-- src/slack/monitor.tool-result.test.ts | 41 ++++++---- 6 files changed, 145 insertions(+), 75 deletions(-) diff --git a/src/gateway/server.agent.gateway-server-agent-b.e2e.test.ts b/src/gateway/server.agent.gateway-server-agent-b.e2e.test.ts index 2457f290aa9..08c999a8eb3 100644 --- a/src/gateway/server.agent.gateway-server-agent-b.e2e.test.ts +++ b/src/gateway/server.agent.gateway-server-agent-b.e2e.test.ts @@ -73,7 +73,8 @@ function expectChannels(call: Record, channel: string) { } function readAgentCommandCall(fromEnd = 1) { - return vi.mocked(agentCommand).mock.calls.at(-fromEnd)?.[0] as Record; + const calls = vi.mocked(agentCommand).mock.calls as unknown[][]; + return (calls.at(-fromEnd)?.[0] ?? {}) as Record; } function expectAgentRoutingCall(params: { @@ -272,7 +273,8 @@ describe("gateway server agent", () => { test("agent routes bare /new through session reset before running greeting prompt", async () => { await writeMainSessionEntry({ sessionId: "sess-main-before-reset" }); const spy = vi.mocked(agentCommand); - const callsBefore = spy.mock.calls.length; + const calls = spy.mock.calls as unknown[][]; + const callsBefore = calls.length; const res = await rpcReq(ws, "agent", { message: "/new", sessionKey: "main", @@ -280,8 +282,8 @@ describe("gateway server agent", () => { }); expect(res.ok).toBe(true); - await vi.waitFor(() => expect(spy.mock.calls.length).toBeGreaterThan(callsBefore)); - const call = spy.mock.calls.at(-1)?.[0] as Record; + await vi.waitFor(() => expect(calls.length).toBeGreaterThan(callsBefore)); + const call = (calls.at(-1)?.[0] ?? {}) as Record; expect(call.message).toBe(BARE_SESSION_RESET_PROMPT); expect(typeof call.sessionId).toBe("string"); expect(call.sessionId).not.toBe("sess-main-before-reset"); @@ -399,10 +401,7 @@ describe("gateway server agent", () => { }); const evt = await finalChatP; - const payload = - evt.payload && typeof evt.payload === "object" - ? (evt.payload as Record) - : {}; + const payload = evt.payload && typeof evt.payload === "object" ? evt.payload : {}; expect(payload.sessionKey).toBe("main"); expect(payload.runId).toBe("run-auto-1"); diff --git a/src/gateway/server.chat.gateway-server-chat.e2e.test.ts b/src/gateway/server.chat.gateway-server-chat.e2e.test.ts index c3492d1cb15..118aaf366b3 100644 --- a/src/gateway/server.chat.gateway-server-chat.e2e.test.ts +++ b/src/gateway/server.chat.gateway-server-chat.e2e.test.ts @@ -52,7 +52,8 @@ describe("gateway server chat", () => { const spy = vi.mocked(getReplyFromConfig); spy.mockClear(); - const callsBeforeSanitized = spy.mock.calls.length; + const spyCalls = spy.mock.calls as unknown[][]; + const callsBeforeSanitized = spyCalls.length; const sanitizedRes = await rpcReq(ws, "chat.send", { sessionKey: "main", message: "Cafe\u0301\u0007\tline", @@ -60,8 +61,8 @@ describe("gateway server chat", () => { }); expect(sanitizedRes.ok).toBe(true); - await waitFor(() => spy.mock.calls.length > callsBeforeSanitized); - const ctx = spy.mock.calls.at(-1)?.[0] as + await waitFor(() => spyCalls.length > callsBeforeSanitized); + const ctx = spyCalls.at(-1)?.[0] as | { Body?: string; RawBody?: string; BodyForCommands?: string } | undefined; expect(ctx?.Body).toBe("Café\tline"); @@ -99,8 +100,9 @@ describe("gateway server chat", () => { const spy = vi.mocked(getReplyFromConfig); spy.mockClear(); + const spyCalls = spy.mock.calls as unknown[][]; testState.agentConfig = { timeoutSeconds: 123 }; - const callsBeforeTimeout = spy.mock.calls.length; + const callsBeforeTimeout = spyCalls.length; const timeoutRes = await rpcReq(ws, "chat.send", { sessionKey: "main", message: "hello", @@ -108,13 +110,13 @@ describe("gateway server chat", () => { }); expect(timeoutRes.ok).toBe(true); - await waitFor(() => spy.mock.calls.length > callsBeforeTimeout); - const timeoutCall = spy.mock.calls.at(-1)?.[1] as { runId?: string } | undefined; + await waitFor(() => spyCalls.length > callsBeforeTimeout); + const timeoutCall = spyCalls.at(-1)?.[1] as { runId?: string } | undefined; expect(timeoutCall?.runId).toBe("idem-timeout-1"); testState.agentConfig = undefined; spy.mockClear(); - const callsBeforeSession = spy.mock.calls.length; + const callsBeforeSession = spyCalls.length; const sessionRes = await rpcReq(ws, "chat.send", { sessionKey: "agent:main:subagent:abc", message: "hello", @@ -122,8 +124,8 @@ describe("gateway server chat", () => { }); expect(sessionRes.ok).toBe(true); - await waitFor(() => spy.mock.calls.length > callsBeforeSession); - const sessionCall = spy.mock.calls.at(-1)?.[0] as { SessionKey?: string } | undefined; + await waitFor(() => spyCalls.length > callsBeforeSession); + const sessionCall = spyCalls.at(-1)?.[0] as { SessionKey?: string } | undefined; expect(sessionCall?.SessionKey).toBe("agent:main:subagent:abc"); const sendPolicyDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); @@ -198,7 +200,7 @@ describe("gateway server chat", () => { testState.sessionConfig = undefined; spy.mockClear(); - const callsBeforeImage = spy.mock.calls.length; + const callsBeforeImage = spyCalls.length; const pngB64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/woAAn8B9FD5fHAAAAAASUVORK5CYII="; @@ -228,13 +230,13 @@ describe("gateway server chat", () => { expect(imgRes.ok).toBe(true); expect(imgRes.payload?.runId).toBeDefined(); - await waitFor(() => spy.mock.calls.length > callsBeforeImage, 8000); - const imgOpts = spy.mock.calls.at(-1)?.[1] as + await waitFor(() => spyCalls.length > callsBeforeImage, 8000); + const imgOpts = spyCalls.at(-1)?.[1] as | { images?: Array<{ type: string; data: string; mimeType: string }> } | undefined; expect(imgOpts?.images).toEqual([{ type: "image", data: pngB64, mimeType: "image/png" }]); - const callsBeforeImageOnly = spy.mock.calls.length; + const callsBeforeImageOnly = spyCalls.length; const reqIdOnly = "chat-img-only"; ws.send( JSON.stringify({ @@ -261,8 +263,8 @@ describe("gateway server chat", () => { expect(imgOnlyRes.ok).toBe(true); expect(imgOnlyRes.payload?.runId).toBeDefined(); - await waitFor(() => spy.mock.calls.length > callsBeforeImageOnly, 8000); - const imgOnlyOpts = spy.mock.calls.at(-1)?.[1] as + await waitFor(() => spyCalls.length > callsBeforeImageOnly, 8000); + const imgOnlyOpts = spyCalls.at(-1)?.[1] as | { images?: Array<{ type: string; data: string; mimeType: string }> } | undefined; expect(imgOnlyOpts?.images).toEqual([{ type: "image", data: pngB64, mimeType: "image/png" }]); @@ -410,10 +412,7 @@ describe("gateway server chat", () => { }); const evt = await agentEvtP; - const payload = - evt.payload && typeof evt.payload === "object" - ? (evt.payload as Record) - : {}; + const payload = evt.payload && typeof evt.payload === "object" ? evt.payload : {}; expect(payload.sessionKey).toBe("main"); expect(payload.stream).toBe("assistant"); } diff --git a/src/gateway/server.health.e2e.test.ts b/src/gateway/server.health.e2e.test.ts index f42e7b78b3e..e4c54aa3256 100644 --- a/src/gateway/server.health.e2e.test.ts +++ b/src/gateway/server.health.e2e.test.ts @@ -10,6 +10,16 @@ installGatewayTestHooks({ scope: "suite" }); let harness: GatewayServerHarness; +type GatewayFrame = { + type?: string; + id?: string; + ok?: boolean; + event?: string; + payload?: Record | null; + seq?: number; + stateVersion?: { presence?: number; [key: string]: unknown }; +}; + beforeAll(async () => { harness = await startGatewayServerHarness(); }); @@ -22,10 +32,16 @@ describe("gateway server health/presence", () => { test("connect + health + presence + status succeed", { timeout: 60_000 }, async () => { const { ws } = await harness.openClient(); - const healthP = onceMessage(ws, (o) => o.type === "res" && o.id === "health1"); - const statusP = onceMessage(ws, (o) => o.type === "res" && o.id === "status1"); - const presenceP = onceMessage(ws, (o) => o.type === "res" && o.id === "presence1"); - const channelsP = onceMessage(ws, (o) => o.type === "res" && o.id === "channels1"); + const healthP = onceMessage(ws, (o) => o.type === "res" && o.id === "health1"); + const statusP = onceMessage(ws, (o) => o.type === "res" && o.id === "status1"); + const presenceP = onceMessage( + ws, + (o) => o.type === "res" && o.id === "presence1", + ); + const channelsP = onceMessage( + ws, + (o) => o.type === "res" && o.id === "channels1", + ); const sendReq = (id: string, method: string) => ws.send(JSON.stringify({ type: "req", id, method })); @@ -62,12 +78,6 @@ describe("gateway server health/presence", () => { event: string; payload?: HeartbeatPayload | null; }; - type ResFrame = { - type: "res"; - id: string; - ok: boolean; - payload?: unknown; - }; const { ws } = await harness.openClient(); @@ -87,7 +97,7 @@ describe("gateway server health/presence", () => { method: "last-heartbeat", }), ); - const last = await onceMessage(ws, (o) => o.type === "res" && o.id === "hb-last"); + const last = await onceMessage(ws, (o) => o.type === "res" && o.id === "hb-last"); expect(last.ok).toBe(true); const lastPayload = last.payload as HeartbeatPayload | null | undefined; expect(lastPayload?.status).toBe("sent"); @@ -101,7 +111,7 @@ describe("gateway server health/presence", () => { params: { enabled: false }, }), ); - const toggle = await onceMessage( + const toggle = await onceMessage( ws, (o) => o.type === "res" && o.id === "hb-toggle-off", ); @@ -114,7 +124,10 @@ describe("gateway server health/presence", () => { test("presence events carry seq + stateVersion", { timeout: 8000 }, async () => { const { ws } = await harness.openClient(); - const presenceEventP = onceMessage(ws, (o) => o.type === "event" && o.event === "presence"); + const presenceEventP = onceMessage( + ws, + (o) => o.type === "event" && o.event === "presence", + ); ws.send( JSON.stringify({ type: "req", @@ -127,7 +140,8 @@ describe("gateway server health/presence", () => { const evt = await presenceEventP; expect(typeof evt.seq).toBe("number"); expect(evt.stateVersion?.presence).toBeGreaterThan(0); - expect(Array.isArray(evt.payload?.presence)).toBe(true); + const evtPayload = evt.payload as { presence?: unknown } | undefined; + expect(Array.isArray(evtPayload?.presence)).toBe(true); ws.close(); }); @@ -136,7 +150,7 @@ describe("gateway server health/presence", () => { const { ws } = await harness.openClient(); const runId = randomUUID(); - const evtPromise = onceMessage( + const evtPromise = onceMessage( ws, (o) => o.type === "event" && @@ -146,9 +160,11 @@ describe("gateway server health/presence", () => { ); emitAgentEvent({ runId, stream: "lifecycle", data: { msg: "hi" } }); const evt = await evtPromise; - expect(evt.payload.runId).toBe(runId); + const payload = evt.payload as Record | undefined; + expect(payload?.runId).toBe(runId); expect(typeof evt.seq).toBe("number"); - expect(evt.payload.data.msg).toBe("hi"); + const data = payload?.data as Record | undefined; + expect(data?.msg).toBe("hi"); ws.close(); }); @@ -156,10 +172,15 @@ describe("gateway server health/presence", () => { test("shutdown event is broadcast on close", { timeout: 8000 }, async () => { const localHarness = await startGatewayServerHarness(); const { ws } = await localHarness.openClient(); - const shutdownP = onceMessage(ws, (o) => o.type === "event" && o.event === "shutdown", 5000); + const shutdownP = onceMessage( + ws, + (o) => o.type === "event" && o.event === "shutdown", + 5000, + ); await localHarness.close(); const evt = await shutdownP; - expect(evt.payload?.reason).toBeDefined(); + const evtPayload = evt.payload as { reason?: unknown } | undefined; + expect(evtPayload?.reason).toBeDefined(); }); test("presence broadcast reaches multiple clients", { timeout: 8000 }, async () => { @@ -169,7 +190,7 @@ describe("gateway server health/presence", () => { harness.openClient(), ]); const waits = clients.map(({ ws }) => - onceMessage(ws, (o) => o.type === "event" && o.event === "presence"), + onceMessage(ws, (o) => o.type === "event" && o.event === "presence"), ); clients[0].ws.send( JSON.stringify({ @@ -181,7 +202,8 @@ describe("gateway server health/presence", () => { ); const events = await Promise.all(waits); for (const evt of events) { - expect(evt.payload?.presence?.length).toBeGreaterThan(0); + const evtPayload = evt.payload as { presence?: unknown[] } | undefined; + expect(evtPayload?.presence?.length).toBeGreaterThan(0); expect(typeof evt.seq).toBe("number"); } for (const { ws } of clients) { @@ -206,7 +228,11 @@ describe("gateway server health/presence", () => { }, }); - const presenceP = onceMessage(ws, (o) => o.type === "res" && o.id === "fingerprint", 4000); + const presenceP = onceMessage( + ws, + (o) => o.type === "res" && o.id === "fingerprint", + 4000, + ); ws.send( JSON.stringify({ type: "req", @@ -247,7 +273,11 @@ describe("gateway server health/presence", () => { }, }); - const presenceP = onceMessage(ws, (o) => o.type === "res" && o.id === "cli-presence", 4000); + const presenceP = onceMessage( + ws, + (o) => o.type === "res" && o.id === "cli-presence", + 4000, + ); ws.send( JSON.stringify({ type: "req", @@ -257,7 +287,7 @@ describe("gateway server health/presence", () => { ); const presenceRes = await presenceP; - const entries = presenceRes.payload as Array>; + const entries = (presenceRes.payload ?? []) as Array>; expect(entries.some((e) => e.instanceId === cliId)).toBe(false); ws.close(); diff --git a/src/gateway/test-helpers.server.ts b/src/gateway/test-helpers.server.ts index a5a6f926f58..37081eaa042 100644 --- a/src/gateway/test-helpers.server.ts +++ b/src/gateway/test-helpers.server.ts @@ -296,9 +296,20 @@ export async function occupyPort(): Promise<{ }); } -export function onceMessage( +type GatewayTestMessage = { + type?: string; + id?: string; + ok?: boolean; + event?: string; + payload?: Record | null; + seq?: number; + stateVersion?: Record; + [key: string]: unknown; +}; + +export function onceMessage( ws: WebSocket, - filter: (obj: unknown) => boolean, + filter: (obj: T) => boolean, // Full-suite runs can saturate the event loop (581+ files). Keep this high // enough to avoid flaky RPC timeouts, but still fail fast when a response // never arrives. @@ -312,12 +323,12 @@ export function onceMessage( reject(new Error(`closed ${code}: ${reason.toString()}`)); }; const handler = (data: WebSocket.RawData) => { - const obj = JSON.parse(rawDataToString(data)); + const obj = JSON.parse(rawDataToString(data)) as T; if (filter(obj)) { clearTimeout(timer); ws.off("message", handler); ws.off("close", closeHandler); - resolve(obj as T); + resolve(obj); } }; ws.on("message", handler); diff --git a/src/slack/monitor.test-helpers.ts b/src/slack/monitor.test-helpers.ts index e00005ea0b2..151eb587111 100644 --- a/src/slack/monitor.test-helpers.ts +++ b/src/slack/monitor.test-helpers.ts @@ -7,7 +7,7 @@ type SlackProviderMonitor = (params: { abortSignal: AbortSignal; }) => Promise; -const slackTestState: { +type SlackTestState = { config: Record; sendMock: Mock<(...args: unknown[]) => Promise>; replyMock: Mock<(...args: unknown[]) => unknown>; @@ -15,7 +15,9 @@ const slackTestState: { reactMock: Mock<(...args: unknown[]) => unknown>; readAllowFromStoreMock: Mock<(...args: unknown[]) => Promise>; upsertPairingRequestMock: Mock<(...args: unknown[]) => Promise>; -} = vi.hoisted(() => ({ +}; + +const slackTestState: SlackTestState = vi.hoisted(() => ({ config: {} as Record, sendMock: vi.fn(), replyMock: vi.fn(), @@ -25,7 +27,26 @@ const slackTestState: { upsertPairingRequestMock: vi.fn(), })); -export const getSlackTestState: () => void = () => slackTestState; +export const getSlackTestState = (): SlackTestState => slackTestState; + +type SlackClient = { + auth: { test: Mock<(...args: unknown[]) => Promise>> }; + conversations: { + info: Mock<(...args: unknown[]) => Promise>>; + replies: Mock<(...args: unknown[]) => Promise>>; + }; + users: { + info: Mock<(...args: unknown[]) => Promise<{ user: { profile: { display_name: string } } }>>; + }; + assistant: { + threads: { + setStatus: Mock<(...args: unknown[]) => Promise<{ ok: boolean }>>; + }; + }; + reactions: { + add: (...args: unknown[]) => unknown; + }; +}; export const getSlackHandlers = () => ( @@ -34,8 +55,7 @@ export const getSlackHandlers = () => } ).__slackHandlers; -export const getSlackClient = () => - (globalThis as { __slackClient?: Record }).__slackClient; +export const getSlackClient = () => (globalThis as { __slackClient?: SlackClient }).__slackClient; export const flush = () => new Promise((resolve) => setTimeout(resolve, 0)); diff --git a/src/slack/monitor.tool-result.test.ts b/src/slack/monitor.tool-result.test.ts index 50535f51c33..777b9500193 100644 --- a/src/slack/monitor.tool-result.test.ts +++ b/src/slack/monitor.tool-result.test.ts @@ -63,6 +63,10 @@ describe("monitorSlackProvider tool results", () => { }; } + function firstReplyCtx(): { WasMentioned?: boolean } { + return (replyMock.mock.calls[0]?.[0] ?? {}) as { WasMentioned?: boolean }; + } + async function runDirectMessageEvent(ts: string, extraEvent: Record = {}) { await runSlackMessageOnce(monitorSlackProvider, { event: makeSlackMessageEvent({ ts, ...extraEvent }), @@ -168,7 +172,7 @@ describe("monitorSlackProvider tool results", () => { }; let capturedCtx: { Body?: string; RawBody?: string; CommandBody?: string } = {}; - replyMock.mockImplementation(async (ctx) => { + replyMock.mockImplementation(async (ctx: unknown) => { capturedCtx = ctx ?? {}; return undefined; }); @@ -221,7 +225,7 @@ describe("monitorSlackProvider tool results", () => { }; const capturedCtx: Array<{ Body?: string }> = []; - replyMock.mockImplementation(async (ctx) => { + replyMock.mockImplementation(async (ctx: unknown) => { capturedCtx.push(ctx ?? {}); return undefined; }); @@ -274,7 +278,8 @@ describe("monitorSlackProvider tool results", () => { }); it("updates assistant thread status when replies start", async () => { - replyMock.mockImplementation(async (_ctx, opts) => { + replyMock.mockImplementation(async (...args: unknown[]) => { + const opts = (args[1] ?? {}) as { onReplyStart?: () => Promise | void }; await opts?.onReplyStart?.(); return { text: "final reply" }; }); @@ -325,7 +330,7 @@ describe("monitorSlackProvider tool results", () => { }); expect(replyMock).toHaveBeenCalledTimes(1); - expect(replyMock.mock.calls[0][0].WasMentioned).toBe(true); + expect(firstReplyCtx().WasMentioned).toBe(true); } it("accepts channel messages when mentionPatterns match", async () => { @@ -358,7 +363,7 @@ describe("monitorSlackProvider tool results", () => { }); expect(replyMock).toHaveBeenCalledTimes(1); - expect(replyMock.mock.calls[0][0].WasMentioned).toBe(true); + expect(firstReplyCtx().WasMentioned).toBe(true); }); it("accepts channel messages without mention when channels.slack.requireMention is false", async () => { @@ -380,7 +385,7 @@ describe("monitorSlackProvider tool results", () => { }); expect(replyMock).toHaveBeenCalledTimes(1); - expect(replyMock.mock.calls[0][0].WasMentioned).toBe(false); + expect(firstReplyCtx().WasMentioned).toBe(false); expect(sendMock).toHaveBeenCalledTimes(1); }); @@ -395,7 +400,7 @@ describe("monitorSlackProvider tool results", () => { }); expect(replyMock).toHaveBeenCalledTimes(1); - expect(replyMock.mock.calls[0][0].WasMentioned).toBe(true); + expect(firstReplyCtx().WasMentioned).toBe(true); }); it("threads replies when incoming message is in a thread", async () => { @@ -478,12 +483,15 @@ describe("monitorSlackProvider tool results", () => { }); it("replies with pairing code when dmPolicy is pairing and no allowFrom is set", async () => { + const currentConfig = slackTestState.config as { + channels?: { slack?: Record }; + }; slackTestState.config = { - ...slackTestState.config, + ...currentConfig, channels: { - ...slackTestState.config.channels, + ...currentConfig.channels, slack: { - ...slackTestState.config.channels?.slack, + ...currentConfig.channels?.slack, dm: { enabled: true, policy: "pairing", allowFrom: [] }, }, }, @@ -496,17 +504,20 @@ describe("monitorSlackProvider tool results", () => { expect(replyMock).not.toHaveBeenCalled(); expect(upsertPairingRequestMock).toHaveBeenCalled(); expect(sendMock).toHaveBeenCalledTimes(1); - expect(String(sendMock.mock.calls[0]?.[1] ?? "")).toContain("Your Slack user id: U1"); - expect(String(sendMock.mock.calls[0]?.[1] ?? "")).toContain("Pairing code: PAIRCODE"); + expect(sendMock.mock.calls[0]?.[1]).toContain("Your Slack user id: U1"); + expect(sendMock.mock.calls[0]?.[1]).toContain("Pairing code: PAIRCODE"); }); it("does not resend pairing code when a request is already pending", async () => { + const currentConfig = slackTestState.config as { + channels?: { slack?: Record }; + }; slackTestState.config = { - ...slackTestState.config, + ...currentConfig, channels: { - ...slackTestState.config.channels, + ...currentConfig.channels, slack: { - ...slackTestState.config.channels?.slack, + ...currentConfig.channels?.slack, dm: { enabled: true, policy: "pairing", allowFrom: [] }, }, },