| import { beforeEach, describe, expect, it, vi } from "vitest"; |
| import { models as codexFallbackModels } from "@paperclipai/adapter-codex-local"; |
| import { models as cursorFallbackModels } from "@paperclipai/adapter-cursor-local"; |
| import { resetOpenCodeModelsCacheForTests } from "@paperclipai/adapter-opencode-local/server"; |
| import { listAdapterModels } from "../adapters/index.js"; |
| import { resetCodexModelsCacheForTests } from "../adapters/codex-models.js"; |
| import { resetCursorModelsCacheForTests, setCursorModelsRunnerForTests } from "../adapters/cursor-models.js"; |
|
|
| describe("adapter model listing", () => { |
| beforeEach(() => { |
| delete process.env.OPENAI_API_KEY; |
| delete process.env.PAPERCLIP_OPENCODE_COMMAND; |
| resetCodexModelsCacheForTests(); |
| resetCursorModelsCacheForTests(); |
| setCursorModelsRunnerForTests(null); |
| resetOpenCodeModelsCacheForTests(); |
| vi.restoreAllMocks(); |
| }); |
|
|
| it("returns an empty list for unknown adapters", async () => { |
| const models = await listAdapterModels("unknown_adapter"); |
| expect(models).toEqual([]); |
| }); |
|
|
| it("returns codex fallback models when no OpenAI key is available", async () => { |
| const fetchSpy = vi.spyOn(globalThis, "fetch"); |
| const models = await listAdapterModels("codex_local"); |
|
|
| expect(models).toEqual(codexFallbackModels); |
| expect(fetchSpy).not.toHaveBeenCalled(); |
| }); |
|
|
| it("loads codex models dynamically and merges fallback options", async () => { |
| process.env.OPENAI_API_KEY = "sk-test"; |
| const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue({ |
| ok: true, |
| json: async () => ({ |
| data: [ |
| { id: "gpt-5-pro" }, |
| { id: "gpt-5" }, |
| ], |
| }), |
| } as Response); |
|
|
| const first = await listAdapterModels("codex_local"); |
| const second = await listAdapterModels("codex_local"); |
|
|
| expect(fetchSpy).toHaveBeenCalledTimes(1); |
| expect(first).toEqual(second); |
| expect(first.some((model) => model.id === "gpt-5-pro")).toBe(true); |
| expect(first.some((model) => model.id === "codex-mini-latest")).toBe(true); |
| }); |
|
|
| it("falls back to static codex models when OpenAI model discovery fails", async () => { |
| process.env.OPENAI_API_KEY = "sk-test"; |
| vi.spyOn(globalThis, "fetch").mockResolvedValue({ |
| ok: false, |
| status: 401, |
| json: async () => ({}), |
| } as Response); |
|
|
| const models = await listAdapterModels("codex_local"); |
| expect(models).toEqual(codexFallbackModels); |
| }); |
|
|
|
|
| it("returns cursor fallback models when CLI discovery is unavailable", async () => { |
| setCursorModelsRunnerForTests(() => ({ |
| status: null, |
| stdout: "", |
| stderr: "", |
| hasError: true, |
| })); |
|
|
| const models = await listAdapterModels("cursor"); |
| expect(models).toEqual(cursorFallbackModels); |
| }); |
|
|
| it("loads cursor models dynamically and caches them", async () => { |
| const runner = vi.fn(() => ({ |
| status: 0, |
| stdout: "Available models: auto, composer-1.5, gpt-5.3-codex-high, sonnet-4.6", |
| stderr: "", |
| hasError: false, |
| })); |
| setCursorModelsRunnerForTests(runner); |
|
|
| const first = await listAdapterModels("cursor"); |
| const second = await listAdapterModels("cursor"); |
|
|
| expect(runner).toHaveBeenCalledTimes(1); |
| expect(first).toEqual(second); |
| expect(first.some((model) => model.id === "auto")).toBe(true); |
| expect(first.some((model) => model.id === "gpt-5.3-codex-high")).toBe(true); |
| expect(first.some((model) => model.id === "composer-1")).toBe(true); |
| }); |
|
|
| it("returns no opencode models when opencode command is unavailable", async () => { |
| process.env.PAPERCLIP_OPENCODE_COMMAND = "__paperclip_missing_opencode_command__"; |
|
|
| const models = await listAdapterModels("opencode_local"); |
| expect(models).toEqual([]); |
| }); |
| }); |
|
|