diff --git a/src/agents/model-catalog-browse.test.ts b/src/agents/model-catalog-browse.test.ts index e03a99d6a5ac..5ec621f096d3 100644 --- a/src/agents/model-catalog-browse.test.ts +++ b/src/agents/model-catalog-browse.test.ts @@ -6,13 +6,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { MAX_TIMER_TIMEOUT_MS } from "../shared/number-coercion.js"; import { - DEFAULT_MODEL_CATALOG_BROWSE_TIMEOUT_MS, loadModelCatalogForBrowse, - restoreModelCatalogBrowseTestDeps, - setModelCatalogBrowseTestDeps, } from "./model-catalog-browse.js"; import type { ModelCatalogEntry } from "./model-catalog.types.js"; +const DEFAULT_MODEL_CATALOG_BROWSE_TIMEOUT_MS = 750; const readOnlyCatalog: ModelCatalogEntry[] = [ { id: "gpt-readonly", name: "GPT Readonly", provider: "openai" }, ]; @@ -41,7 +39,6 @@ describe("loadModelCatalogForBrowse", () => { vi.clearAllTimers(); vi.restoreAllMocks(); vi.useRealTimers(); - restoreModelCatalogBrowseTestDeps(); }); it("uses the read-only catalog for default browse views", async () => { @@ -97,16 +94,8 @@ describe("loadModelCatalogForBrowse", () => { }); it("returns an empty catalog when read-only catalog loading times out with provider wildcards", async () => { + vi.useFakeTimers(); const onTimeout = vi.fn(); - const timeoutHandle = { unref: vi.fn() } as unknown as NodeJS.Timeout; - const clearTimeout = vi.fn(); - setModelCatalogBrowseTestDeps({ - setTimeout: vi.fn((callback: () => void) => { - queueMicrotask(callback); - return timeoutHandle; - }) as unknown as typeof globalThis.setTimeout, - clearTimeout: clearTimeout as unknown as typeof globalThis.clearTimeout, - }); const loadCatalog = vi.fn(() => new Promise(() => {})); const resultPromise = loadModelCatalogForBrowse({ @@ -116,21 +105,15 @@ describe("loadModelCatalogForBrowse", () => { onTimeout, }); + await vi.advanceTimersByTimeAsync(5); await expect(resultPromise).resolves.toEqual([]); expect(onTimeout).toHaveBeenCalledExactlyOnceWith(5); - expect(timeoutHandle["unref"]).toHaveBeenCalledOnce(); - expect(clearTimeout).toHaveBeenCalledExactlyOnceWith(timeoutHandle); }); it("uses the default timeout when timeoutMs is non-finite", async () => { const onTimeout = vi.fn(); - const timeoutHandle = { unref: vi.fn() } as unknown as NodeJS.Timeout; - const setTimeout = vi.fn(() => timeoutHandle); - const clearTimeout = vi.fn(); - setModelCatalogBrowseTestDeps({ - setTimeout: setTimeout as unknown as typeof globalThis.setTimeout, - clearTimeout: clearTimeout as unknown as typeof globalThis.clearTimeout, - }); + const setTimeout = vi.spyOn(globalThis, "setTimeout"); + const clearTimeout = vi.spyOn(globalThis, "clearTimeout"); const loadCatalog = vi.fn(async () => readOnlyCatalog); const resultPromise = loadModelCatalogForBrowse({ @@ -145,18 +128,13 @@ describe("loadModelCatalogForBrowse", () => { expect.any(Function), DEFAULT_MODEL_CATALOG_BROWSE_TIMEOUT_MS, ); - expect(clearTimeout).toHaveBeenCalledExactlyOnceWith(timeoutHandle); + expect(clearTimeout).toHaveBeenCalledOnce(); expect(onTimeout).not.toHaveBeenCalled(); }); it("caps oversized browse timeouts before scheduling the fallback timer", async () => { - const timeoutHandle = { unref: vi.fn() } as unknown as NodeJS.Timeout; - const setTimeout = vi.fn(() => timeoutHandle); - const clearTimeout = vi.fn(); - setModelCatalogBrowseTestDeps({ - setTimeout: setTimeout as unknown as typeof globalThis.setTimeout, - clearTimeout: clearTimeout as unknown as typeof globalThis.clearTimeout, - }); + const setTimeout = vi.spyOn(globalThis, "setTimeout"); + const clearTimeout = vi.spyOn(globalThis, "clearTimeout"); const loadCatalog = vi.fn(async () => readOnlyCatalog); const resultPromise = loadModelCatalogForBrowse({ @@ -167,6 +145,6 @@ describe("loadModelCatalogForBrowse", () => { await expect(resultPromise).resolves.toBe(readOnlyCatalog); expect(setTimeout).toHaveBeenCalledExactlyOnceWith(expect.any(Function), MAX_TIMER_TIMEOUT_MS); - expect(clearTimeout).toHaveBeenCalledExactlyOnceWith(timeoutHandle); + expect(clearTimeout).toHaveBeenCalledOnce(); }); }); diff --git a/src/agents/model-catalog-browse.ts b/src/agents/model-catalog-browse.ts index 37ba09c39681..3d7c93b82867 100644 --- a/src/agents/model-catalog-browse.ts +++ b/src/agents/model-catalog-browse.ts @@ -13,29 +13,11 @@ import { parseConfiguredModelVisibilityEntries } from "./model-selection-shared. * Loads the model catalog shape used by browse/list commands without letting optional * provider discovery stall the CLI path. */ -export const DEFAULT_MODEL_CATALOG_BROWSE_TIMEOUT_MS = 750; +const DEFAULT_MODEL_CATALOG_BROWSE_TIMEOUT_MS = 750; /** Visible model subset requested by model browse callers. */ export type ModelCatalogBrowseView = "default" | "configured" | "all"; -const modelCatalogBrowseDeps = { - setTimeout: globalThis.setTimeout, - clearTimeout: globalThis.clearTimeout, -}; - -/** Replaces timer hooks for deterministic timeout tests. */ -export function setModelCatalogBrowseTestDeps( - overrides: Partial, -): void { - Object.assign(modelCatalogBrowseDeps, overrides); -} - -/** Restores global timer hooks after catalog browse timeout tests. */ -export function restoreModelCatalogBrowseTestDeps(): void { - modelCatalogBrowseDeps.setTimeout = globalThis.setTimeout; - modelCatalogBrowseDeps.clearTimeout = globalThis.clearTimeout; -} - /** True when a browse view cannot be answered from read-only cached catalog entries. */ export function modelCatalogBrowseRequiresFullDiscovery(params: { cfg: OpenClawConfig; @@ -74,7 +56,7 @@ export async function loadModelCatalogForBrowse(params: { const timedOut = Symbol("model-catalog-browse-timeout"); const catalogPromise = params.loadCatalog({ readOnly: true }); const timeoutPromise = new Promise((resolve) => { - timeout = modelCatalogBrowseDeps.setTimeout(() => resolve(timedOut), timeoutMs); + timeout = globalThis.setTimeout(() => resolve(timedOut), timeoutMs); timeout.unref?.(); }); @@ -89,7 +71,7 @@ export async function loadModelCatalogForBrowse(params: { return result; } finally { if (timeout) { - modelCatalogBrowseDeps.clearTimeout(timeout); + globalThis.clearTimeout(timeout); } } }