diff --git a/CHANGELOG.md b/CHANGELOG.md index 40f69d69545..48aa146b799 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Signal/RPC: guard malformed Signal RPC JSON responses with a clear status-scoped error and add regression coverage for invalid JSON responses. (#22995) Thanks @adhitShet. - Gateway/Subagents: guard gateway and subagent session-key/message trim paths against undefined inputs to prevent early `Cannot read properties of undefined (reading 'trim')` crashes during subagent spawn and wait flows. - Agents/Workspace: guard `resolveUserPath` against undefined/null input to prevent `Cannot read properties of undefined (reading 'trim')` crashes when workspace paths are missing in embedded runner flows. - Auth/Profiles: keep active `cooldownUntil`/`disabledUntil` windows immutable across retries so mid-window failures cannot extend recovery indefinitely; only recompute a backoff window after the previous deadline has expired. This resolves cron/inbound retry loops that could trap gateways until manual `usageStats` cleanup. (#23516, #23536) Thanks @arosstale. diff --git a/src/signal/client.test.ts b/src/signal/client.test.ts new file mode 100644 index 00000000000..939784428ed --- /dev/null +++ b/src/signal/client.test.ts @@ -0,0 +1,56 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const fetchWithTimeoutMock = vi.fn(); +const resolveFetchMock = vi.fn(); + +vi.mock("../infra/fetch.js", () => ({ + resolveFetch: (...args: unknown[]) => resolveFetchMock(...args), +})); + +vi.mock("../infra/secure-random.js", () => ({ + generateSecureUuid: () => "test-id", +})); + +vi.mock("../utils/fetch-timeout.js", () => ({ + fetchWithTimeout: (...args: unknown[]) => fetchWithTimeoutMock(...args), +})); + +import { signalRpcRequest } from "./client.js"; + +type ErrorWithCause = Error & { cause?: unknown }; + +describe("signalRpcRequest", () => { + beforeEach(() => { + vi.clearAllMocks(); + resolveFetchMock.mockReturnValue(vi.fn()); + }); + + it("returns parsed RPC result", async () => { + fetchWithTimeoutMock.mockResolvedValueOnce( + new Response( + JSON.stringify({ jsonrpc: "2.0", result: { version: "0.13.22" }, id: "test-id" }), + { + status: 200, + }, + ), + ); + + const result = await signalRpcRequest<{ version: string }>("version", undefined, { + baseUrl: "http://127.0.0.1:8080", + }); + + expect(result).toEqual({ version: "0.13.22" }); + }); + + it("throws a wrapped error when RPC response JSON is malformed", async () => { + fetchWithTimeoutMock.mockResolvedValueOnce(new Response("not-json", { status: 502 })); + + const err = (await signalRpcRequest("version", undefined, { + baseUrl: "http://127.0.0.1:8080", + }).catch((error: unknown) => error)) as ErrorWithCause; + + expect(err).toBeInstanceOf(Error); + expect(err.message).toBe("Signal RPC returned malformed JSON (status 502)"); + expect(err.cause).toBeInstanceOf(SyntaxError); + }); +});