From b76df1fa8dc935a9350a0cc5a096ca55d7ed4bd2 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 9 May 2026 19:49:50 +0100 Subject: [PATCH] test: tighten model catalog assertions --- src/agents/model-catalog.test.ts | 155 ++++++++++++++----------------- 1 file changed, 70 insertions(+), 85 deletions(-) diff --git a/src/agents/model-catalog.test.ts b/src/agents/model-catalog.test.ts index e02318889fb..16d59d44aa3 100644 --- a/src/agents/model-catalog.test.ts +++ b/src/agents/model-catalog.test.ts @@ -105,6 +105,45 @@ function emptyPluginMetadataSnapshot() { }; } +type ModelCatalogEntry = Awaited< + ReturnType +>[number]; + +function findCatalogEntry( + entries: readonly ModelCatalogEntry[], + provider: string, + id: string, +): ModelCatalogEntry | undefined { + return entries.find((entry) => entry.provider === provider && entry.id === id); +} + +function requireCatalogEntry( + entries: readonly ModelCatalogEntry[], + provider: string, + id: string, +): ModelCatalogEntry { + const entry = findCatalogEntry(entries, provider, id); + if (!entry) { + throw new Error(`expected catalog entry ${provider}/${id}`); + } + return entry; +} + +function expectNoCatalogEntry(entries: readonly ModelCatalogEntry[], provider: string, id: string) { + expect(findCatalogEntry(entries, provider, id)).toBeUndefined(); +} + +function requireMockCallParam( + mock: ReturnType, + label: string, +): Record { + const first = mock.mock.calls[0]?.[0]; + if (!first || typeof first !== "object" || Array.isArray(first)) { + throw new Error(`expected ${label}`); + } + return first as Record; +} + describe("loadModelCatalog", () => { beforeAll(async () => { readFileMock = vi.fn(); @@ -278,9 +317,8 @@ describe("loadModelCatalog", () => { readOnly: true, }); - expect(result).toContainEqual( - expect.objectContaining({ id: "gpt-test", name: "GPT Test", provider: "openai" }), - ); + const entry = requireCatalogEntry(result, "openai", "gpt-test"); + expect(entry.name).toBe("GPT Test"); expect(ensureOpenClawModelsJsonMock).not.toHaveBeenCalled(); expect(importPiSdk).not.toHaveBeenCalled(); expect(loadPluginMetadataSnapshotMock).not.toHaveBeenCalled(); @@ -484,19 +522,9 @@ describe("loadModelCatalog", () => { ]); const result = await loadModelCatalog({ config: {} as OpenClawConfig }); - expect(result).not.toContainEqual( - expect.objectContaining({ - provider: "openai-codex", - id: "gpt-5.3-codex-spark", - }), - ); - expect(result).toContainEqual( - expect.objectContaining({ - provider: "openai-codex", - id: "gpt-5.4", - name: "GPT-5.3 Codex", - }), - ); + expectNoCatalogEntry(result, "openai-codex", "gpt-5.3-codex-spark"); + const entry = requireCatalogEntry(result, "openai-codex", "gpt-5.4"); + expect(entry.name).toBe("GPT-5.3 Codex"); }); it("filters stale gpt-5.3-codex-spark built-ins from the catalog", async () => { @@ -528,24 +556,9 @@ describe("loadModelCatalog", () => { ]); const result = await loadModelCatalog({ config: {} as OpenClawConfig }); - expect(result).not.toContainEqual( - expect.objectContaining({ - provider: "openai", - id: "gpt-5.3-codex-spark", - }), - ); - expect(result).not.toContainEqual( - expect.objectContaining({ - provider: "azure-openai-responses", - id: "gpt-5.3-codex-spark", - }), - ); - expect(result).not.toContainEqual( - expect.objectContaining({ - provider: "openai-codex", - id: "gpt-5.3-codex-spark", - }), - ); + expectNoCatalogEntry(result, "openai", "gpt-5.3-codex-spark"); + expectNoCatalogEntry(result, "azure-openai-responses", "gpt-5.3-codex-spark"); + expectNoCatalogEntry(result, "openai-codex", "gpt-5.3-codex-spark"); }); it("filters stale openai-codex 5.1/5.2/5.3 built-ins from the catalog", async () => { @@ -585,18 +598,10 @@ describe("loadModelCatalog", () => { ]); const result = await loadModelCatalog({ config: {} as OpenClawConfig }); - expect(result).not.toContainEqual( - expect.objectContaining({ provider: "openai-codex", id: "gpt-5.1-codex-mini" }), - ); - expect(result).not.toContainEqual( - expect.objectContaining({ provider: "openai-codex", id: "gpt-5.2-codex" }), - ); - expect(result).not.toContainEqual( - expect.objectContaining({ provider: "openai-codex", id: "gpt-5.3-codex" }), - ); - expect(result).toContainEqual( - expect.objectContaining({ provider: "openai-codex", id: "gpt-5.5" }), - ); + expectNoCatalogEntry(result, "openai-codex", "gpt-5.1-codex-mini"); + expectNoCatalogEntry(result, "openai-codex", "gpt-5.2-codex"); + expectNoCatalogEntry(result, "openai-codex", "gpt-5.3-codex"); + expect(requireCatalogEntry(result, "openai-codex", "gpt-5.5").name).toBe("GPT-5.5"); }); it("does not synthesize gpt-5.4 OpenAI forward-compat entries from template models", async () => { @@ -648,13 +653,8 @@ describe("loadModelCatalog", () => { expect( result.some((entry) => entry.provider === "openai" && entry.id.startsWith("gpt-5.4")), ).toBe(false); - expect(result).toContainEqual( - expect.objectContaining({ - provider: "openai-codex", - id: "gpt-5.4", - name: "GPT-5.3 Codex", - }), - ); + const entry = requireCatalogEntry(result, "openai-codex", "gpt-5.4"); + expect(entry.name).toBe("GPT-5.3 Codex"); expect( result.some((entry) => entry.provider === "openai-codex" && entry.id === "gpt-5.4-mini"), ).toBe(false); @@ -675,13 +675,8 @@ describe("loadModelCatalog", () => { const result = await loadModelCatalog({ config: {} as OpenClawConfig }); - expect(result).toContainEqual( - expect.objectContaining({ - provider: "kilocode", - id: "google/gemini-3.1-pro-preview", - name: "Gemini 3 Pro Preview", - }), - ); + const entry = requireCatalogEntry(result, "kilocode", "google/gemini-3.1-pro-preview"); + expect(entry.name).toBe("Gemini 3 Pro Preview"); }); it("loads manifest catalog rows from the current metadata snapshot without provider runtime", () => { @@ -743,11 +738,8 @@ describe("loadModelCatalog", () => { fallbackToMetadataScan: false, }); - expect(currentPluginMetadataSnapshotMock).toHaveBeenCalledWith( - expect.objectContaining({ - allowWorkspaceScopedSnapshot: true, - }), - ); + const call = requireMockCallParam(currentPluginMetadataSnapshotMock, "current snapshot params"); + expect(call.allowWorkspaceScopedSnapshot).toBe(true); expect(loadPluginMetadataSnapshotMock).not.toHaveBeenCalled(); }); @@ -760,12 +752,9 @@ describe("loadModelCatalog", () => { fallbackToMetadataScan: false, }); - expect(currentPluginMetadataSnapshotMock).toHaveBeenCalledWith( - expect.objectContaining({ - env, - allowWorkspaceScopedSnapshot: true, - }), - ); + const call = requireMockCallParam(currentPluginMetadataSnapshotMock, "current snapshot params"); + expect(call.env).toBe(env); + expect(call.allowWorkspaceScopedSnapshot).toBe(true); expect(loadPluginMetadataSnapshotMock).not.toHaveBeenCalled(); }); @@ -789,9 +778,7 @@ describe("loadModelCatalog", () => { const result = await loadModelCatalog({ config: {} as OpenClawConfig }); - expect(result).toContainEqual( - expect.objectContaining({ provider: "ollama", id: "llama3.2", name: "Llama 3.2" }), - ); + expect(requireCatalogEntry(result, "ollama", "llama3.2").name).toBe("Llama 3.2"); expect( result.filter((entry) => entry.provider === "openai" && entry.id === "gpt-4.1"), ).toHaveLength(1); @@ -823,16 +810,11 @@ describe("loadModelCatalog", () => { } as OpenClawConfig, }); - expect(result).toContainEqual( - expect.objectContaining({ - provider: "modelscope", - id: "Qwen/Qwen3.5-35B-A3B", - name: "Qwen3.5 35B", - input: ["text", "image"], - reasoning: true, - contextWindow: 128_000, - }), - ); + const entry = requireCatalogEntry(result, "modelscope", "Qwen/Qwen3.5-35B-A3B"); + expect(entry.name).toBe("Qwen3.5 35B"); + expect(entry.input).toEqual(["text", "image"]); + expect(entry.reasoning).toBe(true); + expect(entry.contextWindow).toBe(128_000); }); it("dedupes configured models against discovered provider aliases", async () => { @@ -863,7 +845,10 @@ describe("loadModelCatalog", () => { const matches = result.filter((entry) => findModelInCatalog([entry], "z-ai", "glm-5")); expect(matches).toHaveLength(1); - expect(matches[0]).toMatchObject({ provider: "z.ai", id: "glm-5", name: "GLM-5" }); + const match = matches[0]; + expect(match?.provider).toBe("z.ai"); + expect(match?.id).toBe("glm-5"); + expect(match?.name).toBe("GLM-5"); }); it("does not add unrelated models when provider plugins return nothing", async () => {