diff --git a/src/gateway/server-methods/models.test.ts b/src/gateway/server-methods/models.test.ts index 1d79f800320..eb452de49e0 100644 --- a/src/gateway/server-methods/models.test.ts +++ b/src/gateway/server-methods/models.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it, vi } from "vitest"; import { ErrorCodes } from "../../../packages/gateway-protocol/src/index.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { modelsHandlers } from "./models.js"; +import type { RespondFn } from "./types.js"; type Deferred = { promise: Promise; @@ -22,44 +23,58 @@ function createDeferred(): Deferred { return { promise, resolve, reject }; } +function requestModelsList(params: { + view: "configured" | "all"; + respond?: ReturnType; + runtimeConfig?: OpenClawConfig; + loadGatewayModelCatalog: () => Promise>>; + reqId?: string; +}) { + const respond = params.respond ?? vi.fn(); + const request = modelsHandlers["models.list"]({ + req: { + type: "req", + id: params.reqId ?? `req-models-list-${params.view}`, + method: "models.list", + params: { view: params.view }, + }, + params: { view: params.view }, + respond: respond as RespondFn, + client: null, + isWebchatConnect: () => false, + context: { + getRuntimeConfig: () => params.runtimeConfig ?? ({} as OpenClawConfig), + loadGatewayModelCatalog: params.loadGatewayModelCatalog, + logGateway: { + debug: vi.fn(), + }, + } as never, + }); + return { request, respond }; +} + describe("models.list", () => { it("does not block the configured view on slow model catalog discovery", async () => { const catalog = createDeferred(); - const respond = vi.fn(); const loadGatewayModelCatalog = vi.fn(() => catalog.promise); + const runtimeConfig = { + models: { + providers: { + openai: { + baseUrl: "https://openai.example.com", + models: [{ id: "gpt-test", name: "GPT Test" }], + }, + }, + }, + } as unknown as OpenClawConfig; vi.useFakeTimers(); try { - const request = modelsHandlers["models.list"]({ - req: { - type: "req", - id: "req-models-list-slow-catalog", - method: "models.list", - params: { view: "configured" }, - }, - params: { view: "configured" }, - respond, - client: null, - isWebchatConnect: () => false, - context: { - getRuntimeConfig: () => { - const config = { - models: { - providers: { - openai: { - baseUrl: "https://openai.example.com", - models: [{ id: "gpt-test", name: "GPT Test" }], - }, - }, - }, - }; - return config as unknown as OpenClawConfig; - }, - loadGatewayModelCatalog, - logGateway: { - debug: vi.fn(), - }, - } as never, + const { request, respond } = requestModelsList({ + view: "configured", + runtimeConfig, + loadGatewayModelCatalog, + reqId: "req-models-list-slow-catalog", }); await vi.advanceTimersByTimeAsync(800); @@ -86,29 +101,14 @@ describe("models.list", () => { it("keeps the all view exact instead of timing out to a partial catalog", async () => { const catalog = createDeferred<[{ id: string; name: string; provider: string }]>(); - const respond = vi.fn(); const loadGatewayModelCatalog = vi.fn(() => catalog.promise); vi.useFakeTimers(); try { - const request = modelsHandlers["models.list"]({ - req: { - type: "req", - id: "req-models-list-all-slow-catalog", - method: "models.list", - params: { view: "all" }, - }, - params: { view: "all" }, - respond, - client: null, - isWebchatConnect: () => false, - context: { - getRuntimeConfig: () => ({}) as OpenClawConfig, - loadGatewayModelCatalog, - logGateway: { - debug: vi.fn(), - }, - } as never, + const { request, respond } = requestModelsList({ + view: "all", + loadGatewayModelCatalog, + reqId: "req-models-list-all-slow-catalog", }); await vi.advanceTimersByTimeAsync(800); @@ -129,35 +129,21 @@ describe("models.list", () => { }); it("does not expose runtime params from catalog rows", async () => { - const respond = vi.fn(); - await modelsHandlers["models.list"]({ - req: { - type: "req", - id: "req-models-list-redact-params", - method: "models.list", - params: { view: "all" }, - }, - params: { view: "all" }, - respond, - client: null, - isWebchatConnect: () => false, - context: { - getRuntimeConfig: () => ({}) as OpenClawConfig, - loadGatewayModelCatalog: vi.fn(() => - Promise.resolve([ - { - id: "qwen-local", - name: "Qwen Local", - provider: "vllm", - params: { qwenThinkingFormat: "chat-template" }, - }, - ]), - ), - logGateway: { - debug: vi.fn(), - }, - } as never, + const { request, respond } = requestModelsList({ + view: "all", + loadGatewayModelCatalog: vi.fn(() => + Promise.resolve([ + { + id: "qwen-local", + name: "Qwen Local", + provider: "vllm", + params: { qwenThinkingFormat: "chat-template" }, + }, + ]), + ), + reqId: "req-models-list-redact-params", }); + await request; expect(respond).toHaveBeenCalledWith( true, @@ -191,27 +177,14 @@ describe("models.list", () => { }, } as unknown as OpenClawConfig; - const configuredRespond = vi.fn(); const loadConfiguredCatalog = vi.fn(() => Promise.resolve(catalog)); - await modelsHandlers["models.list"]({ - req: { - type: "req", - id: "req-models-list-provider-allowlist", - method: "models.list", - params: { view: "configured" }, - }, - params: { view: "configured" }, - respond: configuredRespond, - client: null, - isWebchatConnect: () => false, - context: { - getRuntimeConfig: () => cfg, - loadGatewayModelCatalog: loadConfiguredCatalog, - logGateway: { - debug: vi.fn(), - }, - } as never, + const { request: configuredRequest, respond: configuredRespond } = requestModelsList({ + view: "configured", + runtimeConfig: cfg, + loadGatewayModelCatalog: loadConfiguredCatalog, + reqId: "req-models-list-provider-allowlist", }); + await configuredRequest; expect(configuredRespond).toHaveBeenCalledWith( true, @@ -227,52 +200,24 @@ describe("models.list", () => { ); expect(loadConfiguredCatalog).toHaveBeenCalledWith({ readOnly: false }); - const allRespond = vi.fn(); - await modelsHandlers["models.list"]({ - req: { - type: "req", - id: "req-models-list-provider-allowlist-all", - method: "models.list", - params: { view: "all" }, - }, - params: { view: "all" }, - respond: allRespond, - client: null, - isWebchatConnect: () => false, - context: { - getRuntimeConfig: () => cfg, - loadGatewayModelCatalog: vi.fn(() => Promise.resolve(catalog)), - logGateway: { - debug: vi.fn(), - }, - } as never, + const { request: allRequest, respond: allRespond } = requestModelsList({ + view: "all", + runtimeConfig: cfg, + loadGatewayModelCatalog: vi.fn(() => Promise.resolve(catalog)), + reqId: "req-models-list-provider-allowlist-all", }); + await allRequest; expect(allRespond).toHaveBeenCalledWith(true, { models: catalog }, undefined); }); it("preserves catalog load errors before the timeout fallback wins", async () => { - const respond = vi.fn(); - - await modelsHandlers["models.list"]({ - req: { - type: "req", - id: "req-models-list-catalog-error", - method: "models.list", - params: { view: "configured" }, - }, - params: { view: "configured" }, - respond, - client: null, - isWebchatConnect: () => false, - context: { - getRuntimeConfig: () => ({}) as OpenClawConfig, - loadGatewayModelCatalog: vi.fn(() => Promise.reject(new Error("catalog failed"))), - logGateway: { - debug: vi.fn(), - }, - } as never, + const { request, respond } = requestModelsList({ + view: "configured", + loadGatewayModelCatalog: vi.fn(() => Promise.reject(new Error("catalog failed"))), + reqId: "req-models-list-catalog-error", }); + await request; const call = respond.mock.calls.at(0) as | [boolean, unknown, { code?: number; message?: string }]