import { expect, it, vi } from "vitest"; import type { InstanceConfig } from "./contracts.ts"; import { discoverAgentEndpointModels, officialModels } from "./agent-endpoint-models.ts"; function endpoint(codex: boolean, url = "https://gateway.example/v1"): InstanceConfig { return codex ? { driver: "codex", config: { endpoint: { url, models: ["old"] } }, environment: { OPENMAUSBOT_CODEX_API_KEY: "fixture-secret" } } : { driver: "claudeAgent", config: { endpointModels: ["old"] }, environment: { ANTHROPIC_BASE_URL: url, ANTHROPIC_API_KEY: "fixture-secret" } }; } it.each([true, false])("discovers valid, unique per-agent model choices (Codex=%s)", async codex => { const entry = endpoint(codex); const request = vi.fn().mockResolvedValue(Response.json({ data: [{ id: "model-a" }, { id: "model-b" }, { id: "model-a" }, { id: "invalid model" }, {}] })); await discoverAgentEndpointModels(entry, request); expect(request).toHaveBeenCalledWith("https://gateway.example/v1/models", expect.objectContaining({ redirect: "manual", signal: expect.any(AbortSignal), headers: expect.objectContaining({ authorization: "Bearer fixture-secret", ...(codex ? {} : { "x-api-key": "fixture-secret", "anthropic-version": "2023-06-01" }) }) })); expect(entry.config).toMatchObject(codex ? { endpoint: { models: ["model-a", "model-b"] } } : { endpointModels: ["model-a", "model-b"] }); expect(entry.environment).not.toHaveProperty("ANTHROPIC_MODEL"); }); it.each([ [new Response("fixture-secret", { status: 401 }), "rejected the API key (HTTP 401)"], [new Response("fixture-secret", { status: 403 }), "rejected the API key (HTTP 403)"], [new Response("fixture-secret", { status: 404 }), "model catalog was not found (HTTP 404)"], [new Response("fixture-secret", { status: 302 }), "redirected the model request"], [new Response("fixture-secret", { status: 500 }), "model request failed (HTTP 500)"], [new Response("fixture-secret"), "did not return a JSON model catalog"], [Response.json({ data: [] }), "returned no supported model IDs"], ])("explains discovery failures without exposing gateway response bodies", async (response, detail) => { const entry = endpoint(true); const failure = await discoverAgentEndpointModels(entry, vi.fn().mockResolvedValue(response as Response)).catch(error => error); expect(failure.message).toContain(detail); expect(failure.message).not.toContain("fixture-secret"); expect(failure.status).toBe(400); expect(entry.config).toMatchObject({ endpoint: { models: ["old"] } }); }); it.each([ ["http://localhost:8317/v1", "localhost refers to OpenMausBot's server or container"], ["https://gateway.example/v1", "could not reach the gateway"], ])("explains network failures for %s without exposing raw exceptions", async (url, detail) => { const failure = await discoverAgentEndpointModels(endpoint(true, url), vi.fn().mockRejectedValue(new Error("fixture-secret"))).catch(error => error); expect(failure.message).toContain(detail); expect(failure.message).not.toContain("fixture-secret"); }); it("identifies a model discovery timeout", async () => { await expect(discoverAgentEndpointModels(endpoint(true), vi.fn().mockRejectedValue(new DOMException("fixture-secret", "TimeoutError")))) .rejects.toThrow("timed out"); }); it.each([true, false])("keeps the standard catalog for official connections (Codex=%s)", async codex => { const entry = endpoint(codex, codex ? "https://api.openai.com/v1" : "https://api.anthropic.com"); const request = vi.fn(); await discoverAgentEndpointModels(entry, request); expect(request).not.toHaveBeenCalled(); }); it.each([true, false])("restores the standard catalog when a gateway moves to the official URL (Codex=%s)", async codex => { const entry = endpoint(codex, codex ? "https://api.openai.com/v1" : "https://api.anthropic.com"); await discoverAgentEndpointModels(entry, vi.fn()); const models = codex ? (entry.config as any).endpoint.models : (entry.config as any).endpointModels; expect(models).toEqual(officialModels(codex)); expect(models).not.toContain("old"); }); it("refuses a catalog larger than 2 MB without parsing it", async () => { const entry = endpoint(true); const huge = new Response(`{"data":[${'{"id":"m"},'.repeat(300_000)}{"id":"m"}]}`); await expect(discoverAgentEndpointModels(entry, vi.fn().mockResolvedValue(huge))).rejects.toThrow("larger than 2 MB"); expect(entry.config).toMatchObject({ endpoint: { models: ["old"] } }); }); it("keeps at most 1,000 models", async () => { const entry = endpoint(true); const data = Array.from({ length: 1500 }, (_, index) => ({ id: `model-${index}` })); await discoverAgentEndpointModels(entry, vi.fn().mockResolvedValue(Response.json({ data }))); expect((entry.config as any).endpoint.models).toHaveLength(1000); }); it("never offers Claude Code's reserved `default` as a gateway model", async () => { const entry = endpoint(false); await discoverAgentEndpointModels(entry, vi.fn().mockResolvedValue(Response.json({ data: [{ id: "default" }, { id: "model-a" }] }))); expect((entry.config as any).endpointModels).toEqual(["model-a"]); }); it("explains a gateway that drops the connection mid-catalog without exposing the error", async () => { const entry = endpoint(true); const body = new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode('{"data":[')); controller.error(new Error("fixture-secret")); } }); const failure = await discoverAgentEndpointModels(entry, vi.fn().mockResolvedValue(new Response(body))).catch(error => error); expect(failure.message).toContain("closed the connection while sending its model catalog"); expect(failure.message).not.toContain("fixture-secret"); expect(failure.status).toBe(400); }); it.each([true, false])("never sends the key to a plain-HTTP host off this computer (Codex=%s)", async codex => { const entry = endpoint(codex, "http://gateway.example/v1"); const request = vi.fn(); const failure = await discoverAgentEndpointModels(entry, request).catch(error => error); expect(request).not.toHaveBeenCalled(); expect(failure.message).toContain("only over HTTPS or from this computer"); expect(failure.message).toContain("type the model"); expect(failure.status).toBe(400); }); it.each(["http://127.0.0.1:8317/v1", "http://[::1]:8317/v1"])("still reads a loopback catalog over plain HTTP (%s)", async url => { const entry = endpoint(false, url); const request = vi.fn().mockResolvedValue(Response.json({ data: [{ id: "model-a" }] })); await discoverAgentEndpointModels(entry, request); expect(request).toHaveBeenCalledTimes(1); expect((entry.config as any).endpointModels).toEqual(["model-a"]); });