diff --git a/extensions/mattermost/src/mattermost/client.retry.test.ts b/extensions/mattermost/src/mattermost/client.retry.test.ts index 456313def16..ac15fb6278d 100644 --- a/extensions/mattermost/src/mattermost/client.retry.test.ts +++ b/extensions/mattermost/src/mattermost/client.retry.test.ts @@ -100,13 +100,12 @@ describe("createMattermostDirectChannelWithRetry", () => { return run; } + function jsonResponse(body: unknown, status = 200): Response { + return Response.json(body, { status }); + } + it("succeeds on first attempt without retries", async () => { - mockFetch.mockResolvedValueOnce({ - ok: true, - status: 201, - headers: new Headers({ "content-type": "application/json" }), - json: async () => ({ id: "dm-channel-123" }), - } as Response); + mockFetch.mockResolvedValueOnce(jsonResponse({ id: "dm-channel-123" }, 201)); const client = createMockClient(); const onRetry = vi.fn(); @@ -124,19 +123,8 @@ describe("createMattermostDirectChannelWithRetry", () => { it("retries on 429 rate limit error and succeeds", async () => { mockFetch - .mockResolvedValueOnce({ - ok: false, - status: 429, - headers: new Headers({ "content-type": "application/json" }), - json: async () => ({ message: "Too many requests" }), - text: async () => "Too many requests", - } as Response) - .mockResolvedValueOnce({ - ok: true, - status: 201, - headers: new Headers({ "content-type": "application/json" }), - json: async () => ({ id: "dm-channel-456" }), - } as Response); + .mockResolvedValueOnce(jsonResponse({ message: "Too many requests" }, 429)) + .mockResolvedValueOnce(jsonResponse({ id: "dm-channel-456" }, 201)); const client = createMockClient(); const onRetry = vi.fn(); @@ -157,21 +145,14 @@ describe("createMattermostDirectChannelWithRetry", () => { expect(retryCall?.[1]).toBeGreaterThanOrEqual(10); expect(retryCall?.[1]).toBeLessThanOrEqual(20); expect(retryCall?.[2]).toBeInstanceOf(Error); - expect((retryCall?.[2] as Error | undefined)?.message).toBe( - "Mattermost API 429 undefined: Too many requests", - ); + expect((retryCall?.[2] as Error | undefined)?.message).toContain("Too many requests"); }); it("retries on port 443 connection errors (not misclassified as 4xx)", async () => { // This tests that port numbers like :443 don't trigger false 4xx classification mockFetch .mockRejectedValueOnce(new Error("connect ECONNRESET 104.18.32.10:443")) - .mockResolvedValueOnce({ - ok: true, - status: 201, - headers: new Headers({ "content-type": "application/json" }), - json: async () => ({ id: "dm-channel-port" }), - } as Response); + .mockResolvedValueOnce(jsonResponse({ id: "dm-channel-port" }, 201)); const client = createMockClient(); @@ -190,13 +171,7 @@ describe("createMattermostDirectChannelWithRetry", () => { it("does not retry on 400 even if error message contains '429' text", async () => { // This tests that "429" in error detail doesn't trigger false rate-limit retry // e.g., "Invalid user ID: 4294967295" should NOT be retried - mockFetch.mockResolvedValueOnce({ - ok: false, - status: 400, - headers: new Headers({ "content-type": "application/json" }), - json: async () => ({ message: "Invalid user ID: 4294967295" }), - text: async () => "Invalid user ID: 4294967295", - } as Response); + mockFetch.mockResolvedValueOnce(jsonResponse({ message: "Invalid user ID: 4294967295" }, 400)); const client = createMockClient(); @@ -214,26 +189,9 @@ describe("createMattermostDirectChannelWithRetry", () => { it("retries on 5xx server errors", async () => { mockFetch - .mockResolvedValueOnce({ - ok: false, - status: 503, - headers: new Headers({ "content-type": "application/json" }), - json: async () => ({ message: "Service unavailable" }), - text: async () => "Service unavailable", - } as Response) - .mockResolvedValueOnce({ - ok: false, - status: 502, - headers: new Headers({ "content-type": "application/json" }), - json: async () => ({ message: "Bad gateway" }), - text: async () => "Bad gateway", - } as Response) - .mockResolvedValueOnce({ - ok: true, - status: 201, - headers: new Headers({ "content-type": "application/json" }), - json: async () => ({ id: "dm-channel-789" }), - } as Response); + .mockResolvedValueOnce(jsonResponse({ message: "Service unavailable" }, 503)) + .mockResolvedValueOnce(jsonResponse({ message: "Bad gateway" }, 502)) + .mockResolvedValueOnce(jsonResponse({ id: "dm-channel-789" }, 201)); const client = createMockClient(); @@ -252,12 +210,7 @@ describe("createMattermostDirectChannelWithRetry", () => { mockFetch .mockRejectedValueOnce(new Error("Network error: connection refused")) .mockRejectedValueOnce(new Error("ECONNRESET")) - .mockResolvedValueOnce({ - ok: true, - status: 201, - headers: new Headers({ "content-type": "application/json" }), - json: async () => ({ id: "dm-channel-abc" }), - } as Response); + .mockResolvedValueOnce(jsonResponse({ id: "dm-channel-abc" }, 201)); const client = createMockClient(); @@ -280,12 +233,7 @@ describe("createMattermostDirectChannelWithRetry", () => { code: "ECONNREFUSED", }), ) - .mockResolvedValueOnce({ - ok: true, - status: 201, - headers: new Headers({ "content-type": "application/json" }), - json: async () => ({ id: "dm-channel-fetch-failed" }), - } as Response); + .mockResolvedValueOnce(jsonResponse({ id: "dm-channel-fetch-failed" }, 201)); const client = createMockClient(); @@ -301,13 +249,7 @@ describe("createMattermostDirectChannelWithRetry", () => { }); it("does not retry on 4xx client errors (except 429)", async () => { - mockFetch.mockResolvedValueOnce({ - ok: false, - status: 400, - headers: new Headers({ "content-type": "application/json" }), - json: async () => ({ message: "Bad request" }), - text: async () => "Bad request", - } as Response); + mockFetch.mockResolvedValueOnce(jsonResponse({ message: "Bad request" }, 400)); const client = createMockClient(); @@ -323,13 +265,7 @@ describe("createMattermostDirectChannelWithRetry", () => { }); it("does not retry on 404 not found", async () => { - mockFetch.mockResolvedValueOnce({ - ok: false, - status: 404, - headers: new Headers({ "content-type": "application/json" }), - json: async () => ({ message: "User not found" }), - text: async () => "User not found", - } as Response); + mockFetch.mockResolvedValueOnce(jsonResponse({ message: "User not found" }, 404)); const client = createMockClient(); @@ -345,13 +281,7 @@ describe("createMattermostDirectChannelWithRetry", () => { }); it("throws after exhausting all retries", async () => { - mockFetch.mockResolvedValue({ - ok: false, - status: 503, - headers: new Headers({ "content-type": "application/json" }), - json: async () => ({ message: "Service unavailable" }), - text: async () => "Service unavailable", - } as Response); + mockFetch.mockImplementation(async () => jsonResponse({ message: "Service unavailable" }, 503)); const client = createMockClient(); @@ -414,12 +344,7 @@ describe("createMattermostDirectChannelWithRetry", () => { .spyOn(globalThis, "setTimeout") .mockReturnValue(1 as unknown as ReturnType); vi.spyOn(globalThis, "clearTimeout").mockImplementation(() => undefined); - mockFetch.mockResolvedValueOnce({ - ok: true, - status: 201, - headers: new Headers({ "content-type": "application/json" }), - json: async () => ({ id: "dm-channel-capped" }), - } as Response); + mockFetch.mockResolvedValueOnce(jsonResponse({ id: "dm-channel-capped" }, 201)); const client = createMockClient(); @@ -436,12 +361,7 @@ describe("createMattermostDirectChannelWithRetry", () => { mockFetch .mockRejectedValueOnce(new Error("Mattermost API 503 Service Unavailable")) .mockRejectedValueOnce(new Error("Mattermost API 503 Service Unavailable")) - .mockResolvedValueOnce({ - ok: true, - status: 201, - headers: new Headers({ "content-type": "application/json" }), - json: async () => ({ id: "dm-channel-delay" }), - } as Response); + .mockResolvedValueOnce(jsonResponse({ id: "dm-channel-delay" }, 201)); const client = createMockClient(); @@ -472,12 +392,7 @@ describe("createMattermostDirectChannelWithRetry", () => { .mockRejectedValueOnce(new Error("Mattermost API 503")) .mockRejectedValueOnce(new Error("Mattermost API 503")) .mockRejectedValueOnce(new Error("Mattermost API 503")) - .mockResolvedValueOnce({ - ok: true, - status: 201, - headers: new Headers({ "content-type": "application/json" }), - json: async () => ({ id: "dm-channel-max" }), - } as Response); + .mockResolvedValueOnce(jsonResponse({ id: "dm-channel-max" }, 201)); const client = createMockClient(); @@ -502,13 +417,9 @@ describe("createMattermostDirectChannelWithRetry", () => { it("does not retry on 4xx errors even if message contains retryable keywords", async () => { // This tests the fix for false positives where a 400 error with "timeout" in the message // would incorrectly be retried - mockFetch.mockResolvedValueOnce({ - ok: false, - status: 400, - headers: new Headers({ "content-type": "application/json" }), - json: async () => ({ message: "Request timeout: connection timed out" }), - text: async () => "Request timeout: connection timed out", - } as Response); + mockFetch.mockResolvedValueOnce( + jsonResponse({ message: "Request timeout: connection timed out" }, 400), + ); const client = createMockClient(); @@ -525,13 +436,7 @@ describe("createMattermostDirectChannelWithRetry", () => { }); it("does not retry on 403 Forbidden even with 'abort' in message", async () => { - mockFetch.mockResolvedValueOnce({ - ok: false, - status: 403, - headers: new Headers({ "content-type": "application/json" }), - json: async () => ({ message: "Request aborted: forbidden" }), - text: async () => "Request aborted: forbidden", - } as Response); + mockFetch.mockResolvedValueOnce(jsonResponse({ message: "Request aborted: forbidden" }, 403)); const client = createMockClient(); @@ -550,12 +455,7 @@ describe("createMattermostDirectChannelWithRetry", () => { let capturedSignal: AbortSignal | undefined; mockFetch.mockImplementationOnce((url, init) => { capturedSignal = init?.signal ?? undefined; - return Promise.resolve({ - ok: true, - status: 201, - headers: new Headers({ "content-type": "application/json" }), - json: async () => ({ id: "dm-channel-signal" }), - } as Response); + return Promise.resolve(jsonResponse({ id: "dm-channel-signal" }, 201)); }); const client = createMockClient(); @@ -573,12 +473,7 @@ describe("createMattermostDirectChannelWithRetry", () => { // This tests the fix for the ordering bug: 503 with "upstream 404" should be retried mockFetch .mockRejectedValueOnce(new Error("Mattermost API 503: upstream returned 404 Not Found")) - .mockResolvedValueOnce({ - ok: true, - status: 201, - headers: new Headers({ "content-type": "application/json" }), - json: async () => ({ id: "dm-channel-5xx-with-404" }), - } as Response); + .mockResolvedValueOnce(jsonResponse({ id: "dm-channel-5xx-with-404" }, 201)); const client = createMockClient(); diff --git a/extensions/nvidia/index.test.ts b/extensions/nvidia/index.test.ts index 7f864147800..721db0a622a 100644 --- a/extensions/nvidia/index.test.ts +++ b/extensions/nvidia/index.test.ts @@ -7,7 +7,10 @@ import { } from "openclaw/plugin-sdk/plugin-test-runtime"; import { afterEach, describe, expect, it, vi } from "vitest"; import plugin from "./index.js"; -import { clearNvidiaFeaturedModelCacheForTests } from "./provider-catalog.js"; +import { + clearNvidiaFeaturedModelCacheForTests, + NVIDIA_FEATURED_MODELS_URL, +} from "./provider-catalog.js"; const ssrfRuntimeMocks = vi.hoisted(() => ({ fetchWithSsrFGuard: vi.fn(), @@ -44,6 +47,7 @@ afterEach(() => { function mockFeaturedCatalogResponse(payload: unknown, status = 200) { ssrfRuntimeMocks.fetchWithSsrFGuard.mockResolvedValueOnce({ response: Response.json(payload, { status }), + finalUrl: NVIDIA_FEATURED_MODELS_URL, release: vi.fn(), }); } diff --git a/extensions/nvidia/provider-catalog.test.ts b/extensions/nvidia/provider-catalog.test.ts index 37f6959b7a8..bce32fb1f2d 100644 --- a/extensions/nvidia/provider-catalog.test.ts +++ b/extensions/nvidia/provider-catalog.test.ts @@ -28,6 +28,7 @@ function mockFeaturedCatalogResponse(payload: unknown, status = 200) { const release = vi.fn(); ssrfRuntimeMocks.fetchWithSsrFGuard.mockResolvedValueOnce({ response: Response.json(payload, { status }), + finalUrl: NVIDIA_FEATURED_MODELS_URL, release, }); return release; diff --git a/extensions/telegram/src/bot.command-menu.test.ts b/extensions/telegram/src/bot.command-menu.test.ts index 2c0943220a8..e9a1f4db806 100644 --- a/extensions/telegram/src/bot.command-menu.test.ts +++ b/extensions/telegram/src/bot.command-menu.test.ts @@ -132,7 +132,10 @@ describe("createTelegramBot command menu", () => { const registered = registeredCommands(); const skillCommands = resolveSkillCommands(config); - const native = listNativeCommandSpecsForConfig(config, { skillCommands }).map((command) => ({ + const native = listNativeCommandSpecsForConfig(config, { + skillCommands, + provider: "telegram", + }).map((command) => ({ command: normalizeTelegramCommandName(command.name), description: command.description, })); @@ -183,7 +186,10 @@ describe("createTelegramBot command menu", () => { const registered = registeredCommands(); const skillCommands = resolveSkillCommands(config); - const native = listNativeCommandSpecsForConfig(config, { skillCommands }).map((command) => ({ + const native = listNativeCommandSpecsForConfig(config, { + skillCommands, + provider: "telegram", + }).map((command) => ({ command: normalizeTelegramCommandName(command.name), description: command.description, }));