mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
test: add fetch mock helper and reaction coverage
This commit is contained in:
@@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai
|
||||
- iOS/Talk: harden barge-in behavior by disabling interrupt-on-speech when output route is built-in speaker/receiver, reducing false interruptions from local TTS bleed-through. Thanks @zeulewan.
|
||||
- iOS/Talk: add a `Voice Directive Hint` toggle for Talk Mode prompts so users can disable ElevenLabs voice-switching instructions to save tokens when not needed. (#18250) Thanks @zeulewan.
|
||||
- Telegram/Agents: add inline button `style` support (`primary|success|danger`) across message tool schema, Telegram action parsing, send pipeline, and runtime prompt guidance. (#18241) Thanks @obviyus.
|
||||
- Telegram: surface user message reactions as system events, with configurable `channels.telegram.reactionNotifications` scope. (#10075) Thanks @Glucksberg.
|
||||
- Discord: expose native `/exec` command options (host/security/ask/node) so Discord slash commands get autocomplete and structured inputs. Thanks @thewilloftheshadow.
|
||||
- Discord: allow reusable interactive components with `components.reusable=true` so buttons, selects, and forms can be used multiple times before expiring. Thanks @thewilloftheshadow.
|
||||
- Cron/Gateway: separate per-job webhook delivery (`delivery.mode = "webhook"`) from announce delivery, enforce valid HTTP(S) webhook URLs, and keep a temporary legacy `notify + cron.webhook` fallback for stored jobs. (#17901) Thanks @advaitpaliwal.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { withFetchPreconnect } from "../test-utils/fetch-mock.js";
|
||||
import {
|
||||
CHUTES_TOKEN_ENDPOINT,
|
||||
CHUTES_USERINFO_ENDPOINT,
|
||||
@@ -15,7 +16,7 @@ const urlToString = (url: Request | URL | string): string => {
|
||||
|
||||
describe("chutes-oauth", () => {
|
||||
it("exchanges code for tokens and stores username as email", async () => {
|
||||
const fetchFn: typeof fetch = async (input, init) => {
|
||||
const fetchFn = withFetchPreconnect(async (input, init) => {
|
||||
const url = urlToString(input);
|
||||
if (url === CHUTES_TOKEN_ENDPOINT) {
|
||||
expect(init?.method).toBe("POST");
|
||||
@@ -41,7 +42,7 @@ describe("chutes-oauth", () => {
|
||||
});
|
||||
}
|
||||
return new Response("not found", { status: 404 });
|
||||
};
|
||||
});
|
||||
|
||||
const now = 1_000_000;
|
||||
const creds = await exchangeChutesCodeForTokens({
|
||||
@@ -65,7 +66,7 @@ describe("chutes-oauth", () => {
|
||||
});
|
||||
|
||||
it("refreshes tokens using stored client id and falls back to old refresh token", async () => {
|
||||
const fetchFn: typeof fetch = async (input, init) => {
|
||||
const fetchFn = withFetchPreconnect(async (input, init) => {
|
||||
const url = urlToString(input);
|
||||
if (url !== CHUTES_TOKEN_ENDPOINT) {
|
||||
return new Response("not found", { status: 404 });
|
||||
@@ -82,7 +83,7 @@ describe("chutes-oauth", () => {
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
const now = 2_000_000;
|
||||
const refreshed = await refreshChutesTokens({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { withFetchPreconnect } from "../test-utils/fetch-mock.js";
|
||||
|
||||
describe("minimaxUnderstandImage apiKey normalization", () => {
|
||||
const priorFetch = global.fetch;
|
||||
@@ -21,7 +22,7 @@ describe("minimaxUnderstandImage apiKey normalization", () => {
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
});
|
||||
global.fetch = fetchSpy;
|
||||
global.fetch = withFetchPreconnect(fetchSpy);
|
||||
|
||||
const { minimaxUnderstandImage } = await import("./minimax-vlm.js");
|
||||
const text = await minimaxUnderstandImage({
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { captureEnv } from "../test-utils/env.js";
|
||||
import { withFetchPreconnect } from "../test-utils/fetch-mock.js";
|
||||
import { scanOpenRouterModels } from "./model-scan.js";
|
||||
|
||||
function createFetchFixture(payload: unknown): typeof fetch {
|
||||
return async () =>
|
||||
new Response(JSON.stringify(payload), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
return withFetchPreconnect(
|
||||
async () =>
|
||||
new Response(JSON.stringify(payload), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
describe("scanOpenRouterModels", () => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import type { ModelDefinitionConfig } from "../../config/types.models.js";
|
||||
import { withFetchPreconnect } from "../../test-utils/fetch-mock.js";
|
||||
import { createOpenClawCodingTools } from "../pi-tools.js";
|
||||
import { createHostSandboxFsBridge } from "../test-helpers/host-sandbox-fs-bridge.js";
|
||||
import { __testing, createImageTool, resolveImageModelConfigForTool } from "./image-tool.js";
|
||||
@@ -47,7 +48,7 @@ function stubMinimaxOkFetch() {
|
||||
base_resp: { status_code: 0, status_msg: "" },
|
||||
}),
|
||||
});
|
||||
global.fetch = fetch;
|
||||
global.fetch = withFetchPreconnect(fetch);
|
||||
vi.stubEnv("MINIMAX_API_KEY", "minimax-test");
|
||||
return fetch;
|
||||
}
|
||||
@@ -414,7 +415,7 @@ describe("image tool implicit imageModel config", () => {
|
||||
base_resp: { status_code: 0, status_msg: "" },
|
||||
}),
|
||||
});
|
||||
global.fetch = fetch;
|
||||
global.fetch = withFetchPreconnect(fetch);
|
||||
vi.stubEnv("MINIMAX_API_KEY", "minimax-test");
|
||||
|
||||
const cfg: OpenClawConfig = {
|
||||
@@ -487,7 +488,7 @@ describe("image tool MiniMax VLM routing", () => {
|
||||
base_resp: baseResp,
|
||||
}),
|
||||
});
|
||||
global.fetch = fetch;
|
||||
global.fetch = withFetchPreconnect(fetch);
|
||||
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-minimax-vlm-"));
|
||||
vi.stubEnv("MINIMAX_API_KEY", "minimax-test");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import * as logger from "../../logger.js";
|
||||
import { withFetchPreconnect } from "../../test-utils/fetch-mock.js";
|
||||
import {
|
||||
createBaseWebFetchToolConfig,
|
||||
installWebFetchSsrfHarness,
|
||||
@@ -37,7 +38,7 @@ function htmlResponse(body: string): Response {
|
||||
describe("web_fetch Cloudflare Markdown for Agents", () => {
|
||||
it("sends Accept header preferring text/markdown", async () => {
|
||||
const fetchSpy = vi.fn().mockResolvedValue(markdownResponse("# Test Page\n\nHello world."));
|
||||
global.fetch = fetchSpy;
|
||||
global.fetch = withFetchPreconnect(fetchSpy);
|
||||
|
||||
const tool = createWebFetchTool(baseToolConfig);
|
||||
|
||||
@@ -51,7 +52,7 @@ describe("web_fetch Cloudflare Markdown for Agents", () => {
|
||||
it("uses cf-markdown extractor for text/markdown responses", async () => {
|
||||
const md = "# CF Markdown\n\nThis is server-rendered markdown.";
|
||||
const fetchSpy = vi.fn().mockResolvedValue(markdownResponse(md));
|
||||
global.fetch = fetchSpy;
|
||||
global.fetch = withFetchPreconnect(fetchSpy);
|
||||
|
||||
const tool = createWebFetchTool(baseToolConfig);
|
||||
|
||||
@@ -73,7 +74,7 @@ describe("web_fetch Cloudflare Markdown for Agents", () => {
|
||||
const html =
|
||||
"<html><body><article><h1>HTML Page</h1><p>Content here.</p></article></body></html>";
|
||||
const fetchSpy = vi.fn().mockResolvedValue(htmlResponse(html));
|
||||
global.fetch = fetchSpy;
|
||||
global.fetch = withFetchPreconnect(fetchSpy);
|
||||
|
||||
const tool = createWebFetchTool(baseToolConfig);
|
||||
|
||||
@@ -88,7 +89,7 @@ describe("web_fetch Cloudflare Markdown for Agents", () => {
|
||||
const fetchSpy = vi
|
||||
.fn()
|
||||
.mockResolvedValue(markdownResponse("# Tokens Test", { "x-markdown-tokens": "1500" }));
|
||||
global.fetch = fetchSpy;
|
||||
global.fetch = withFetchPreconnect(fetchSpy);
|
||||
|
||||
const tool = createWebFetchTool(baseToolConfig);
|
||||
|
||||
@@ -108,7 +109,7 @@ describe("web_fetch Cloudflare Markdown for Agents", () => {
|
||||
it("converts markdown to text when extractMode is text", async () => {
|
||||
const md = "# Heading\n\n**Bold text** and [a link](https://example.com).";
|
||||
const fetchSpy = vi.fn().mockResolvedValue(markdownResponse(md));
|
||||
global.fetch = fetchSpy;
|
||||
global.fetch = withFetchPreconnect(fetchSpy);
|
||||
|
||||
const tool = createWebFetchTool(baseToolConfig);
|
||||
|
||||
@@ -132,7 +133,7 @@ describe("web_fetch Cloudflare Markdown for Agents", () => {
|
||||
it("does not log x-markdown-tokens when header is absent", async () => {
|
||||
const logSpy = vi.spyOn(logger, "logDebug").mockImplementation(() => {});
|
||||
const fetchSpy = vi.fn().mockResolvedValue(markdownResponse("# No tokens"));
|
||||
global.fetch = fetchSpy;
|
||||
global.fetch = withFetchPreconnect(fetchSpy);
|
||||
|
||||
const tool = createWebFetchTool(baseToolConfig);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { withFetchPreconnect } from "../../test-utils/fetch-mock.js";
|
||||
|
||||
vi.mock("../../infra/net/fetch-guard.js", () => {
|
||||
return {
|
||||
@@ -33,7 +34,7 @@ describe("web_fetch firecrawl apiKey normalization", () => {
|
||||
);
|
||||
});
|
||||
|
||||
global.fetch = fetchSpy;
|
||||
global.fetch = withFetchPreconnect(fetchSpy);
|
||||
|
||||
const { createWebFetchTool } = await import("./web-tools.js");
|
||||
const tool = createWebFetchTool({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { withFetchPreconnect } from "../../test-utils/fetch-mock.js";
|
||||
import {
|
||||
createBaseWebFetchToolConfig,
|
||||
installWebFetchSsrfHarness,
|
||||
@@ -23,7 +24,7 @@ describe("web_fetch response size limits", () => {
|
||||
});
|
||||
|
||||
const fetchSpy = vi.fn().mockResolvedValue(response);
|
||||
global.fetch = fetchSpy;
|
||||
global.fetch = withFetchPreconnect(fetchSpy);
|
||||
|
||||
const tool = createWebFetchTool(baseToolConfig);
|
||||
const result = await tool?.execute?.("call", { url: "https://example.com/stream" });
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import * as ssrf from "../../infra/net/ssrf.js";
|
||||
import { withFetchPreconnect } from "../../test-utils/fetch-mock.js";
|
||||
|
||||
const lookupMock = vi.fn();
|
||||
const resolvePinnedHostname = ssrf.resolvePinnedHostname;
|
||||
@@ -30,7 +31,7 @@ function textResponse(body: string): Response {
|
||||
|
||||
function setMockFetch(impl?: (...args: unknown[]) => unknown) {
|
||||
const fetchSpy = vi.fn(impl);
|
||||
global.fetch = fetchSpy as typeof fetch;
|
||||
global.fetch = withFetchPreconnect(fetchSpy);
|
||||
return fetchSpy;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { withFetchPreconnect } from "../../test-utils/fetch-mock.js";
|
||||
import { createWebFetchTool, createWebSearchTool } from "./web-tools.js";
|
||||
|
||||
function installMockFetch(payload: unknown) {
|
||||
@@ -8,7 +9,7 @@ function installMockFetch(payload: unknown) {
|
||||
json: () => Promise.resolve(payload),
|
||||
} as Response),
|
||||
);
|
||||
global.fetch = mockFetch;
|
||||
global.fetch = withFetchPreconnect(mockFetch);
|
||||
return mockFetch;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import * as ssrf from "../../infra/net/ssrf.js";
|
||||
import { withFetchPreconnect } from "../../test-utils/fetch-mock.js";
|
||||
import { createWebFetchTool } from "./web-tools.js";
|
||||
|
||||
type MockResponse = {
|
||||
@@ -92,7 +93,7 @@ function requestUrl(input: RequestInfo | URL): string {
|
||||
|
||||
function installMockFetch(impl: (input: RequestInfo | URL) => Promise<Response>) {
|
||||
const mockFetch = vi.fn(async (input: RequestInfo | URL) => await impl(input));
|
||||
global.fetch = mockFetch as typeof global.fetch;
|
||||
global.fetch = withFetchPreconnect(mockFetch);
|
||||
return mockFetch;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { withFetchPreconnect } from "../test-utils/fetch-mock.js";
|
||||
import type { BrowserServerState } from "./server-context.js";
|
||||
import "./server-context.chrome-test-harness.js";
|
||||
import { createBrowserRouteContext } from "./server-context.js";
|
||||
@@ -56,7 +57,7 @@ function stubChromeJsonList(responses: unknown[]) {
|
||||
} as unknown as Response;
|
||||
});
|
||||
|
||||
global.fetch = fetchMock;
|
||||
global.fetch = withFetchPreconnect(fetchMock);
|
||||
return fetchMock;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { withFetchPreconnect } from "../test-utils/fetch-mock.js";
|
||||
import * as cdpModule from "./cdp.js";
|
||||
import * as pwAiModule from "./pw-ai-module.js";
|
||||
import type { BrowserServerState } from "./server-context.js";
|
||||
@@ -70,7 +71,7 @@ describe("browser server-context remote profile tab operations", () => {
|
||||
throw new Error("unexpected fetch");
|
||||
});
|
||||
|
||||
global.fetch = fetchMock;
|
||||
global.fetch = withFetchPreconnect(fetchMock);
|
||||
|
||||
const state = makeState("remote");
|
||||
const ctx = createBrowserRouteContext({ getState: () => state });
|
||||
@@ -135,7 +136,7 @@ describe("browser server-context remote profile tab operations", () => {
|
||||
throw new Error("unexpected fetch");
|
||||
});
|
||||
|
||||
global.fetch = fetchMock;
|
||||
global.fetch = withFetchPreconnect(fetchMock);
|
||||
|
||||
const state = makeState("remote");
|
||||
const ctx = createBrowserRouteContext({ getState: () => state });
|
||||
@@ -162,7 +163,7 @@ describe("browser server-context remote profile tab operations", () => {
|
||||
throw new Error("unexpected fetch");
|
||||
});
|
||||
|
||||
global.fetch = fetchMock;
|
||||
global.fetch = withFetchPreconnect(fetchMock);
|
||||
|
||||
const state = makeState("remote");
|
||||
const ctx = createBrowserRouteContext({ getState: () => state });
|
||||
@@ -188,7 +189,7 @@ describe("browser server-context remote profile tab operations", () => {
|
||||
throw new Error("unexpected fetch");
|
||||
});
|
||||
|
||||
global.fetch = fetchMock;
|
||||
global.fetch = withFetchPreconnect(fetchMock);
|
||||
|
||||
const state = makeState("remote");
|
||||
const ctx = createBrowserRouteContext({ getState: () => state });
|
||||
@@ -220,7 +221,7 @@ describe("browser server-context remote profile tab operations", () => {
|
||||
} as unknown as Response;
|
||||
});
|
||||
|
||||
global.fetch = fetchMock;
|
||||
global.fetch = withFetchPreconnect(fetchMock);
|
||||
|
||||
const state = makeState("remote");
|
||||
const ctx = createBrowserRouteContext({ getState: () => state });
|
||||
@@ -255,7 +256,7 @@ describe("browser server-context tab selection state", () => {
|
||||
} as unknown as Response;
|
||||
});
|
||||
|
||||
global.fetch = fetchMock;
|
||||
global.fetch = withFetchPreconnect(fetchMock);
|
||||
|
||||
const state = makeState("openclaw");
|
||||
const ctx = createBrowserRouteContext({ getState: () => state });
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import net from "node:net";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { CHUTES_TOKEN_ENDPOINT, CHUTES_USERINFO_ENDPOINT } from "../agents/chutes-oauth.js";
|
||||
import { withFetchPreconnect } from "../test-utils/fetch-mock.js";
|
||||
import { loginChutes } from "./chutes-oauth.js";
|
||||
|
||||
async function getFreePort(): Promise<number> {
|
||||
@@ -32,7 +33,7 @@ function createOAuthFetchFn(params: {
|
||||
username: string;
|
||||
passthrough?: boolean;
|
||||
}): typeof fetch {
|
||||
return async (input, init) => {
|
||||
return withFetchPreconnect(async (input, init) => {
|
||||
const url = urlToString(input);
|
||||
if (url === CHUTES_TOKEN_ENDPOINT) {
|
||||
return new Response(
|
||||
@@ -54,7 +55,7 @@ function createOAuthFetchFn(params: {
|
||||
return fetch(input, init);
|
||||
}
|
||||
return new Response("not found", { status: 404 });
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
describe("loginChutes", () => {
|
||||
@@ -157,7 +158,7 @@ describe("loginChutes", () => {
|
||||
});
|
||||
|
||||
it("rejects pasted redirect URLs missing state", async () => {
|
||||
const fetchFn: typeof fetch = async () => new Response("not found", { status: 404 });
|
||||
const fetchFn = withFetchPreconnect(async () => new Response("not found", { status: 404 }));
|
||||
|
||||
await expect(
|
||||
loginChutes({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { withFetchPreconnect } from "../test-utils/fetch-mock.js";
|
||||
import { fetchDiscord } from "./api.js";
|
||||
|
||||
function jsonResponse(body: unknown, status = 200) {
|
||||
@@ -7,7 +8,7 @@ function jsonResponse(body: unknown, status = 200) {
|
||||
|
||||
describe("fetchDiscord", () => {
|
||||
it("formats rate limit payloads without raw JSON", async () => {
|
||||
const fetcher = async () =>
|
||||
const fetcher = withFetchPreconnect(async () =>
|
||||
jsonResponse(
|
||||
{
|
||||
message: "You are being rate limited.",
|
||||
@@ -15,11 +16,12 @@ describe("fetchDiscord", () => {
|
||||
global: false,
|
||||
},
|
||||
429,
|
||||
);
|
||||
),
|
||||
);
|
||||
|
||||
let error: unknown;
|
||||
try {
|
||||
await fetchDiscord("/users/@me/guilds", "test", fetcher as typeof fetch, {
|
||||
await fetchDiscord("/users/@me/guilds", "test", fetcher, {
|
||||
retry: { attempts: 1 },
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -35,9 +37,9 @@ describe("fetchDiscord", () => {
|
||||
});
|
||||
|
||||
it("preserves non-JSON error text", async () => {
|
||||
const fetcher = async () => new Response("Not Found", { status: 404 });
|
||||
const fetcher = withFetchPreconnect(async () => new Response("Not Found", { status: 404 }));
|
||||
await expect(
|
||||
fetchDiscord("/users/@me/guilds", "test", fetcher as typeof fetch, {
|
||||
fetchDiscord("/users/@me/guilds", "test", fetcher, {
|
||||
retry: { attempts: 1 },
|
||||
}),
|
||||
).rejects.toThrow("Discord API /users/@me/guilds failed (404): Not Found");
|
||||
@@ -45,7 +47,7 @@ describe("fetchDiscord", () => {
|
||||
|
||||
it("retries rate limits before succeeding", async () => {
|
||||
let calls = 0;
|
||||
const fetcher = async () => {
|
||||
const fetcher = withFetchPreconnect(async () => {
|
||||
calls += 1;
|
||||
if (calls === 1) {
|
||||
return jsonResponse(
|
||||
@@ -58,12 +60,12 @@ describe("fetchDiscord", () => {
|
||||
);
|
||||
}
|
||||
return jsonResponse([{ id: "1", name: "Guild" }], 200);
|
||||
};
|
||||
});
|
||||
|
||||
const result = await fetchDiscord<Array<{ id: string; name: string }>>(
|
||||
"/users/@me/guilds",
|
||||
"test",
|
||||
fetcher as typeof fetch,
|
||||
fetcher,
|
||||
{ retry: { attempts: 2, minDelayMs: 0, maxDelayMs: 0 } },
|
||||
);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { withFetchPreconnect } from "../test-utils/fetch-mock.js";
|
||||
import { resolveDiscordChannelAllowlist } from "./resolve-channels.js";
|
||||
|
||||
function jsonResponse(body: unknown) {
|
||||
@@ -14,7 +15,7 @@ const urlToString = (url: Request | URL | string): string => {
|
||||
|
||||
describe("resolveDiscordChannelAllowlist", () => {
|
||||
it("resolves guild/channel by name", async () => {
|
||||
const fetcher = async (input: RequestInfo | URL) => {
|
||||
const fetcher = withFetchPreconnect(async (input: RequestInfo | URL) => {
|
||||
const url = urlToString(input);
|
||||
if (url.endsWith("/users/@me/guilds")) {
|
||||
return jsonResponse([{ id: "g1", name: "My Guild" }]);
|
||||
@@ -26,7 +27,7 @@ describe("resolveDiscordChannelAllowlist", () => {
|
||||
]);
|
||||
}
|
||||
return new Response("not found", { status: 404 });
|
||||
};
|
||||
});
|
||||
|
||||
const res = await resolveDiscordChannelAllowlist({
|
||||
token: "test",
|
||||
@@ -40,7 +41,7 @@ describe("resolveDiscordChannelAllowlist", () => {
|
||||
});
|
||||
|
||||
it("resolves channel id to guild", async () => {
|
||||
const fetcher = async (input: RequestInfo | URL) => {
|
||||
const fetcher = withFetchPreconnect(async (input: RequestInfo | URL) => {
|
||||
const url = urlToString(input);
|
||||
if (url.endsWith("/users/@me/guilds")) {
|
||||
return jsonResponse([{ id: "g1", name: "Guild One" }]);
|
||||
@@ -49,7 +50,7 @@ describe("resolveDiscordChannelAllowlist", () => {
|
||||
return jsonResponse({ id: "123", name: "general", guild_id: "g1", type: 0 });
|
||||
}
|
||||
return new Response("not found", { status: 404 });
|
||||
};
|
||||
});
|
||||
|
||||
const res = await resolveDiscordChannelAllowlist({
|
||||
token: "test",
|
||||
@@ -63,7 +64,7 @@ describe("resolveDiscordChannelAllowlist", () => {
|
||||
});
|
||||
|
||||
it("resolves guild: prefixed id as guild (not channel)", async () => {
|
||||
const fetcher = async (input: RequestInfo | URL) => {
|
||||
const fetcher = withFetchPreconnect(async (input: RequestInfo | URL) => {
|
||||
const url = urlToString(input);
|
||||
if (url.endsWith("/users/@me/guilds")) {
|
||||
return jsonResponse([{ id: "111222333444555666", name: "Guild One" }]);
|
||||
@@ -73,7 +74,7 @@ describe("resolveDiscordChannelAllowlist", () => {
|
||||
throw new Error("guild id was incorrectly routed to /channels/");
|
||||
}
|
||||
return new Response("not found", { status: 404 });
|
||||
};
|
||||
});
|
||||
|
||||
const res = await resolveDiscordChannelAllowlist({
|
||||
token: "test",
|
||||
@@ -90,7 +91,7 @@ describe("resolveDiscordChannelAllowlist", () => {
|
||||
// Demonstrates why provider.ts must prefix guild-only entries with "guild:"
|
||||
// In reality, Discord returns 404 when a guild ID is sent to /channels/<guildId>,
|
||||
// which causes fetchDiscord to throw and the entire resolver to crash.
|
||||
const fetcher = async (input: RequestInfo | URL) => {
|
||||
const fetcher = withFetchPreconnect(async (input: RequestInfo | URL) => {
|
||||
const url = urlToString(input);
|
||||
if (url.endsWith("/users/@me/guilds")) {
|
||||
return jsonResponse([{ id: "999", name: "My Server" }]);
|
||||
@@ -100,7 +101,7 @@ describe("resolveDiscordChannelAllowlist", () => {
|
||||
return new Response(JSON.stringify({ message: "Unknown Channel" }), { status: 404 });
|
||||
}
|
||||
return new Response("not found", { status: 404 });
|
||||
};
|
||||
});
|
||||
|
||||
// Without the guild: prefix, a bare numeric string hits /channels/999 → 404 → throws
|
||||
await expect(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { withFetchPreconnect } from "../test-utils/fetch-mock.js";
|
||||
import { resolveFetch, wrapFetchWithAbortSignal } from "./fetch.js";
|
||||
|
||||
function createForeignSignalHarness() {
|
||||
@@ -29,10 +30,12 @@ function createForeignSignalHarness() {
|
||||
describe("wrapFetchWithAbortSignal", () => {
|
||||
it("adds duplex for requests with a body", async () => {
|
||||
let seenInit: RequestInit | undefined;
|
||||
const fetchImpl = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => {
|
||||
seenInit = init;
|
||||
return {} as Response;
|
||||
});
|
||||
const fetchImpl = withFetchPreconnect(
|
||||
vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => {
|
||||
seenInit = init;
|
||||
return {} as Response;
|
||||
}),
|
||||
);
|
||||
|
||||
const wrapped = wrapFetchWithAbortSignal(fetchImpl);
|
||||
|
||||
@@ -43,10 +46,12 @@ describe("wrapFetchWithAbortSignal", () => {
|
||||
|
||||
it("converts foreign abort signals to native controllers", async () => {
|
||||
let seenSignal: AbortSignal | undefined;
|
||||
const fetchImpl = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => {
|
||||
seenSignal = init?.signal as AbortSignal | undefined;
|
||||
return {} as Response;
|
||||
});
|
||||
const fetchImpl = withFetchPreconnect(
|
||||
vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => {
|
||||
seenSignal = init?.signal as AbortSignal | undefined;
|
||||
return {} as Response;
|
||||
}),
|
||||
);
|
||||
|
||||
const wrapped = wrapFetchWithAbortSignal(fetchImpl);
|
||||
|
||||
@@ -71,8 +76,8 @@ describe("wrapFetchWithAbortSignal", () => {
|
||||
process.on("unhandledRejection", onUnhandled);
|
||||
|
||||
const fetchError = new TypeError("fetch failed");
|
||||
const fetchImpl = vi.fn((_input: RequestInfo | URL, _init?: RequestInit) =>
|
||||
Promise.reject(fetchError),
|
||||
const fetchImpl = withFetchPreconnect(
|
||||
vi.fn((_input: RequestInfo | URL, _init?: RequestInit) => Promise.reject(fetchError)),
|
||||
);
|
||||
const wrapped = wrapFetchWithAbortSignal(fetchImpl);
|
||||
|
||||
@@ -92,9 +97,11 @@ describe("wrapFetchWithAbortSignal", () => {
|
||||
|
||||
it("cleans up listener and rethrows when fetch throws synchronously", () => {
|
||||
const syncError = new TypeError("sync fetch failure");
|
||||
const fetchImpl = vi.fn(() => {
|
||||
throw syncError;
|
||||
});
|
||||
const fetchImpl = withFetchPreconnect(
|
||||
vi.fn(() => {
|
||||
throw syncError;
|
||||
}),
|
||||
);
|
||||
const wrapped = wrapFetchWithAbortSignal(fetchImpl);
|
||||
|
||||
const { fakeSignal, removeEventListener } = createForeignSignalHarness();
|
||||
@@ -106,8 +113,8 @@ describe("wrapFetchWithAbortSignal", () => {
|
||||
it("preserves original rejection when listener cleanup throws", async () => {
|
||||
const fetchError = new TypeError("fetch failed");
|
||||
const cleanupError = new TypeError("cleanup failed");
|
||||
const fetchImpl = vi.fn((_input: RequestInfo | URL, _init?: RequestInit) =>
|
||||
Promise.reject(fetchError),
|
||||
const fetchImpl = withFetchPreconnect(
|
||||
vi.fn((_input: RequestInfo | URL, _init?: RequestInit) => Promise.reject(fetchError)),
|
||||
);
|
||||
const wrapped = wrapFetchWithAbortSignal(fetchImpl);
|
||||
|
||||
@@ -128,9 +135,11 @@ describe("wrapFetchWithAbortSignal", () => {
|
||||
it("preserves original sync throw when listener cleanup throws", () => {
|
||||
const syncError = new TypeError("sync fetch failure");
|
||||
const cleanupError = new TypeError("cleanup failed");
|
||||
const fetchImpl = vi.fn(() => {
|
||||
throw syncError;
|
||||
});
|
||||
const fetchImpl = withFetchPreconnect(
|
||||
vi.fn(() => {
|
||||
throw syncError;
|
||||
}),
|
||||
);
|
||||
const wrapped = wrapFetchWithAbortSignal(fetchImpl);
|
||||
|
||||
const removeEventListener = vi.fn(() => {
|
||||
@@ -150,7 +159,7 @@ describe("wrapFetchWithAbortSignal", () => {
|
||||
it("skips listener cleanup when foreign signal is already aborted", async () => {
|
||||
const addEventListener = vi.fn();
|
||||
const removeEventListener = vi.fn();
|
||||
const fetchImpl = vi.fn(async () => ({ ok: true }) as Response);
|
||||
const fetchImpl = withFetchPreconnect(vi.fn(async () => ({ ok: true }) as Response));
|
||||
const wrapped = wrapFetchWithAbortSignal(fetchImpl);
|
||||
|
||||
const fakeSignal = {
|
||||
@@ -166,7 +175,7 @@ describe("wrapFetchWithAbortSignal", () => {
|
||||
});
|
||||
|
||||
it("returns the same function when called with an already wrapped fetch", () => {
|
||||
const fetchImpl = vi.fn(async () => ({ ok: true }) as Response);
|
||||
const fetchImpl = withFetchPreconnect(vi.fn(async () => ({ ok: true }) as Response));
|
||||
const wrapped = wrapFetchWithAbortSignal(fetchImpl);
|
||||
|
||||
expect(wrapFetchWithAbortSignal(wrapped)).toBe(wrapped);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { withFetchPreconnect } from "../test-utils/fetch-mock.js";
|
||||
import { fetchAntigravityUsage } from "./provider-usage.fetch.antigravity.js";
|
||||
|
||||
const makeResponse = (status: number, body: unknown): Response => {
|
||||
@@ -12,10 +13,12 @@ const toRequestUrl = (input: Parameters<typeof fetch>[0]): string =>
|
||||
|
||||
const createAntigravityFetch = (
|
||||
handler: (url: string, init?: Parameters<typeof fetch>[1]) => Promise<Response> | Response,
|
||||
) =>
|
||||
vi.fn(async (input: string | Request | URL, init?: RequestInit) =>
|
||||
) => {
|
||||
const mockFetch = vi.fn(async (input: string | Request | URL, init?: RequestInit) =>
|
||||
handler(toRequestUrl(input), init),
|
||||
);
|
||||
return withFetchPreconnect(mockFetch) as typeof fetch & typeof mockFetch;
|
||||
};
|
||||
|
||||
const getRequestBody = (init?: Parameters<typeof fetch>[1]) =>
|
||||
typeof init?.body === "string" ? init.body : undefined;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { withFetchPreconnect } from "../test-utils/fetch-mock.js";
|
||||
import { MediaAttachmentCache } from "./attachments.js";
|
||||
import { normalizeMediaUnderstandingChatType, resolveMediaUnderstandingScope } from "./scope.js";
|
||||
|
||||
@@ -28,7 +29,7 @@ describe("media understanding attachments SSRF", () => {
|
||||
|
||||
it("blocks private IP URLs before fetching", async () => {
|
||||
const fetchSpy = vi.fn();
|
||||
globalThis.fetch = fetchSpy as typeof fetch;
|
||||
globalThis.fetch = withFetchPreconnect(fetchSpy);
|
||||
|
||||
const cache = new MediaAttachmentCache([{ index: 0, url: "http://127.0.0.1/secret.jpg" }]);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { withFetchPreconnect } from "../../../test-utils/fetch-mock.js";
|
||||
import { installPinnedHostnameTestHooks, resolveRequestUrl } from "../audio.test-helpers.js";
|
||||
import { transcribeDeepgramAudio } from "./audio.js";
|
||||
|
||||
@@ -7,7 +8,7 @@ installPinnedHostnameTestHooks();
|
||||
describe("transcribeDeepgramAudio", () => {
|
||||
it("respects lowercase authorization header overrides", async () => {
|
||||
let seenAuth: string | null = null;
|
||||
const fetchFn = async (_input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const fetchFn = withFetchPreconnect(async (_input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const headers = new Headers(init?.headers);
|
||||
seenAuth = headers.get("authorization");
|
||||
return new Response(
|
||||
@@ -19,7 +20,7 @@ describe("transcribeDeepgramAudio", () => {
|
||||
headers: { "content-type": "application/json" },
|
||||
},
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
const result = await transcribeDeepgramAudio({
|
||||
buffer: Buffer.from("audio"),
|
||||
@@ -37,7 +38,7 @@ describe("transcribeDeepgramAudio", () => {
|
||||
it("builds the expected request payload", async () => {
|
||||
let seenUrl: string | null = null;
|
||||
let seenInit: RequestInit | undefined;
|
||||
const fetchFn = async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const fetchFn = withFetchPreconnect(async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
seenUrl = resolveRequestUrl(input);
|
||||
seenInit = init;
|
||||
return new Response(
|
||||
@@ -49,7 +50,7 @@ describe("transcribeDeepgramAudio", () => {
|
||||
headers: { "content-type": "application/json" },
|
||||
},
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
const result = await transcribeDeepgramAudio({
|
||||
buffer: Buffer.from("audio-bytes"),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import * as ssrf from "../../../infra/net/ssrf.js";
|
||||
import { withFetchPreconnect } from "../../../test-utils/fetch-mock.js";
|
||||
import { describeGeminiVideo } from "./video.js";
|
||||
|
||||
const TEST_NET_IP = "203.0.113.10";
|
||||
@@ -47,7 +48,7 @@ describe("describeGeminiVideo", () => {
|
||||
|
||||
it("respects case-insensitive x-goog-api-key overrides", async () => {
|
||||
let seenKey: string | null = null;
|
||||
const fetchFn = async (_input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const fetchFn = withFetchPreconnect(async (_input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const headers = new Headers(init?.headers);
|
||||
seenKey = headers.get("x-goog-api-key");
|
||||
return new Response(
|
||||
@@ -56,7 +57,7 @@ describe("describeGeminiVideo", () => {
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } },
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
const result = await describeGeminiVideo({
|
||||
buffer: Buffer.from("video"),
|
||||
@@ -74,7 +75,7 @@ describe("describeGeminiVideo", () => {
|
||||
it("builds the expected request payload", async () => {
|
||||
let seenUrl: string | null = null;
|
||||
let seenInit: RequestInit | undefined;
|
||||
const fetchFn = async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const fetchFn = withFetchPreconnect(async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
seenUrl = resolveRequestUrl(input);
|
||||
seenInit = init;
|
||||
return new Response(
|
||||
@@ -89,7 +90,7 @@ describe("describeGeminiVideo", () => {
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } },
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
const result = await describeGeminiVideo({
|
||||
buffer: Buffer.from("video-bytes"),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { withFetchPreconnect } from "../../../test-utils/fetch-mock.js";
|
||||
import { installPinnedHostnameTestHooks, resolveRequestUrl } from "../audio.test-helpers.js";
|
||||
import { transcribeOpenAiCompatibleAudio } from "./audio.js";
|
||||
|
||||
@@ -7,14 +8,14 @@ installPinnedHostnameTestHooks();
|
||||
describe("transcribeOpenAiCompatibleAudio", () => {
|
||||
it("respects lowercase authorization header overrides", async () => {
|
||||
let seenAuth: string | null = null;
|
||||
const fetchFn = async (_input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const fetchFn = withFetchPreconnect(async (_input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const headers = new Headers(init?.headers);
|
||||
seenAuth = headers.get("authorization");
|
||||
return new Response(JSON.stringify({ text: "ok" }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
const result = await transcribeOpenAiCompatibleAudio({
|
||||
buffer: Buffer.from("audio"),
|
||||
@@ -32,14 +33,14 @@ describe("transcribeOpenAiCompatibleAudio", () => {
|
||||
it("builds the expected request payload", async () => {
|
||||
let seenUrl: string | null = null;
|
||||
let seenInit: RequestInit | undefined;
|
||||
const fetchFn = async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const fetchFn = withFetchPreconnect(async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
seenUrl = resolveRequestUrl(input);
|
||||
seenInit = init;
|
||||
return new Response(JSON.stringify({ text: "hello" }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
const result = await transcribeOpenAiCompatibleAudio({
|
||||
buffer: Buffer.from("audio-bytes"),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import * as authModule from "../agents/model-auth.js";
|
||||
import { withFetchPreconnect } from "../test-utils/fetch-mock.js";
|
||||
import { createVoyageEmbeddingProvider, normalizeVoyageModel } from "./embeddings-voyage.js";
|
||||
|
||||
vi.mock("../agents/model-auth.js", () => ({
|
||||
@@ -12,12 +13,14 @@ vi.mock("../agents/model-auth.js", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const createFetchMock = () =>
|
||||
vi.fn(async () => ({
|
||||
const createFetchMock = () => {
|
||||
const fetchMock = vi.fn(async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ data: [{ embedding: [0.1, 0.2, 0.3] }] }),
|
||||
})) as unknown as typeof fetch;
|
||||
}));
|
||||
return withFetchPreconnect(fetchMock) as typeof fetch & typeof fetchMock;
|
||||
};
|
||||
|
||||
describe("voyage embedding provider", () => {
|
||||
afterEach(() => {
|
||||
@@ -26,8 +29,8 @@ describe("voyage embedding provider", () => {
|
||||
});
|
||||
|
||||
it("configures client with correct defaults and headers", async () => {
|
||||
const fetchMock = createFetchMock() as ReturnType<typeof vi.fn>;
|
||||
vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);
|
||||
const fetchMock = createFetchMock();
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
vi.mocked(authModule.resolveApiKeyForProvider).mockResolvedValue({
|
||||
apiKey: "voyage-key-123",
|
||||
@@ -64,8 +67,8 @@ describe("voyage embedding provider", () => {
|
||||
});
|
||||
|
||||
it("respects remote overrides for baseUrl and apiKey", async () => {
|
||||
const fetchMock = createFetchMock() as ReturnType<typeof vi.fn>;
|
||||
vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);
|
||||
const fetchMock = createFetchMock();
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const result = await createVoyageEmbeddingProvider({
|
||||
config: {} as never,
|
||||
@@ -90,14 +93,16 @@ describe("voyage embedding provider", () => {
|
||||
});
|
||||
|
||||
it("passes input_type=document for embedBatch", async () => {
|
||||
const fetchMock = vi.fn(async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
data: [{ embedding: [0.1, 0.2] }, { embedding: [0.3, 0.4] }],
|
||||
}),
|
||||
})) as ReturnType<typeof vi.fn>;
|
||||
vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);
|
||||
const fetchMock = withFetchPreconnect(
|
||||
vi.fn(async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
data: [{ embedding: [0.1, 0.2] }, { embedding: [0.3, 0.4] }],
|
||||
}),
|
||||
})),
|
||||
);
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
vi.mocked(authModule.resolveApiKeyForProvider).mockResolvedValue({
|
||||
apiKey: "voyage-key-123",
|
||||
|
||||
@@ -43,7 +43,8 @@ describe("memory indexing with OpenAI batches", () => {
|
||||
|
||||
async function readOpenAIBatchUploadRequests(body: FormData) {
|
||||
let uploadedRequests: Array<{ custom_id?: string }> = [];
|
||||
for (const [key, value] of body.entries()) {
|
||||
const entries = body.entries() as IterableIterator<[string, FormDataEntryValue]>;
|
||||
for (const [key, value] of entries) {
|
||||
if (key !== "file") {
|
||||
continue;
|
||||
}
|
||||
@@ -51,7 +52,7 @@ describe("memory indexing with OpenAI batches", () => {
|
||||
uploadedRequests = text
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((line) => JSON.parse(line) as { custom_id?: string });
|
||||
.map((line: string) => JSON.parse(line) as { custom_id?: string });
|
||||
}
|
||||
return uploadedRequests;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import * as ssrf from "../../infra/net/ssrf.js";
|
||||
import type { SavedMedia } from "../../media/store.js";
|
||||
import * as mediaStore from "../../media/store.js";
|
||||
import { withFetchPreconnect } from "../../test-utils/fetch-mock.js";
|
||||
import {
|
||||
fetchWithSlackAuth,
|
||||
resolveSlackAttachmentContent,
|
||||
@@ -23,7 +24,7 @@ describe("fetchWithSlackAuth", () => {
|
||||
beforeEach(() => {
|
||||
// Create a new mock for each test
|
||||
mockFetch = vi.fn();
|
||||
globalThis.fetch = mockFetch as typeof fetch;
|
||||
globalThis.fetch = withFetchPreconnect(mockFetch);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -169,7 +170,7 @@ describe("fetchWithSlackAuth", () => {
|
||||
describe("resolveSlackMedia", () => {
|
||||
beforeEach(() => {
|
||||
mockFetch = vi.fn();
|
||||
globalThis.fetch = mockFetch as typeof fetch;
|
||||
globalThis.fetch = withFetchPreconnect(mockFetch);
|
||||
vi.spyOn(ssrf, "resolvePinnedHostname").mockImplementation(async (hostname) => {
|
||||
const normalized = hostname.trim().toLowerCase().replace(/\.$/, "");
|
||||
const addresses = ["93.184.216.34"];
|
||||
@@ -432,7 +433,7 @@ describe("resolveSlackMedia", () => {
|
||||
describe("resolveSlackAttachmentContent", () => {
|
||||
beforeEach(() => {
|
||||
mockFetch = vi.fn();
|
||||
globalThis.fetch = mockFetch as typeof fetch;
|
||||
globalThis.fetch = withFetchPreconnect(mockFetch);
|
||||
vi.spyOn(ssrf, "resolvePinnedHostnameWithPolicy").mockImplementation(async (hostname) => {
|
||||
const normalized = hostname.trim().toLowerCase().replace(/\.$/, "");
|
||||
const addresses = ["93.184.216.34"];
|
||||
|
||||
@@ -1001,6 +1001,44 @@ describe("createTelegramBot", () => {
|
||||
expect(enqueueSystemEventSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("enqueues one event per added emoji reaction", async () => {
|
||||
onSpy.mockReset();
|
||||
enqueueSystemEventSpy.mockReset();
|
||||
|
||||
loadConfig.mockReturnValue({
|
||||
channels: {
|
||||
telegram: { dmPolicy: "open", reactionNotifications: "all" },
|
||||
},
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message_reaction") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
update: { update_id: 505 },
|
||||
messageReaction: {
|
||||
chat: { id: 1234, type: "private" },
|
||||
message_id: 42,
|
||||
user: { id: 9, first_name: "Ada" },
|
||||
date: 1736380800,
|
||||
old_reaction: [{ type: "emoji", emoji: "👍" }],
|
||||
new_reaction: [
|
||||
{ type: "emoji", emoji: "👍" },
|
||||
{ type: "emoji", emoji: "🔥" },
|
||||
{ type: "emoji", emoji: "🎉" },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(enqueueSystemEventSpy).toHaveBeenCalledTimes(2);
|
||||
expect(enqueueSystemEventSpy.mock.calls.map((call) => call[0])).toEqual([
|
||||
"Telegram reaction added: 🔥 by Ada on msg 42",
|
||||
"Telegram reaction added: 🎉 by Ada on msg 42",
|
||||
]);
|
||||
});
|
||||
|
||||
it("routes forum group reactions to the general topic (thread id not available on reactions)", async () => {
|
||||
onSpy.mockReset();
|
||||
enqueueSystemEventSpy.mockReset();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { type Mock, describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
||||
import { withFetchPreconnect } from "../test-utils/fetch-mock.js";
|
||||
import { probeTelegram } from "./probe.js";
|
||||
|
||||
describe("probeTelegram retry logic", () => {
|
||||
@@ -38,7 +39,7 @@ describe("probeTelegram retry logic", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
fetchMock = vi.fn();
|
||||
global.fetch = fetchMock;
|
||||
global.fetch = withFetchPreconnect(fetchMock);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
6
src/test-utils/fetch-mock.ts
Normal file
6
src/test-utils/fetch-mock.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const withFetchPreconnect = <T extends (...args: unknown[]) => unknown>(
|
||||
fn: T,
|
||||
): typeof fetch =>
|
||||
Object.assign(fn, {
|
||||
preconnect: () => {},
|
||||
}) as unknown as typeof fetch;
|
||||
Reference in New Issue
Block a user