
























@@ -0,0 +1,107 @@
1+import { beforeEach, describe, expect, it, vi } from "vitest";
2+import type { OpenClawConfig } from "../config/types.openclaw.js";
3+import type { GatewayModelChoice } from "./server-model-catalog.js";
4+import {
5+__resetModelCatalogCacheForTest,
6+loadGatewayModelCatalog,
7+markGatewayModelCatalogStaleForReload,
8+} from "./server-model-catalog.js";
9+10+type Deferred<T> = {
11+promise: Promise<T>;
12+resolve: (value: T) => void;
13+reject: (error: unknown) => void;
14+};
15+type LoadModelCatalogForTest = NonNullable<
16+NonNullable<Parameters<typeof loadGatewayModelCatalog>[0]>["loadModelCatalog"]
17+>;
18+19+function createDeferred<T>(): Deferred<T> {
20+let resolve!: (value: T) => void;
21+let reject!: (error: unknown) => void;
22+const promise = new Promise<T>((resolvePromise, rejectPromise) => {
23+resolve = resolvePromise;
24+reject = rejectPromise;
25+});
26+return { promise, resolve, reject };
27+}
28+29+function model(id: string): GatewayModelChoice {
30+return { id, name: id, provider: "openai" } as GatewayModelChoice;
31+}
32+33+const getConfig = () => ({}) as OpenClawConfig;
34+35+describe("loadGatewayModelCatalog", () => {
36+beforeEach(async () => {
37+await __resetModelCatalogCacheForTest();
38+});
39+40+it("caches the first successful catalog until reload marks it stale", async () => {
41+const catalog = [model("gpt-5.4")];
42+const loadModelCatalog = vi.fn(async () => catalog);
43+44+await expect(loadGatewayModelCatalog({ getConfig, loadModelCatalog })).resolves.toBe(catalog);
45+await expect(loadGatewayModelCatalog({ getConfig, loadModelCatalog })).resolves.toBe(catalog);
46+47+expect(loadModelCatalog).toHaveBeenCalledTimes(1);
48+});
49+50+it("returns the last catalog while a stale reload refresh is still pending", async () => {
51+const staleCatalog = [model("gpt-5.4")];
52+const freshCatalog = [model("gpt-5.5")];
53+const refresh = createDeferred<GatewayModelChoice[]>();
54+const loadModelCatalog = vi
55+.fn<LoadModelCatalogForTest>()
56+.mockResolvedValueOnce(staleCatalog)
57+.mockReturnValueOnce(refresh.promise);
58+59+await expect(loadGatewayModelCatalog({ getConfig, loadModelCatalog })).resolves.toBe(
60+staleCatalog,
61+);
62+63+markGatewayModelCatalogStaleForReload();
64+await expect(loadGatewayModelCatalog({ getConfig, loadModelCatalog })).resolves.toBe(
65+staleCatalog,
66+);
67+await vi.waitFor(() => expect(loadModelCatalog).toHaveBeenCalledTimes(2));
68+69+refresh.resolve(freshCatalog);
70+await vi.waitFor(async () => {
71+await expect(loadGatewayModelCatalog({ getConfig, loadModelCatalog })).resolves.toBe(
72+freshCatalog,
73+);
74+});
75+});
76+77+it("keeps serving the last catalog when a stale background refresh fails", async () => {
78+const staleCatalog = [model("gpt-5.4")];
79+const freshCatalog = [model("gpt-5.5")];
80+const loadModelCatalog = vi
81+.fn<LoadModelCatalogForTest>()
82+.mockResolvedValueOnce(staleCatalog)
83+.mockRejectedValueOnce(new Error("provider offline"))
84+.mockResolvedValueOnce(freshCatalog);
85+86+await expect(loadGatewayModelCatalog({ getConfig, loadModelCatalog })).resolves.toBe(
87+staleCatalog,
88+);
89+90+markGatewayModelCatalogStaleForReload();
91+await expect(loadGatewayModelCatalog({ getConfig, loadModelCatalog })).resolves.toBe(
92+staleCatalog,
93+);
94+await vi.waitFor(() => expect(loadModelCatalog).toHaveBeenCalledTimes(2));
95+96+await expect(loadGatewayModelCatalog({ getConfig, loadModelCatalog })).resolves.toBe(
97+staleCatalog,
98+);
99+await vi.waitFor(() => expect(loadModelCatalog).toHaveBeenCalledTimes(3));
100+101+await vi.waitFor(async () => {
102+await expect(loadGatewayModelCatalog({ getConfig, loadModelCatalog })).resolves.toBe(
103+freshCatalog,
104+);
105+});
106+});
107+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。