diff --git a/src/gateway/server-model-catalog.test.ts b/src/gateway/server-model-catalog.test.ts index e5c20045eafc..592d80e64a91 100644 --- a/src/gateway/server-model-catalog.test.ts +++ b/src/gateway/server-model-catalog.test.ts @@ -35,6 +35,39 @@ function model(id: string): GatewayModelChoice { const getConfig = () => ({}) as OpenClawConfig; +function createRefreshingCatalogLoader( + firstCatalog: GatewayModelChoice[], + secondCatalog: GatewayModelChoice[], +) { + return vi + .fn() + .mockResolvedValueOnce(firstCatalog) + .mockResolvedValueOnce(secondCatalog); +} + +async function expectCatalog( + loadModelCatalog: LoadModelCatalogForTest, + catalog: GatewayModelChoice[], + readOnly = true, +) { + await expect( + loadGatewayModelCatalog({ + getConfig, + loadModelCatalog, + ...(readOnly ? {} : { readOnly: false }), + }), + ).resolves.toBe(catalog); +} + +async function markStaleAndExpectPreviousCatalog( + loadModelCatalog: LoadModelCatalogForTest, + catalog: GatewayModelChoice[], +) { + markGatewayModelCatalogStaleForReload(); + await expectCatalog(loadModelCatalog, catalog); + await vi.waitFor(() => expect(loadModelCatalog).toHaveBeenCalledTimes(2)); +} + describe("loadGatewayModelCatalog", () => { beforeEach(async () => { await resetModelCatalogCacheForTest(); @@ -82,46 +115,26 @@ describe("loadGatewayModelCatalog", () => { it("caches an empty read-only catalog until reload marks it stale", async () => { const emptyCatalog: GatewayModelChoice[] = []; const freshCatalog = [model("gpt-5.5")]; - const loadModelCatalog = vi - .fn() - .mockResolvedValueOnce(emptyCatalog) - .mockResolvedValueOnce(freshCatalog); + const loadModelCatalog = createRefreshingCatalogLoader(emptyCatalog, freshCatalog); - await expect(loadGatewayModelCatalog({ getConfig, loadModelCatalog })).resolves.toBe( - emptyCatalog, - ); - await expect(loadGatewayModelCatalog({ getConfig, loadModelCatalog })).resolves.toBe( - emptyCatalog, - ); + await expectCatalog(loadModelCatalog, emptyCatalog); + await expectCatalog(loadModelCatalog, emptyCatalog); expect(loadModelCatalog).toHaveBeenCalledTimes(1); - markGatewayModelCatalogStaleForReload(); - await expect(loadGatewayModelCatalog({ getConfig, loadModelCatalog })).resolves.toBe( - emptyCatalog, - ); - await vi.waitFor(() => expect(loadModelCatalog).toHaveBeenCalledTimes(2)); + await markStaleAndExpectPreviousCatalog(loadModelCatalog, emptyCatalog); await vi.waitFor(async () => { - await expect(loadGatewayModelCatalog({ getConfig, loadModelCatalog })).resolves.toBe( - freshCatalog, - ); + await expectCatalog(loadModelCatalog, freshCatalog); }); }); it("does not cache an empty full catalog so the next all-model request retries", async () => { const emptyCatalog: GatewayModelChoice[] = []; const freshCatalog = [model("gpt-5.5")]; - const loadModelCatalog = vi - .fn() - .mockResolvedValueOnce(emptyCatalog) - .mockResolvedValueOnce(freshCatalog); + const loadModelCatalog = createRefreshingCatalogLoader(emptyCatalog, freshCatalog); - await expect( - loadGatewayModelCatalog({ getConfig, loadModelCatalog, readOnly: false }), - ).resolves.toBe(emptyCatalog); - await expect( - loadGatewayModelCatalog({ getConfig, loadModelCatalog, readOnly: false }), - ).resolves.toBe(freshCatalog); + await expectCatalog(loadModelCatalog, emptyCatalog, false); + await expectCatalog(loadModelCatalog, freshCatalog, false); expect(loadModelCatalog).toHaveBeenCalledTimes(2); }); @@ -135,21 +148,13 @@ describe("loadGatewayModelCatalog", () => { .mockResolvedValueOnce(staleCatalog) .mockReturnValueOnce(refresh.promise); - await expect(loadGatewayModelCatalog({ getConfig, loadModelCatalog })).resolves.toBe( - staleCatalog, - ); + await expectCatalog(loadModelCatalog, staleCatalog); - markGatewayModelCatalogStaleForReload(); - await expect(loadGatewayModelCatalog({ getConfig, loadModelCatalog })).resolves.toBe( - staleCatalog, - ); - await vi.waitFor(() => expect(loadModelCatalog).toHaveBeenCalledTimes(2)); + await markStaleAndExpectPreviousCatalog(loadModelCatalog, staleCatalog); refresh.resolve(freshCatalog); await vi.waitFor(async () => { - await expect(loadGatewayModelCatalog({ getConfig, loadModelCatalog })).resolves.toBe( - freshCatalog, - ); + await expectCatalog(loadModelCatalog, freshCatalog); }); }); @@ -162,25 +167,15 @@ describe("loadGatewayModelCatalog", () => { .mockRejectedValueOnce(new Error("provider offline")) .mockResolvedValueOnce(freshCatalog); - await expect(loadGatewayModelCatalog({ getConfig, loadModelCatalog })).resolves.toBe( - staleCatalog, - ); + await expectCatalog(loadModelCatalog, staleCatalog); - markGatewayModelCatalogStaleForReload(); - await expect(loadGatewayModelCatalog({ getConfig, loadModelCatalog })).resolves.toBe( - staleCatalog, - ); - await vi.waitFor(() => expect(loadModelCatalog).toHaveBeenCalledTimes(2)); + await markStaleAndExpectPreviousCatalog(loadModelCatalog, staleCatalog); - await expect(loadGatewayModelCatalog({ getConfig, loadModelCatalog })).resolves.toBe( - staleCatalog, - ); + await expectCatalog(loadModelCatalog, staleCatalog); await vi.waitFor(() => expect(loadModelCatalog).toHaveBeenCalledTimes(3)); await vi.waitFor(async () => { - await expect(loadGatewayModelCatalog({ getConfig, loadModelCatalog })).resolves.toBe( - freshCatalog, - ); + await expectCatalog(loadModelCatalog, freshCatalog); }); }); });