From da1b55d2332406c8304f2d7211497206fa2dfa46 Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Sun, 26 Apr 2026 02:07:51 +0000 Subject: [PATCH] test: add coverage for listK8sModels CLI fetch and fallback chore: bump version to 0.1.32 Co-Authored-By: Paperclip --- package.json | 2 +- src/server/models.test.ts | 78 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 src/server/models.test.ts diff --git a/package.json b/package.json index caa305a..ed36bef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "paperclip-adapter-opencode-k8s", - "version": "0.1.31", + "version": "0.1.32", "description": "Paperclip adapter plugin that runs OpenCode agents as Kubernetes Jobs", "license": "MIT", "type": "module", diff --git a/src/server/models.test.ts b/src/server/models.test.ts new file mode 100644 index 0000000..418401f --- /dev/null +++ b/src/server/models.test.ts @@ -0,0 +1,78 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +const execMock = vi.fn(); + +vi.mock("child_process", () => ({ + exec: (cmd: string, opts: unknown, cb: (err: Error | null, result: { stdout: string; stderr: string }) => void) => { + execMock(cmd, opts, cb); + }, +})); + +const { listK8sModels } = await import("./models.js"); + +function mockExecResult(stdout: string) { + execMock.mockImplementation((_cmd, _opts, cb) => { + cb(null, { stdout, stderr: "" }); + }); +} + +function mockExecError(err: Error) { + execMock.mockImplementation((_cmd, _opts, cb) => { + cb(err, { stdout: "", stderr: "" }); + }); +} + +describe("listK8sModels", () => { + beforeEach(() => { + execMock.mockReset(); + }); + + it("parses opencode models output into AdapterModel entries", async () => { + mockExecResult( + [ + "anthropic/claude-opus-4-7", + "openai/gpt-4o", + "google/gemini-2.5-pro", + ].join("\n"), + ); + + const models = await listK8sModels(); + + expect(models).toHaveLength(3); + expect(models[0]).toEqual({ id: "anthropic/claude-opus-4-7", label: "claude opus 4 7" }); + expect(models[1]).toEqual({ id: "openai/gpt-4o", label: "gpt 4o" }); + expect(models[2]).toEqual({ id: "google/gemini-2.5-pro", label: "gemini 2.5 pro" }); + }); + + it("ignores blank lines and trims whitespace", async () => { + mockExecResult("\nanthropic/claude-opus-4-7\n\n openai/gpt-4o \n\n"); + + const models = await listK8sModels(); + + expect(models.map((m) => m.id)).toEqual([ + "anthropic/claude-opus-4-7", + "openai/gpt-4o", + ]); + }); + + it("invokes opencode models with a timeout", async () => { + mockExecResult("anthropic/claude-opus-4-7"); + + await listK8sModels(); + + expect(execMock).toHaveBeenCalledTimes(1); + const [cmd, opts] = execMock.mock.calls[0]; + expect(cmd).toBe("opencode models"); + expect(opts).toMatchObject({ timeout: 30_000 }); + }); + + it("falls back to the static list when the CLI fails", async () => { + mockExecError(new Error("ENOENT: opencode not found")); + + const models = await listK8sModels(); + + expect(models.length).toBeGreaterThan(0); + expect(models.map((m) => m.id)).toContain("anthropic/claude-opus-4-7"); + expect(models.map((m) => m.id)).toContain("openai/gpt-4o"); + }); +});