diff --git a/packages/coding-agent/test/model-registry-runtime-provider.test.ts b/packages/coding-agent/test/model-registry-runtime-provider.test.ts index 6b2439409..467cc0ec8 100644 --- a/packages/coding-agent/test/model-registry-runtime-provider.test.ts +++ b/packages/coding-agent/test/model-registry-runtime-provider.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { afterEach, beforeEach, describe, expect, test, vi } from "bun:test"; import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; @@ -38,6 +38,7 @@ describe("ModelRegistry runtime provider registration", () => { }); afterEach(() => { + vi.useRealTimers(); clearCustomApis(); for (const sourceId of sourceIds) { unregisterOAuthProviders(sourceId); @@ -89,6 +90,14 @@ describe("ModelRegistry runtime provider registration", () => { expectProviderHeader(registry, providerName, headerName, expectedValue); } + async function drainMicrotasksUntil(predicate: () => boolean, errorMessage: string): Promise { + for (let i = 0; i < 1000; i++) { + if (predicate()) return; + await Promise.resolve(); + } + throw new Error(errorMessage); + } + async function expectModelTransportAcrossRefresh( registry: ModelRegistry, providerName: string, @@ -252,6 +261,46 @@ describe("ModelRegistry runtime provider registration", () => { }); }); + test("refreshRuntimeProviders times out extension fetchDynamicModels that never resolves", async () => { + vi.useFakeTimers(); + const hangingFetch = Promise.withResolvers[number][]>(); + registry.registerProvider( + "hanging-runtime-provider", + { + baseUrl: "https://runtime.example.com/v1", + apiKey: "RUNTIME_KEY", + api: "openai-completions", + fetchDynamicModels: () => hangingFetch.promise, + }, + "ext://runtime", + ); + + const baselineTimers = vi.getTimerCount(); + let outcome: "resolved" | "rejected" | undefined; + const refresh = registry.refreshRuntimeProviders("online").then( + () => { + outcome = "resolved"; + }, + error => { + outcome = "rejected"; + throw error; + }, + ); + + await drainMicrotasksUntil( + () => vi.getTimerCount() > baselineTimers, + "dynamic fetch timeout timer was not armed", + ); + expect(outcome).toBeUndefined(); + vi.advanceTimersByTime(14_999); + await Promise.resolve(); + expect(outcome).toBeUndefined(); + vi.advanceTimersByTime(1); + await refresh; + expect(outcome).toBe("resolved"); + expect(registry.find("hanging-runtime-provider", "any-model")).toBeUndefined(); + }); + test("registerProvider preserves explicit thinking and backfills wire facts", () => { const config: ProviderConfigInput = { baseUrl: "https://runtime.example.com/v1",