From ca3913a8b753ead3b0c38a47482b36d24f8a831e Mon Sep 17 00:00:00 2001 From: Erwann Mest Date: Sat, 4 Jul 2026 19:59:40 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(config):=20align=20model=20a?= =?UTF-8?q?llow/block=20env=20var=20names=20+=20add=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test set OPENCODE_MODEL_ALLOW/BLOCK while the source and docs use the MCP_-prefixed names, so allow/block patterns were empty at module load and isModelAllowed allowed everything — 3 failing tests. Align the test with the documented MCP_OPENCODE_MODEL_ALLOW/BLOCK contract. Add a CI workflow running typecheck, build, and test on pull_request and push to main. --- .github/workflows/ci.yml | 21 ++++++ src/__tests__/tools.test.ts | 127 ++++++++++++++++++------------------ 2 files changed, 86 insertions(+), 62 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..bdb3d6d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,21 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: "24" + cache: npm + - run: npm ci + - run: npm run typecheck + - run: npm run build + - run: npm test diff --git a/src/__tests__/tools.test.ts b/src/__tests__/tools.test.ts index aad7aba..f30a17e 100644 --- a/src/__tests__/tools.test.ts +++ b/src/__tests__/tools.test.ts @@ -1,25 +1,25 @@ -import { describe, it, expect, vi, beforeEach } from "vitest" +import { describe, it, expect, vi, beforeEach } from "vitest"; vi.hoisted(() => { - process.env.OPENCODE_MODEL_ALLOW = "github-copilot/*" - process.env.OPENCODE_MODEL_BLOCK = "" -}) + process.env.MCP_OPENCODE_MODEL_ALLOW = "github-copilot/*"; + process.env.MCP_OPENCODE_MODEL_BLOCK = ""; +}); vi.mock("child_process", () => ({ execSync: vi.fn(), spawn: vi.fn(() => ({ unref: vi.fn() })), -})) +})); vi.mock("@opencode-ai/sdk/client", () => ({ createOpencodeClient: vi.fn(), -})) +})); -import { execSync } from "child_process" -import { createOpencodeClient } from "@opencode-ai/sdk/client" -import { query, listModels, isModelAllowed } from "../index.js" +import { execSync } from "child_process"; +import { createOpencodeClient } from "@opencode-ai/sdk/client"; +import { query, listModels, isModelAllowed } from "../index.js"; -const mockExecSync = vi.mocked(execSync) -const mockCreateClient = vi.mocked(createOpencodeClient) +const mockExecSync = vi.mocked(execSync); +const mockCreateClient = vi.mocked(createOpencodeClient); const makeClient = () => ({ @@ -44,88 +44,91 @@ const makeClient = () => }, }), }, - }) as unknown as ReturnType + }) as unknown as ReturnType; beforeEach(() => { - vi.clearAllMocks() - mockExecSync.mockImplementation(() => Buffer.from("")) - mockCreateClient.mockReturnValue(makeClient()) -}) + vi.clearAllMocks(); + mockExecSync.mockImplementation(() => Buffer.from("")); + mockCreateClient.mockReturnValue(makeClient()); +}); describe("isModelAllowed", () => { it("allows matching wildcard pattern", () => { - expect(isModelAllowed("github-copilot/gpt-4.1")).toBe(true) - }) + expect(isModelAllowed("github-copilot/gpt-4.1")).toBe(true); + }); it("rejects model not in allow list", () => { - expect(isModelAllowed("anthropic/claude-3")).toBe(false) - }) -}) + expect(isModelAllowed("anthropic/claude-3")).toBe(false); + }); +}); describe("query", () => { it("returns response on success", async () => { - const result = await query({ prompt: "hello" }) - expect(result.content[0].text).toBe("Hello!") - }) + const result = await query({ prompt: "hello" }); + expect(result.content[0].text).toBe("Hello!"); + }); it("uses default model when none specified", async () => { - const client = makeClient() - mockCreateClient.mockReturnValue(client) + const client = makeClient(); + mockCreateClient.mockReturnValue(client); - await query({ prompt: "hello" }) + await query({ prompt: "hello" }); expect( (client.session.prompt as ReturnType).mock.calls[0][0].body .model, - ).toEqual({ providerID: "github-copilot", modelID: "gpt-4.1" }) - }) + ).toEqual({ providerID: "github-copilot", modelID: "gpt-4.1" }); + }); it("rejects disallowed model", async () => { - const result = await query({ prompt: "hello", model: "anthropic/claude-3" }) - expect(result.content[0].text).toContain("Error:") - expect(result.content[0].text).toContain("list_models") - }) + const result = await query({ + prompt: "hello", + model: "anthropic/claude-3", + }); + expect(result.content[0].text).toContain("Error:"); + expect(result.content[0].text).toContain("list_models"); + }); it("returns error when session creation fails", async () => { - const client = makeClient() - ;(client.session.create as ReturnType).mockResolvedValue({ + const client = makeClient(); + (client.session.create as ReturnType).mockResolvedValue({ data: null, - }) - mockCreateClient.mockReturnValue(client) + }); + mockCreateClient.mockReturnValue(client); - const result = await query({ prompt: "hello" }) - expect(result.content[0].text).toContain("Error:") - }) + const result = await query({ prompt: "hello" }); + expect(result.content[0].text).toContain("Error:"); + }); it("returns error when prompt throws", async () => { - const client = makeClient() - ;(client.session.prompt as ReturnType).mockRejectedValue( + const client = makeClient(); + (client.session.prompt as ReturnType).mockRejectedValue( new Error("network error"), - ) - mockCreateClient.mockReturnValue(client) + ); + mockCreateClient.mockReturnValue(client); - const result = await query({ prompt: "hello" }) - expect(result.content[0].text).toContain("Error:") - }) -}) + const result = await query({ prompt: "hello" }); + expect(result.content[0].text).toContain("Error:"); + }); +}); describe("listModels", () => { it("returns only allowed models", async () => { - const result = await listModels() - expect(result.content[0].text).toContain("github-copilot/gpt-4.1") - expect(result.content[0].text).toContain("github-copilot/gpt-5") - expect(result.content[0].text).not.toContain("anthropic/") - expect(result.content[0].text).not.toContain("openrouter/") - }) + const result = await listModels(); + expect(result.content[0].text).toContain("github-copilot/gpt-4.1"); + expect(result.content[0].text).toContain("github-copilot/gpt-5"); + expect(result.content[0].text).not.toContain("anthropic/"); + expect(result.content[0].text).not.toContain("openrouter/"); + }); it("returns error when provider list throws", async () => { - const client = makeClient() - ;(client.config.providers as ReturnType).mockRejectedValue( + const client = makeClient(); + (client.config.providers as ReturnType).mockRejectedValue( new Error("server unreachable"), - ) - mockCreateClient.mockReturnValue(client) + ); + mockCreateClient.mockReturnValue(client); - const result = await listModels() - expect(result.content[0].text).toContain("Error:") - }) -}) + const result = await listModels(); + expect(result.content[0].text).toContain("Error:"); + }); +});