|
| 1 | +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; |
| 2 | +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; |
| 3 | +import { describe, expect, it } from "vitest"; |
| 4 | +import { GittensoryMcp } from "../../src/mcp/server"; |
| 5 | +import { recordGateBlockOutcome, upsertPullRequestFromGitHub } from "../../src/db/repositories"; |
| 6 | +import { createTestEnv } from "../helpers/d1"; |
| 7 | + |
| 8 | +const REPO = "owner/widgets"; |
| 9 | + |
| 10 | +async function connect(env: Env) { |
| 11 | + const server = new GittensoryMcp(env).createServer(); |
| 12 | + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); |
| 13 | + await server.connect(serverTransport); |
| 14 | + const client = new Client({ name: "gittensory-gate-precision-test", version: "0.1.0" }, { capabilities: {} }); |
| 15 | + await client.connect(clientTransport); |
| 16 | + return client; |
| 17 | +} |
| 18 | + |
| 19 | +// 6 blocks citing one code, 2 on PRs that later merged (false positives) → rate 2/6 = 0.333, |
| 20 | +// comfortably above the service's MIN_SAMPLE guard so the per-type rate is a number, not null. |
| 21 | +async function seedGateLedger(env: Env) { |
| 22 | + for (let n = 1; n <= 6; n += 1) { |
| 23 | + await recordGateBlockOutcome(env, { repoFullName: REPO, pullNumber: n, headSha: `sha${n}`, blockerCodes: ["missing_linked_issue"] }); |
| 24 | + await upsertPullRequestFromGitHub(env, REPO, { |
| 25 | + number: n, |
| 26 | + title: `PR ${n}`, |
| 27 | + state: "closed", |
| 28 | + user: { login: "alice" }, |
| 29 | + ...(n <= 2 ? { merged_at: "2026-06-01T00:00:00.000Z" } : {}), |
| 30 | + }); |
| 31 | + } |
| 32 | +} |
| 33 | + |
| 34 | +describe("MCP gittensory_get_gate_precision (#2220)", () => { |
| 35 | + it("returns the per-gate-type precision report for an authorized caller and passes windowDays through", async () => { |
| 36 | + const env = createTestEnv(); |
| 37 | + await seedGateLedger(env); |
| 38 | + const client = await connect(env); |
| 39 | + const result = await client.callTool({ name: "gittensory_get_gate_precision", arguments: { owner: "owner", repo: "widgets", windowDays: 30 } }); |
| 40 | + expect(result.isError).toBeFalsy(); |
| 41 | + const data = result.structuredContent as { |
| 42 | + repoFullName: string; |
| 43 | + windowDays: number | null; |
| 44 | + perGateType: Array<{ gateType: string; blocked: number; blockedThenMerged: number; falsePositiveRate: number | null }>; |
| 45 | + overall: { blocked: number; blockedThenMerged: number; falsePositiveRate: number | null }; |
| 46 | + signals: string[]; |
| 47 | + }; |
| 48 | + expect(data.repoFullName).toBe(REPO); |
| 49 | + expect(data.windowDays).toBe(30); |
| 50 | + expect(data.overall).toMatchObject({ blocked: 6, blockedThenMerged: 2, falsePositiveRate: 0.333 }); |
| 51 | + expect(data.perGateType[0]).toMatchObject({ gateType: "missing_linked_issue", blocked: 6, blockedThenMerged: 2, falsePositiveRate: 0.333 }); |
| 52 | + expect(Array.isArray(data.signals)).toBe(true); |
| 53 | + // Numeric branch of the summary's ?? fallback. |
| 54 | + expect(JSON.stringify(result.content)).toContain("overall false-positive rate 0.333"); |
| 55 | + }); |
| 56 | + |
| 57 | + it("returns an empty report with a null rate when no gate blocks are recorded (no windowDays)", async () => { |
| 58 | + const env = createTestEnv(); |
| 59 | + const client = await connect(env); |
| 60 | + const result = await client.callTool({ name: "gittensory_get_gate_precision", arguments: { owner: "owner", repo: "widgets" } }); |
| 61 | + expect(result.isError).toBeFalsy(); |
| 62 | + const data = result.structuredContent as { windowDays: number | null; perGateType: unknown[]; overall: { blocked: number; falsePositiveRate: number | null } }; |
| 63 | + expect(data.windowDays).toBeNull(); |
| 64 | + expect(data.perGateType).toEqual([]); |
| 65 | + expect(data.overall.blocked).toBe(0); |
| 66 | + expect(data.overall.falsePositiveRate).toBeNull(); |
| 67 | + // Null branch of the summary's ?? fallback. |
| 68 | + expect(JSON.stringify(result.content)).toContain("n/a (below sample threshold)"); |
| 69 | + }); |
| 70 | + |
| 71 | + it("forbids the static mcp identity when the repo is outside MCP_READ_REPO_ALLOWLIST", async () => { |
| 72 | + const env = createTestEnv({ MCP_READ_REPO_ALLOWLIST: "" }); |
| 73 | + await seedGateLedger(env); |
| 74 | + const client = await connect(env); |
| 75 | + const result = await client.callTool({ name: "gittensory_get_gate_precision", arguments: { owner: "owner", repo: "widgets" } }); |
| 76 | + expect(result.isError).toBeTruthy(); |
| 77 | + expect(JSON.stringify(result.content)).toMatch(/cannot access this repository/i); |
| 78 | + }); |
| 79 | +}); |
0 commit comments