diff --git a/.oxlintrc.json b/.oxlintrc.json index 2143000aeea..c0d6cc8eaa3 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -59,7 +59,6 @@ "**/*test-support.ts" ], "rules": { - "typescript/no-base-to-string": "off", "typescript/no-explicit-any": "off", "typescript/no-redundant-type-constituents": "off", "typescript/unbound-method": "off", diff --git a/extensions/feishu/src/bot.broadcast.test.ts b/extensions/feishu/src/bot.broadcast.test.ts index 1eae82a61d8..27b2f201ae0 100644 --- a/extensions/feishu/src/bot.broadcast.test.ts +++ b/extensions/feishu/src/bot.broadcast.test.ts @@ -364,7 +364,10 @@ describe("broadcast dispatch", () => { }); expect(mockDispatchReplyFromConfig).toHaveBeenCalledTimes(1); - const sessionKey = String(finalizeInboundContextCalls[0]?.SessionKey ?? ""); + const sessionKey = + typeof finalizeInboundContextCalls[0]?.SessionKey === "string" + ? finalizeInboundContextCalls[0].SessionKey + : ""; expect(sessionKey).toBe("agent:susan:feishu:group:oc-broadcast-group"); }); }); diff --git a/extensions/feishu/src/reply-dispatcher.test.ts b/extensions/feishu/src/reply-dispatcher.test.ts index 94561f3f064..0487e426559 100644 --- a/extensions/feishu/src/reply-dispatcher.test.ts +++ b/extensions/feishu/src/reply-dispatcher.test.ts @@ -539,7 +539,7 @@ describe("createFeishuReplyDispatcher streaming behavior", () => { expect(streamingInstances).toHaveLength(1); const updateCalls = streamingInstances[0].update.mock.calls.map((c: unknown[]) => - String(c[0] ?? ""), + typeof c[0] === "string" ? c[0] : "", ); const reasoningUpdate = updateCalls.find((c) => c.includes("Thinking")); expect(reasoningUpdate).toContain("> 💭 **Thinking**"); diff --git a/extensions/feishu/src/tool-account-routing.test.ts b/extensions/feishu/src/tool-account-routing.test.ts index b26aac77e3b..581ba45f997 100644 --- a/extensions/feishu/src/tool-account-routing.test.ts +++ b/extensions/feishu/src/tool-account-routing.test.ts @@ -180,7 +180,7 @@ describe("feishu tool account routing", () => { const result = await tool.execute("call", { action: "search" }); expect(createFeishuClientMock).not.toHaveBeenCalled(); - expect(String(result.details.error ?? "")).toContain( + expect(typeof result.details.error === "string" ? result.details.error : "").toContain( "Resolve this command against an active gateway runtime snapshot before reading it.", ); }); diff --git a/extensions/google/oauth.test.ts b/extensions/google/oauth.test.ts index c6b6b4a259f..c0311540058 100644 --- a/extensions/google/oauth.test.ts +++ b/extensions/google/oauth.test.ts @@ -706,7 +706,9 @@ describe("loginGeminiCliOAuth", () => { expect(clientMetadata).toBeDefined(); expect(JSON.parse(clientMetadata as string)).toEqual(EXPECTED_LOAD_CODE_ASSIST_METADATA); - const body = JSON.parse(String(loadRequests[0]?.init?.body)); + const loadBody = loadRequests[0]?.init?.body; + expect(typeof loadBody).toBe("string"); + const body = JSON.parse(loadBody as string); expect(body).toEqual({ metadata: EXPECTED_LOAD_CODE_ASSIST_METADATA, }); diff --git a/extensions/matrix/src/matrix/monitor/handler.test.ts b/extensions/matrix/src/matrix/monitor/handler.test.ts index 7580165a346..d975081baa7 100644 --- a/extensions/matrix/src/matrix/monitor/handler.test.ts +++ b/extensions/matrix/src/matrix/monitor/handler.test.ts @@ -180,7 +180,7 @@ describe("matrix monitor handler pairing account scope", () => { await handler("!room:example.org", makeEvent("$event1")); await handler("!room:example.org", makeEvent("$event2")); expect(sendMessageMatrixMock).toHaveBeenCalledTimes(1); - expect(String(sendMessageMatrixMock.mock.calls[0]?.[1] ?? "")).toContain( + expect(sendMessageMatrixMock.mock.calls[0]?.[1]).toContain( "Pairing request is still pending approval.", ); diff --git a/extensions/matrix/src/matrix/sdk.test.ts b/extensions/matrix/src/matrix/sdk.test.ts index 8dca0eec27e..702a08a771f 100644 --- a/extensions/matrix/src/matrix/sdk.test.ts +++ b/extensions/matrix/src/matrix/sdk.test.ts @@ -257,14 +257,21 @@ describe("MatrixClient request hardening", () => { await expect(client.downloadContent("mxc://example.org/media")).resolves.toEqual(payload); expect(fetchMock).toHaveBeenCalledTimes(1); - const firstUrl = String((fetchMock.mock.calls as unknown[][])[0]?.[0] ?? ""); + const firstInput = (fetchMock.mock.calls as Array<[RequestInfo | URL]>)[0]?.[0]; + const firstUrl = + typeof firstInput === "string" + ? firstInput + : firstInput instanceof URL + ? firstInput.toString() + : (firstInput?.url ?? ""); expect(firstUrl).toContain("/_matrix/client/v1/media/download/example.org/media"); }); it("falls back to legacy media downloads for older homeservers", async () => { const payload = Buffer.from([5, 6, 7, 8]); const fetchMock = vi.fn(async (input: RequestInfo | URL) => { - const url = String(input); + const url = + typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; if (url.includes("/_matrix/client/v1/media/download/")) { return new Response( JSON.stringify({ @@ -287,8 +294,21 @@ describe("MatrixClient request hardening", () => { await expect(client.downloadContent("mxc://example.org/media")).resolves.toEqual(payload); expect(fetchMock).toHaveBeenCalledTimes(2); - const firstUrl = String((fetchMock.mock.calls as unknown[][])[0]?.[0] ?? ""); - const secondUrl = String((fetchMock.mock.calls as unknown[][])[1]?.[0] ?? ""); + const [firstCall, secondCall] = fetchMock.mock.calls as Array<[RequestInfo | URL]>; + const firstInput = firstCall?.[0]; + const secondInput = secondCall?.[0]; + const firstUrl = + typeof firstInput === "string" + ? firstInput + : firstInput instanceof URL + ? firstInput.toString() + : (firstInput?.url ?? ""); + const secondUrl = + typeof secondInput === "string" + ? secondInput + : secondInput instanceof URL + ? secondInput.toString() + : (secondInput?.url ?? ""); expect(firstUrl).toContain("/_matrix/client/v1/media/download/example.org/media"); expect(secondUrl).toContain("/_matrix/media/v3/download/example.org/media"); }); diff --git a/extensions/mattermost/src/mattermost/client.test.ts b/extensions/mattermost/src/mattermost/client.test.ts index 0c7bda43eac..a6aa278dd08 100644 --- a/extensions/mattermost/src/mattermost/client.test.ts +++ b/extensions/mattermost/src/mattermost/client.test.ts @@ -16,7 +16,7 @@ function createMockFetch(response?: { status?: number; body?: unknown; contentTy const calls: Array<{ url: string; init?: RequestInit }> = []; const mockFetch = vi.fn(async (url: string | URL | Request, init?: RequestInit) => { - const urlStr = typeof url === "string" ? url : url.toString(); + const urlStr = typeof url === "string" ? url : url instanceof URL ? url.toString() : url.url; calls.push({ url: urlStr, init }); return new Response(JSON.stringify(body), { status, diff --git a/extensions/mattermost/src/mattermost/reactions.test-helpers.ts b/extensions/mattermost/src/mattermost/reactions.test-helpers.ts index 2b1aef10f3c..35082dd9277 100644 --- a/extensions/mattermost/src/mattermost/reactions.test-helpers.ts +++ b/extensions/mattermost/src/mattermost/reactions.test-helpers.ts @@ -2,6 +2,10 @@ import { expect, vi } from "vitest"; import type { OpenClawConfig } from "../../runtime-api.js"; import type { MattermostFetch } from "./client.js"; +function requestUrl(url: string | URL | Request): string { + return typeof url === "string" ? url : url instanceof URL ? url.toString() : url.url; +} + export function createMattermostTestConfig(): OpenClawConfig { return { channels: { @@ -31,14 +35,15 @@ export function createMattermostReactionFetchMock(params: { const removePath = `/api/v4/users/${userId}/posts/${params.postId}/reactions/${encodeURIComponent(params.emojiName)}`; return vi.fn(async (url, init) => { - if (String(url).endsWith("/api/v4/users/me")) { + const urlText = requestUrl(url); + if (urlText.endsWith("/api/v4/users/me")) { return new Response(JSON.stringify({ id: userId }), { status: 200, headers: { "content-type": "application/json" }, }); } - if (allowAdd && String(url).endsWith("/api/v4/reactions")) { + if (allowAdd && urlText.endsWith("/api/v4/reactions")) { expect(init?.method).toBe("POST"); const requestBody = init?.body; if (typeof requestBody !== "string") { @@ -59,7 +64,7 @@ export function createMattermostReactionFetchMock(params: { ); } - if (allowRemove && String(url).endsWith(removePath)) { + if (allowRemove && urlText.endsWith(removePath)) { expect(init?.method).toBe("DELETE"); const responseBody = params.body === undefined ? null : params.body; return new Response( @@ -70,7 +75,7 @@ export function createMattermostReactionFetchMock(params: { ); } - throw new Error(`unexpected url: ${String(url)}`); + throw new Error(`unexpected url: ${urlText}`); }); } diff --git a/extensions/mattermost/src/mattermost/reactions.test.ts b/extensions/mattermost/src/mattermost/reactions.test.ts index 90c375867c3..88011bee475 100644 --- a/extensions/mattermost/src/mattermost/reactions.test.ts +++ b/extensions/mattermost/src/mattermost/reactions.test.ts @@ -97,7 +97,12 @@ describe("mattermost reactions", () => { }); const usersMeCalls = fetchMock.mock.calls.filter((call) => - String(call[0]).endsWith("/api/v4/users/me"), + (typeof call[0] === "string" + ? call[0] + : call[0] instanceof URL + ? call[0].toString() + : call[0].url + ).endsWith("/api/v4/users/me"), ); expect(addResult).toEqual({ ok: true }); expect(removeResult).toEqual({ ok: true }); diff --git a/extensions/mattermost/src/mattermost/slash-http.send-config.test.ts b/extensions/mattermost/src/mattermost/slash-http.send-config.test.ts index 49877df3fb2..9b97205d2f2 100644 --- a/extensions/mattermost/src/mattermost/slash-http.send-config.test.ts +++ b/extensions/mattermost/src/mattermost/slash-http.send-config.test.ts @@ -54,7 +54,7 @@ vi.mock("./runtime-api.js", () => { isRequestBodyLimitError: vi.fn(() => false), logTypingFailure: vi.fn(), formatInboundFromLabel: vi.fn(() => ""), - rawDataToString: vi.fn((value: unknown) => String(value ?? "")), + rawDataToString: vi.fn((value: unknown) => (typeof value === "string" ? value : "")), readRequestBodyWithLimit: mockState.readRequestBodyWithLimit, resolveThreadSessionKeys: vi.fn((params: { baseSessionKey: string }) => ({ sessionKey: params.baseSessionKey, diff --git a/extensions/memory-core/src/dreaming-phases.test.ts b/extensions/memory-core/src/dreaming-phases.test.ts index a50a41279a5..f03c009d73a 100644 --- a/extensions/memory-core/src/dreaming-phases.test.ts +++ b/extensions/memory-core/src/dreaming-phases.test.ts @@ -313,7 +313,7 @@ describe("memory-core dreaming phases", () => { } const dailyReadCount = readSpy.mock.calls.filter( - ([target]) => String(target) === dailyPath, + ([target]) => typeof target === "string" && target === dailyPath, ).length; expect(dailyReadCount).toBeLessThanOrEqual(1); await expect( @@ -461,7 +461,7 @@ describe("memory-core dreaming phases", () => { ); } finally { transcriptReadCount = readSpy.mock.calls.filter( - ([target]) => String(target) === transcriptPath, + ([target]) => typeof target === "string" && target === transcriptPath, ).length; readSpy.mockRestore(); vi.unstubAllEnvs(); diff --git a/extensions/msteams/src/attachments.graph.test.ts b/extensions/msteams/src/attachments.graph.test.ts index 977bc9a6619..b4d853bf3f4 100644 --- a/extensions/msteams/src/attachments.graph.test.ts +++ b/extensions/msteams/src/attachments.graph.test.ts @@ -263,7 +263,8 @@ describe("msteams graph attachments", () => { const seen: Array<{ url: string; auth: string }> = []; const referenceAttachment = createReferenceAttachment(); const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { - const url = String(input); + const url = + typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; const auth = new Headers(init?.headers).get("Authorization") ?? ""; seen.push({ url, auth }); diff --git a/extensions/msteams/src/attachments.test.ts b/extensions/msteams/src/attachments.test.ts index fce817c2aea..50e6dd838d5 100644 --- a/extensions/msteams/src/attachments.test.ts +++ b/extensions/msteams/src/attachments.test.ts @@ -531,7 +531,8 @@ describe("msteams attachments", () => { it("blocks redirects to non-https URLs", async () => { const insecureUrl = "http://x/insecure.png"; const fetchMock = vi.fn(async (input: RequestInfo | URL) => { - const url = typeof input === "string" ? input : input.toString(); + const url = + typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; if (url === TEST_URL_IMAGE) { return createRedirectResponse(insecureUrl); } @@ -560,7 +561,8 @@ describe("msteams attachments", () => { const createGraphSharesFetchMock = () => vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { - const url = typeof input === "string" ? input : input.toString(); + const url = + typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; const auth = new Headers(init?.headers).get("Authorization"); if (url.startsWith(GRAPH_SHARES_URL_PREFIX)) { if (!auth) { @@ -617,7 +619,8 @@ describe("msteams attachments", () => { expect(media[0]?.path).toBe(SAVED_PDF_PATH); // The only host that should be fetched is graph.microsoft.com. const calledUrls = (fetchMock.mock.calls as Array<[RequestInfo | URL, RequestInit?]>).map( - ([input]) => (typeof input === "string" ? input : String(input)), + ([input]) => + typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url, ); expect(calledUrls.length).toBeGreaterThan(0); for (const url of calledUrls) { @@ -646,7 +649,11 @@ describe("msteams attachments", () => { expectAttachmentMediaLength(media, 1); const calledUrls = (fetchMock.mock.calls as unknown[]).map((call) => { const input = (call as [RequestInfo | URL])[0]; - return typeof input === "string" ? input : String(input); + return typeof input === "string" + ? input + : input instanceof URL + ? input.toString() + : input.url; }); // Should have hit the original host, NOT graph shares. expect(calledUrls.some((url) => url === directUrl)).toBe(true); diff --git a/extensions/msteams/src/graph.test.ts b/extensions/msteams/src/graph.test.ts index 6165e80f922..1dc97957fed 100644 --- a/extensions/msteams/src/graph.test.ts +++ b/extensions/msteams/src/graph.test.ts @@ -87,11 +87,15 @@ function mockGraphCollection(...items: T[]) { } function requestUrl(input: string | URL | Request) { - return typeof input === "string" ? input : String(input); + return typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; } function fetchCallUrl(index: number) { - return String(vi.mocked(globalThis.fetch).mock.calls[index]?.[0]); + const input = vi.mocked(globalThis.fetch).mock.calls[index]?.[0]; + if (!input) { + return ""; + } + return requestUrl(input); } function expectFetchPathContains(index: number, expectedPath: string) { diff --git a/extensions/msteams/src/setup-surface.test.ts b/extensions/msteams/src/setup-surface.test.ts index 522dc2b8966..a3effc8a445 100644 --- a/extensions/msteams/src/setup-surface.test.ts +++ b/extensions/msteams/src/setup-surface.test.ts @@ -5,7 +5,7 @@ import { createMSTeamsSetupWizardBase, msteamsSetupAdapter } from "./setup-core. const resolveMSTeamsUserAllowlist = vi.hoisted(() => vi.fn()); const resolveMSTeamsChannelAllowlist = vi.hoisted(() => vi.fn()); const normalizeSecretInputString = vi.hoisted(() => - vi.fn((value: unknown) => String(value ?? "").trim() || undefined), + vi.fn((value: unknown) => (typeof value === "string" ? value.trim() || undefined : undefined)), ); const hasConfiguredMSTeamsCredentials = vi.hoisted(() => vi.fn()); const resolveMSTeamsCredentials = vi.hoisted(() => vi.fn()); diff --git a/extensions/nostr/src/nostr-profile-http.test.ts b/extensions/nostr/src/nostr-profile-http.test.ts index fe12e6c5ecd..37ad873060f 100644 --- a/extensions/nostr/src/nostr-profile-http.test.ts +++ b/extensions/nostr/src/nostr-profile-http.test.ts @@ -98,13 +98,13 @@ function createMockResponse(): ServerResponse & { }); res.write = function (chunk: unknown) { - data += String(chunk); + data += typeof chunk === "string" ? chunk : Buffer.isBuffer(chunk) ? chunk.toString() : ""; return true; }; res.end = function (chunk?: unknown) { if (chunk) { - data += String(chunk); + data += typeof chunk === "string" ? chunk : Buffer.isBuffer(chunk) ? chunk.toString() : ""; } return this; }; diff --git a/extensions/openai/speech-provider.test.ts b/extensions/openai/speech-provider.test.ts index 58e634399ab..35a7433d4b8 100644 --- a/extensions/openai/speech-provider.test.ts +++ b/extensions/openai/speech-provider.test.ts @@ -127,7 +127,11 @@ describe("buildOpenAISpeechProvider", () => { const provider = buildOpenAISpeechProvider(); const fetchMock = vi.fn(async (_url: string, init?: RequestInit) => { expect(init?.body).toBeTruthy(); - const body = JSON.parse(String(init?.body)) as { response_format?: string }; + const requestBody = init?.body; + if (typeof requestBody !== "string") { + throw new Error("expected string request body"); + } + const body = JSON.parse(requestBody) as { response_format?: string }; expect(body.response_format).toBe("wav"); return new Response(new Uint8Array([1, 2, 3]), { status: 200 }); }); @@ -155,7 +159,11 @@ describe("buildOpenAISpeechProvider", () => { const provider = buildOpenAISpeechProvider(); const fetchMock = vi.fn(async (_url: string, init?: RequestInit) => { expect(init?.body).toBeTruthy(); - const body = JSON.parse(String(init?.body)) as { response_format?: string }; + const requestBody = init?.body; + if (typeof requestBody !== "string") { + throw new Error("expected string request body"); + } + const body = JSON.parse(requestBody) as { response_format?: string }; expect(body.response_format).toBe("wav"); return new Response(new Uint8Array([1, 2, 3]), { status: 200 }); }); diff --git a/extensions/telegram/src/dm-access.test.ts b/extensions/telegram/src/dm-access.test.ts index b893819cb62..5d4075f37f4 100644 --- a/extensions/telegram/src/dm-access.test.ts +++ b/extensions/telegram/src/dm-access.test.ts @@ -142,7 +142,7 @@ describe("enforceTelegramDmAccess", () => { expect(sendMessage).toHaveBeenCalledTimes(1); const [firstCall] = sendMessage.mock.calls as Array; expect(firstCall?.[0]).toBe(42); - const sentText = String(firstCall?.[1] ?? ""); + const sentText = typeof firstCall?.[1] === "string" ? firstCall[1] : ""; expect(sentText).toContain("Pairing code:"); expect(firstCall?.[2]).toEqual(expect.objectContaining({ parse_mode: "HTML" })); expect(logger.info).toHaveBeenCalledWith( diff --git a/extensions/voice-call/src/providers/twilio/api.test.ts b/extensions/voice-call/src/providers/twilio/api.test.ts index dbbed3493b8..5add6b72e1b 100644 --- a/extensions/voice-call/src/providers/twilio/api.test.ts +++ b/extensions/voice-call/src/providers/twilio/api.test.ts @@ -37,7 +37,9 @@ describe("twilioApiRequest", () => { }, }), ); - expect(String(init?.body)).toBe( + const requestBody = init?.body; + expect(requestBody).toBeInstanceOf(URLSearchParams); + expect((requestBody as URLSearchParams).toString()).toBe( "To=%2B14155550123&StatusCallbackEvent=initiated&StatusCallbackEvent=completed", ); }); diff --git a/extensions/whatsapp/src/channel-react-action.test.ts b/extensions/whatsapp/src/channel-react-action.test.ts index e458293ab81..05623b96898 100644 --- a/extensions/whatsapp/src/channel-react-action.test.ts +++ b/extensions/whatsapp/src/channel-react-action.test.ts @@ -38,7 +38,7 @@ vi.mock("./channel-react-action.runtime.js", async () => { } return undefined; } - const text = String(value); + const text = typeof value === "string" ? value : ""; if (!options?.allowEmpty && !text.trim()) { if (options?.required) { const err = new Error(`${key} required`); diff --git a/ui/src/ui/app-chat.test.ts b/ui/src/ui/app-chat.test.ts index 3cbc01d7cb3..9cca524b6ed 100644 --- a/ui/src/ui/app-chat.test.ts +++ b/ui/src/ui/app-chat.test.ts @@ -107,7 +107,8 @@ describe("refreshChatAvatar", () => { const mainRequest = createDeferred<{ avatarUrl?: string }>(); const opsRequest = createDeferred<{ avatarUrl?: string }>(); const fetchMock = vi.fn((input: string | URL | Request) => { - const url = String(input); + const url = + typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; if (url === "avatar/main?meta=1") { return Promise.resolve({ ok: true,