diff --git a/src/browser/chrome.default-browser.test.ts b/src/browser/chrome.default-browser.test.ts index 8f681e3ce79..d81ad878616 100644 --- a/src/browser/chrome.default-browser.test.ts +++ b/src/browser/chrome.default-browser.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it, vi, beforeEach } from "vitest"; +import { resolveBrowserExecutableForPlatform } from "./chrome.executables.js"; vi.mock("node:child_process", () => ({ execFileSync: vi.fn(), @@ -17,11 +18,10 @@ import * as fs from "node:fs"; describe("browser default executable detection", () => { beforeEach(() => { - vi.resetModules(); vi.clearAllMocks(); }); - it("prefers default Chromium browser on macOS", async () => { + it("prefers default Chromium browser on macOS", () => { vi.mocked(execFileSync).mockImplementation((cmd, args) => { const argsStr = Array.isArray(args) ? args.join(" ") : ""; if (cmd === "/usr/bin/plutil" && argsStr.includes("LSHandlers")) { @@ -45,7 +45,6 @@ describe("browser default executable detection", () => { return value.includes("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"); }); - const { resolveBrowserExecutableForPlatform } = await import("./chrome.executables.js"); const exe = resolveBrowserExecutableForPlatform( {} as Parameters[0], "darwin", @@ -55,7 +54,7 @@ describe("browser default executable detection", () => { expect(exe?.kind).toBe("chrome"); }); - it("falls back when default browser is non-Chromium on macOS", async () => { + it("falls back when default browser is non-Chromium on macOS", () => { vi.mocked(execFileSync).mockImplementation((cmd, args) => { const argsStr = Array.isArray(args) ? args.join(" ") : ""; if (cmd === "/usr/bin/plutil" && argsStr.includes("LSHandlers")) { @@ -73,7 +72,6 @@ describe("browser default executable detection", () => { return value.includes("Google Chrome.app/Contents/MacOS/Google Chrome"); }); - const { resolveBrowserExecutableForPlatform } = await import("./chrome.executables.js"); const exe = resolveBrowserExecutableForPlatform( {} as Parameters[0], "darwin", diff --git a/src/canvas-host/server.state-dir.test.ts b/src/canvas-host/server.state-dir.test.ts index 5953464bfd9..f5cc012e90e 100644 --- a/src/canvas-host/server.state-dir.test.ts +++ b/src/canvas-host/server.state-dir.test.ts @@ -1,13 +1,14 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { defaultRuntime } from "../runtime.js"; import { restoreStateDirEnv, setStateDirEnv, snapshotStateDirEnv, } from "../test-helpers/state-dir-env.js"; +import { createCanvasHostHandler } from "./server.js"; describe("canvas host state dir defaults", () => { let envSnapshot: ReturnType; @@ -17,7 +18,6 @@ describe("canvas host state dir defaults", () => { }); afterEach(() => { - vi.resetModules(); restoreStateDirEnv(envSnapshot); }); @@ -25,9 +25,6 @@ describe("canvas host state dir defaults", () => { const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-canvas-state-")); const stateDir = path.join(tempRoot, "state"); setStateDirEnv(stateDir); - vi.resetModules(); - - const { createCanvasHostHandler } = await import("./server.js"); const handler = await createCanvasHostHandler({ runtime: defaultRuntime, allowInTests: true, diff --git a/src/canvas-host/server.ts b/src/canvas-host/server.ts index a4df38fe126..65b0b83a4cd 100644 --- a/src/canvas-host/server.ts +++ b/src/canvas-host/server.ts @@ -7,7 +7,7 @@ import http, { type IncomingMessage, type Server, type ServerResponse } from "no import path from "node:path"; import { type WebSocket, WebSocketServer } from "ws"; import type { RuntimeEnv } from "../runtime.js"; -import { STATE_DIR } from "../config/paths.js"; +import { resolveStateDir } from "../config/paths.js"; import { isTruthyEnvValue } from "../infra/env.js"; import { SafeOpenError, openFileWithinRoot } from "../infra/fs-safe.js"; import { detectMime } from "../media/mime.js"; @@ -235,7 +235,7 @@ async function prepareCanvasRoot(rootDir: string) { } function resolveDefaultCanvasRoot(): string { - const candidates = [path.join(STATE_DIR, "canvas")]; + const candidates = [path.join(resolveStateDir(), "canvas")]; const existing = candidates.find((dir) => { try { return fsSync.statSync(dir).isDirectory(); diff --git a/src/gateway/client.maxpayload.test.ts b/src/gateway/client.maxpayload.test.ts index d7efb2fe79f..acd23da179e 100644 --- a/src/gateway/client.maxpayload.test.ts +++ b/src/gateway/client.maxpayload.test.ts @@ -1,31 +1,30 @@ import { describe, expect, test, vi } from "vitest"; +import { GatewayClient } from "./client.js"; + +const wsMockState = vi.hoisted(() => ({ + last: null as { url: unknown; opts: unknown } | null, +})); + +vi.mock("ws", () => ({ + WebSocket: class MockWebSocket { + on = vi.fn(); + close = vi.fn(); + send = vi.fn(); + + constructor(url: unknown, opts: unknown) { + wsMockState.last = { url, opts }; + } + }, +})); describe("GatewayClient", () => { - test("uses a large maxPayload for node snapshots", async () => { - vi.resetModules(); - - class MockWebSocket { - static last: { url: unknown; opts: unknown } | null = null; - - on = vi.fn(); - close = vi.fn(); - send = vi.fn(); - - constructor(url: unknown, opts: unknown) { - MockWebSocket.last = { url, opts }; - } - } - - vi.doMock("ws", () => ({ - WebSocket: MockWebSocket, - })); - - const { GatewayClient } = await import("./client.js"); + test("uses a large maxPayload for node snapshots", () => { + wsMockState.last = null; const client = new GatewayClient({ url: "ws://127.0.0.1:1" }); client.start(); - expect(MockWebSocket.last?.url).toBe("ws://127.0.0.1:1"); - expect(MockWebSocket.last?.opts).toEqual( + expect(wsMockState.last?.url).toBe("ws://127.0.0.1:1"); + expect(wsMockState.last?.opts).toEqual( expect.objectContaining({ maxPayload: 25 * 1024 * 1024 }), ); }); diff --git a/src/infra/device-identity.state-dir.test.ts b/src/infra/device-identity.state-dir.test.ts index f549c10ddec..a119616e60a 100644 --- a/src/infra/device-identity.state-dir.test.ts +++ b/src/infra/device-identity.state-dir.test.ts @@ -1,12 +1,13 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { restoreStateDirEnv, setStateDirEnv, snapshotStateDirEnv, } from "../test-helpers/state-dir-env.js"; +import { loadOrCreateDeviceIdentity } from "./device-identity.js"; describe("device identity state dir defaults", () => { let envSnapshot: ReturnType; @@ -16,7 +17,6 @@ describe("device identity state dir defaults", () => { }); afterEach(() => { - vi.resetModules(); restoreStateDirEnv(envSnapshot); }); @@ -24,9 +24,6 @@ describe("device identity state dir defaults", () => { const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-identity-state-")); const stateDir = path.join(tempRoot, "state"); setStateDirEnv(stateDir); - vi.resetModules(); - - const { loadOrCreateDeviceIdentity } = await import("./device-identity.js"); const identity = loadOrCreateDeviceIdentity(); try { diff --git a/src/infra/device-identity.ts b/src/infra/device-identity.ts index d152e26fedd..1fa1659c659 100644 --- a/src/infra/device-identity.ts +++ b/src/infra/device-identity.ts @@ -1,7 +1,7 @@ import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; -import { STATE_DIR } from "../config/paths.js"; +import { resolveStateDir } from "../config/paths.js"; export type DeviceIdentity = { deviceId: string; @@ -17,8 +17,9 @@ type StoredIdentity = { createdAtMs: number; }; -const DEFAULT_DIR = path.join(STATE_DIR, "identity"); -const DEFAULT_FILE = path.join(DEFAULT_DIR, "device.json"); +function resolveDefaultIdentityPath(): string { + return path.join(resolveStateDir(), "identity", "device.json"); +} function ensureDir(filePath: string) { fs.mkdirSync(path.dirname(filePath), { recursive: true }); @@ -61,7 +62,9 @@ function generateIdentity(): DeviceIdentity { return { deviceId, publicKeyPem, privateKeyPem }; } -export function loadOrCreateDeviceIdentity(filePath: string = DEFAULT_FILE): DeviceIdentity { +export function loadOrCreateDeviceIdentity( + filePath: string = resolveDefaultIdentityPath(), +): DeviceIdentity { try { if (fs.existsSync(filePath)) { const raw = fs.readFileSync(filePath, "utf8"); diff --git a/src/infra/heartbeat-wake.test.ts b/src/infra/heartbeat-wake.test.ts index 58d24556672..b3f8e0d32f7 100644 --- a/src/infra/heartbeat-wake.test.ts +++ b/src/infra/heartbeat-wake.test.ts @@ -1,27 +1,33 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; - -async function loadWakeModule() { - vi.resetModules(); - return import("./heartbeat-wake.js"); -} +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { + hasHeartbeatWakeHandler, + hasPendingHeartbeatWake, + requestHeartbeatNow, + resetHeartbeatWakeStateForTests, + setHeartbeatWakeHandler, +} from "./heartbeat-wake.js"; describe("heartbeat-wake", () => { + beforeEach(() => { + resetHeartbeatWakeStateForTests(); + }); + afterEach(() => { + resetHeartbeatWakeStateForTests(); vi.useRealTimers(); vi.restoreAllMocks(); }); it("coalesces multiple wake requests into one run", async () => { vi.useFakeTimers(); - const wake = await loadWakeModule(); const handler = vi.fn().mockResolvedValue({ status: "skipped", reason: "disabled" }); - wake.setHeartbeatWakeHandler(handler); + setHeartbeatWakeHandler(handler); - wake.requestHeartbeatNow({ reason: "interval", coalesceMs: 200 }); - wake.requestHeartbeatNow({ reason: "exec-event", coalesceMs: 200 }); - wake.requestHeartbeatNow({ reason: "retry", coalesceMs: 200 }); + requestHeartbeatNow({ reason: "interval", coalesceMs: 200 }); + requestHeartbeatNow({ reason: "exec-event", coalesceMs: 200 }); + requestHeartbeatNow({ reason: "retry", coalesceMs: 200 }); - expect(wake.hasPendingHeartbeatWake()).toBe(true); + expect(hasPendingHeartbeatWake()).toBe(true); await vi.advanceTimersByTimeAsync(199); expect(handler).not.toHaveBeenCalled(); @@ -29,19 +35,18 @@ describe("heartbeat-wake", () => { await vi.advanceTimersByTimeAsync(1); expect(handler).toHaveBeenCalledTimes(1); expect(handler).toHaveBeenCalledWith({ reason: "exec-event" }); - expect(wake.hasPendingHeartbeatWake()).toBe(false); + expect(hasPendingHeartbeatWake()).toBe(false); }); it("retries requests-in-flight after the default retry delay", async () => { vi.useFakeTimers(); - const wake = await loadWakeModule(); const handler = vi .fn() .mockResolvedValueOnce({ status: "skipped", reason: "requests-in-flight" }) .mockResolvedValueOnce({ status: "ran", durationMs: 1 }); - wake.setHeartbeatWakeHandler(handler); + setHeartbeatWakeHandler(handler); - wake.requestHeartbeatNow({ reason: "interval", coalesceMs: 0 }); + requestHeartbeatNow({ reason: "interval", coalesceMs: 0 }); await vi.advanceTimersByTimeAsync(1); expect(handler).toHaveBeenCalledTimes(1); @@ -56,19 +61,18 @@ describe("heartbeat-wake", () => { it("keeps retry cooldown even when a sooner request arrives", async () => { vi.useFakeTimers(); - const wake = await loadWakeModule(); const handler = vi .fn() .mockResolvedValueOnce({ status: "skipped", reason: "requests-in-flight" }) .mockResolvedValueOnce({ status: "ran", durationMs: 1 }); - wake.setHeartbeatWakeHandler(handler); + setHeartbeatWakeHandler(handler); - wake.requestHeartbeatNow({ reason: "interval", coalesceMs: 0 }); + requestHeartbeatNow({ reason: "interval", coalesceMs: 0 }); await vi.advanceTimersByTimeAsync(1); expect(handler).toHaveBeenCalledTimes(1); // Retry is now waiting for 1000ms. This should not preempt cooldown. - wake.requestHeartbeatNow({ reason: "hook:wake", coalesceMs: 0 }); + requestHeartbeatNow({ reason: "hook:wake", coalesceMs: 0 }); await vi.advanceTimersByTimeAsync(998); expect(handler).toHaveBeenCalledTimes(1); @@ -79,14 +83,13 @@ describe("heartbeat-wake", () => { it("retries thrown handler errors after the default retry delay", async () => { vi.useFakeTimers(); - const wake = await loadWakeModule(); const handler = vi .fn() .mockRejectedValueOnce(new Error("boom")) .mockResolvedValueOnce({ status: "skipped", reason: "disabled" }); - wake.setHeartbeatWakeHandler(handler); + setHeartbeatWakeHandler(handler); - wake.requestHeartbeatNow({ reason: "exec-event", coalesceMs: 0 }); + requestHeartbeatNow({ reason: "exec-event", coalesceMs: 0 }); await vi.advanceTimersByTimeAsync(1); expect(handler).toHaveBeenCalledTimes(1); @@ -101,42 +104,40 @@ describe("heartbeat-wake", () => { it("stale disposer does not clear a newer handler", async () => { vi.useFakeTimers(); - const wake = await loadWakeModule(); const handlerA = vi.fn().mockResolvedValue({ status: "ran", durationMs: 1 }); const handlerB = vi.fn().mockResolvedValue({ status: "ran", durationMs: 1 }); // Runner A registers its handler - const disposeA = wake.setHeartbeatWakeHandler(handlerA); + const disposeA = setHeartbeatWakeHandler(handlerA); // Runner B registers its handler (replaces A) - const disposeB = wake.setHeartbeatWakeHandler(handlerB); + const disposeB = setHeartbeatWakeHandler(handlerB); // Runner A's stale cleanup runs — should NOT clear handlerB disposeA(); - expect(wake.hasHeartbeatWakeHandler()).toBe(true); + expect(hasHeartbeatWakeHandler()).toBe(true); // handlerB should still work - wake.requestHeartbeatNow({ reason: "interval", coalesceMs: 0 }); + requestHeartbeatNow({ reason: "interval", coalesceMs: 0 }); await vi.advanceTimersByTimeAsync(1); expect(handlerB).toHaveBeenCalledTimes(1); expect(handlerA).not.toHaveBeenCalled(); // Runner B's dispose should work disposeB(); - expect(wake.hasHeartbeatWakeHandler()).toBe(false); + expect(hasHeartbeatWakeHandler()).toBe(false); }); it("preempts existing timer when a sooner schedule is requested", async () => { vi.useFakeTimers(); - const wake = await loadWakeModule(); const handler = vi.fn().mockResolvedValue({ status: "ran", durationMs: 1 }); - wake.setHeartbeatWakeHandler(handler); + setHeartbeatWakeHandler(handler); // Schedule for 5 seconds from now - wake.requestHeartbeatNow({ reason: "slow", coalesceMs: 5000 }); + requestHeartbeatNow({ reason: "slow", coalesceMs: 5000 }); // Schedule for 100ms from now — should preempt the 5s timer - wake.requestHeartbeatNow({ reason: "fast", coalesceMs: 100 }); + requestHeartbeatNow({ reason: "fast", coalesceMs: 100 }); await vi.advanceTimersByTimeAsync(100); expect(handler).toHaveBeenCalledTimes(1); @@ -146,15 +147,14 @@ describe("heartbeat-wake", () => { it("keeps existing timer when later schedule is requested", async () => { vi.useFakeTimers(); - const wake = await loadWakeModule(); const handler = vi.fn().mockResolvedValue({ status: "ran", durationMs: 1 }); - wake.setHeartbeatWakeHandler(handler); + setHeartbeatWakeHandler(handler); // Schedule for 100ms from now - wake.requestHeartbeatNow({ reason: "fast", coalesceMs: 100 }); + requestHeartbeatNow({ reason: "fast", coalesceMs: 100 }); // Schedule for 5 seconds from now — should NOT preempt - wake.requestHeartbeatNow({ reason: "slow", coalesceMs: 5000 }); + requestHeartbeatNow({ reason: "slow", coalesceMs: 5000 }); await vi.advanceTimersByTimeAsync(100); expect(handler).toHaveBeenCalledTimes(1); @@ -162,12 +162,11 @@ describe("heartbeat-wake", () => { it("does not downgrade a higher-priority pending reason", async () => { vi.useFakeTimers(); - const wake = await loadWakeModule(); const handler = vi.fn().mockResolvedValue({ status: "ran", durationMs: 1 }); - wake.setHeartbeatWakeHandler(handler); + setHeartbeatWakeHandler(handler); - wake.requestHeartbeatNow({ reason: "exec-event", coalesceMs: 100 }); - wake.requestHeartbeatNow({ reason: "retry", coalesceMs: 100 }); + requestHeartbeatNow({ reason: "exec-event", coalesceMs: 100 }); + requestHeartbeatNow({ reason: "retry", coalesceMs: 100 }); await vi.advanceTimersByTimeAsync(100); expect(handler).toHaveBeenCalledTimes(1); @@ -176,14 +175,13 @@ describe("heartbeat-wake", () => { it("drains pending wake once a handler is registered", async () => { vi.useFakeTimers(); - const wake = await loadWakeModule(); - wake.requestHeartbeatNow({ reason: "manual", coalesceMs: 0 }); + requestHeartbeatNow({ reason: "manual", coalesceMs: 0 }); await vi.advanceTimersByTimeAsync(1); - expect(wake.hasPendingHeartbeatWake()).toBe(true); + expect(hasPendingHeartbeatWake()).toBe(true); const handler = vi.fn().mockResolvedValue({ status: "skipped", reason: "disabled" }); - wake.setHeartbeatWakeHandler(handler); + setHeartbeatWakeHandler(handler); await vi.advanceTimersByTimeAsync(249); expect(handler).not.toHaveBeenCalled(); @@ -191,6 +189,6 @@ describe("heartbeat-wake", () => { await vi.advanceTimersByTimeAsync(1); expect(handler).toHaveBeenCalledTimes(1); expect(handler).toHaveBeenCalledWith({ reason: "manual" }); - expect(wake.hasPendingHeartbeatWake()).toBe(false); + expect(hasPendingHeartbeatWake()).toBe(false); }); }); diff --git a/src/infra/heartbeat-wake.ts b/src/infra/heartbeat-wake.ts index 2bdbc747f43..72f97378f67 100644 --- a/src/infra/heartbeat-wake.ts +++ b/src/infra/heartbeat-wake.ts @@ -173,3 +173,17 @@ export function hasHeartbeatWakeHandler() { export function hasPendingHeartbeatWake() { return pendingWake !== null || Boolean(timer) || scheduled; } + +export function resetHeartbeatWakeStateForTests() { + if (timer) { + clearTimeout(timer); + } + timer = null; + timerDueAt = null; + timerKind = null; + pendingWake = null; + scheduled = false; + running = false; + handlerGeneration += 1; + handler = null; +} diff --git a/src/infra/provider-usage.auth.normalizes-keys.test.ts b/src/infra/provider-usage.auth.normalizes-keys.test.ts index 1b1edb579ae..079f9014a62 100644 --- a/src/infra/provider-usage.auth.normalizes-keys.test.ts +++ b/src/infra/provider-usage.auth.normalizes-keys.test.ts @@ -2,14 +2,12 @@ import fs from "node:fs/promises"; import path from "node:path"; import { describe, expect, it, vi } from "vitest"; import { withTempHome } from "../../test/helpers/temp-home.js"; +import { resolveProviderAuths } from "./provider-usage.auth.js"; describe("resolveProviderAuths key normalization", () => { it("strips embedded CR/LF from env keys", async () => { await withTempHome( async () => { - vi.resetModules(); - const { resolveProviderAuths } = await import("./provider-usage.auth.js"); - const auths = await resolveProviderAuths({ providers: ["zai", "minimax", "xiaomi"], }); @@ -50,9 +48,6 @@ describe("resolveProviderAuths key normalization", () => { "utf8", ); - vi.resetModules(); - const { resolveProviderAuths } = await import("./provider-usage.auth.js"); - const auths = await resolveProviderAuths({ providers: ["minimax", "xiaomi"], }); diff --git a/src/plugins/discovery.test.ts b/src/plugins/discovery.test.ts index 54d046699e5..fd286ea8c22 100644 --- a/src/plugins/discovery.test.ts +++ b/src/plugins/discovery.test.ts @@ -3,6 +3,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; +import { discoverOpenClawPlugins } from "./discovery.js"; const tempDirs: string[] = []; @@ -18,7 +19,6 @@ async function withStateDir(stateDir: string, fn: () => Promise) { const prevBundled = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR; process.env.OPENCLAW_STATE_DIR = stateDir; process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins"; - vi.resetModules(); try { return await fn(); } finally { @@ -32,7 +32,6 @@ async function withStateDir(stateDir: string, fn: () => Promise) { } else { process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = prevBundled; } - vi.resetModules(); } } @@ -60,7 +59,6 @@ describe("discoverOpenClawPlugins", () => { fs.writeFileSync(path.join(workspaceExt, "beta.ts"), "export default function () {}", "utf-8"); const { candidates } = await withStateDir(stateDir, async () => { - const { discoverOpenClawPlugins } = await import("./discovery.js"); return discoverOpenClawPlugins({ workspaceDir }); }); @@ -94,7 +92,6 @@ describe("discoverOpenClawPlugins", () => { ); const { candidates } = await withStateDir(stateDir, async () => { - const { discoverOpenClawPlugins } = await import("./discovery.js"); return discoverOpenClawPlugins({}); }); @@ -123,7 +120,6 @@ describe("discoverOpenClawPlugins", () => { ); const { candidates } = await withStateDir(stateDir, async () => { - const { discoverOpenClawPlugins } = await import("./discovery.js"); return discoverOpenClawPlugins({}); }); @@ -147,7 +143,6 @@ describe("discoverOpenClawPlugins", () => { fs.writeFileSync(path.join(packDir, "index.js"), "module.exports = {}", "utf-8"); const { candidates } = await withStateDir(stateDir, async () => { - const { discoverOpenClawPlugins } = await import("./discovery.js"); return discoverOpenClawPlugins({ extraPaths: [packDir] }); }); diff --git a/src/telegram/fetch.test.ts b/src/telegram/fetch.test.ts index c96966f351d..285e189ff1a 100644 --- a/src/telegram/fetch.test.ts +++ b/src/telegram/fetch.test.ts @@ -1,19 +1,22 @@ import { afterEach, describe, expect, it, vi } from "vitest"; +import { resetTelegramFetchStateForTests, resolveTelegramFetch } from "./fetch.js"; + +const setDefaultAutoSelectFamily = vi.hoisted(() => vi.fn()); + +vi.mock("node:net", async () => { + const actual = await vi.importActual("node:net"); + return { + ...actual, + setDefaultAutoSelectFamily, + }; +}); describe("resolveTelegramFetch", () => { const originalFetch = globalThis.fetch; - const loadModule = async () => { - const setDefaultAutoSelectFamily = vi.fn(); - vi.resetModules(); - vi.doMock("node:net", () => ({ - setDefaultAutoSelectFamily, - })); - const mod = await import("./fetch.js"); - return { resolveTelegramFetch: mod.resolveTelegramFetch, setDefaultAutoSelectFamily }; - }; - afterEach(() => { + resetTelegramFetchStateForTests(); + setDefaultAutoSelectFamily.mockReset(); vi.unstubAllEnvs(); vi.clearAllMocks(); if (originalFetch) { @@ -26,14 +29,12 @@ describe("resolveTelegramFetch", () => { it("returns wrapped global fetch when available", async () => { const fetchMock = vi.fn(async () => ({})); globalThis.fetch = fetchMock as unknown as typeof fetch; - const { resolveTelegramFetch } = await loadModule(); const resolved = resolveTelegramFetch(); expect(resolved).toBeTypeOf("function"); }); it("prefers proxy fetch when provided", async () => { const fetchMock = vi.fn(async () => ({})); - const { resolveTelegramFetch } = await loadModule(); const resolved = resolveTelegramFetch(fetchMock as unknown as typeof fetch); expect(resolved).toBeTypeOf("function"); }); @@ -41,14 +42,12 @@ describe("resolveTelegramFetch", () => { it("honors env enable override", async () => { vi.stubEnv("OPENCLAW_TELEGRAM_ENABLE_AUTO_SELECT_FAMILY", "1"); globalThis.fetch = vi.fn(async () => ({})) as unknown as typeof fetch; - const { resolveTelegramFetch, setDefaultAutoSelectFamily } = await loadModule(); resolveTelegramFetch(); expect(setDefaultAutoSelectFamily).toHaveBeenCalledWith(true); }); it("uses config override when provided", async () => { globalThis.fetch = vi.fn(async () => ({})) as unknown as typeof fetch; - const { resolveTelegramFetch, setDefaultAutoSelectFamily } = await loadModule(); resolveTelegramFetch(undefined, { network: { autoSelectFamily: true } }); expect(setDefaultAutoSelectFamily).toHaveBeenCalledWith(true); }); @@ -57,7 +56,6 @@ describe("resolveTelegramFetch", () => { vi.stubEnv("OPENCLAW_TELEGRAM_ENABLE_AUTO_SELECT_FAMILY", "0"); vi.stubEnv("OPENCLAW_TELEGRAM_DISABLE_AUTO_SELECT_FAMILY", "1"); globalThis.fetch = vi.fn(async () => ({})) as unknown as typeof fetch; - const { resolveTelegramFetch, setDefaultAutoSelectFamily } = await loadModule(); resolveTelegramFetch(undefined, { network: { autoSelectFamily: true } }); expect(setDefaultAutoSelectFamily).toHaveBeenCalledWith(false); }); diff --git a/src/telegram/fetch.ts b/src/telegram/fetch.ts index 96cb092772d..c82a1180a27 100644 --- a/src/telegram/fetch.ts +++ b/src/telegram/fetch.ts @@ -42,3 +42,7 @@ export function resolveTelegramFetch( } return fetchImpl; } + +export function resetTelegramFetchStateForTests(): void { + appliedAutoSelectFamily = null; +}