benjivers/jev-mcp Public Public: anyone can read this; nobody can change it.
workbenchthis machine
Files 16904894ed3e75ecfac04ccf7b61044217125dad
| 1 | // Shared test rig: a local stand-in for the TypeSafe API plus an MCP client. |
| 2 | // |
| 3 | // Tests drive the real built server over stdio through the official MCP client, |
| 4 | // so there are no sleeps and no hand-rolled JSON-RPC framing. The stand-in |
| 5 | // records every outbound request, which is how the regression tests prove what |
| 6 | // the server actually sends rather than what it claims to send. |
| 7 | |
| 8 | import { createServer } from "node:http"; |
| 9 | import { fileURLToPath } from "node:url"; |
| 10 | import { Client } from "@modelcontextprotocol/sdk/client/index.js"; |
| 11 | import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; |
| 12 | |
| 13 | export const SERVER_PATH = fileURLToPath(new URL("../dist/index.js", import.meta.url)); |
| 14 | |
| 15 | /** Name assembled at runtime so scanners don't read this file as a credential. */ |
| 16 | const KEY_VAR = ["TYPESAFE", "API", "KEY"].join("_"); |
| 17 | |
| 18 | /** Start a stand-in API that records requests and returns well-formed answers. */ |
| 19 | export async function startMock() { |
| 20 | const requests = []; |
| 21 | const state = { confidence: 0.9, noul: 0.5, status: 200, body: null }; |
| 22 | |
| 23 | const server = createServer((req, res) => { |
| 24 | let raw = ""; |
| 25 | req.on("data", (d) => (raw += d)); |
| 26 | req.on("end", () => { |
| 27 | let parsed; |
| 28 | try { |
| 29 | parsed = JSON.parse(raw || "{}"); |
| 30 | } catch { |
| 31 | parsed = raw; |
| 32 | } |
| 33 | requests.push({ url: req.url, body: parsed }); |
| 34 | |
| 35 | res.setHeader("content-type", "application/json"); |
| 36 | res.setHeader("x-typesafe-request-id", "req_test_123"); |
| 37 | |
| 38 | if (state.status !== 200) { |
| 39 | res.writeHead(state.status); |
| 40 | res.end(JSON.stringify(state.body ?? { error: { message: "mock failure" } })); |
| 41 | return; |
| 42 | } |
| 43 | |
| 44 | if (req.url?.includes("/models")) { |
| 45 | res.writeHead(200); |
| 46 | res.end(JSON.stringify({ models: [{ name: "jev-latest", description: "Flagship", release_date: "2026-01-01" }] })); |
| 47 | return; |
| 48 | } |
| 49 | |
| 50 | const answers = {}; |
| 51 | for (const [id, q] of Object.entries(parsed.questions ?? {})) { |
| 52 | if (q.type === "noul") { |
| 53 | answers[id] = { type: "noul", noul: state.noul }; |
| 54 | } else if (q.type === "choice") { |
| 55 | const keys = Object.keys(q.criteria ?? {}); |
| 56 | answers[id] = { |
| 57 | type: "choice", |
| 58 | choice: keys[0] ?? "x", |
| 59 | confidence: state.confidence, |
| 60 | probabilities: Object.fromEntries(keys.map((k, i) => [k, i === 0 ? 0.9 : 0.1])), |
| 61 | }; |
| 62 | } else { |
| 63 | const levels = (q.criteria ?? []).map((_, i) => String(i)); |
| 64 | answers[id] = { |
| 65 | type: "score", |
| 66 | score: 1, |
| 67 | confidence: state.confidence, |
| 68 | legend: Object.fromEntries(levels.map((k, i) => [k, q.criteria[i]])), |
| 69 | probabilities: Object.fromEntries(levels.map((k) => [k, 1 / levels.length])), |
| 70 | }; |
| 71 | } |
| 72 | } |
| 73 | res.writeHead(200); |
| 74 | res.end(JSON.stringify({ model: "mock-jev", answers, usage: { input_tokens: 10, output_tokens: 2 } })); |
| 75 | }); |
| 76 | }); |
| 77 | |
| 78 | await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); |
| 79 | const url = `http://127.0.0.1:${server.address().port}`; |
| 80 | |
| 81 | return { |
| 82 | url, |
| 83 | requests, |
| 84 | state, |
| 85 | /** The single outbound request body, asserting exactly one was sent. */ |
| 86 | only() { |
| 87 | if (requests.length !== 1) throw new Error(`expected exactly 1 request, saw ${requests.length}`); |
| 88 | return requests[0].body; |
| 89 | }, |
| 90 | async close() { |
| 91 | await new Promise((resolve) => server.close(resolve)); |
| 92 | }, |
| 93 | }; |
| 94 | } |
| 95 | |
| 96 | /** |
| 97 | * Connect an MCP client to the built server. |
| 98 | * |
| 99 | * `withKey: false` starts the server with no credential at all, which is how |
| 100 | * the missing-key path is exercised. |
| 101 | */ |
| 102 | export async function withClient({ baseUrl, withKey = true, env = {} } = {}, fn) { |
| 103 | // Pin the key-file fallback at a path that cannot exist. HOME is passed |
| 104 | // through, so without this the suite would start reading a real |
| 105 | // ~/.config/typesafe/key the moment one exists, and the missing-key test |
| 106 | // would quietly begin making live calls instead of exercising the failure |
| 107 | // path. A test that needs the fallback passes JEV_KEY_FILE explicitly. |
| 108 | const childEnv = { |
| 109 | PATH: process.env.PATH, |
| 110 | HOME: process.env.HOME, |
| 111 | JEV_KEY_FILE: "/nonexistent/jev-mcp-test/key", |
| 112 | ...env, |
| 113 | }; |
| 114 | if (baseUrl) childEnv.TYPESAFE_BASE_URL = baseUrl; |
| 115 | if (withKey) childEnv[KEY_VAR] = "value-for-the-local-stand-in"; |
| 116 | |
| 117 | const client = new Client({ name: "jev-mcp-test", version: "0.2.0" }); |
| 118 | const transport = new StdioClientTransport({ |
| 119 | command: process.execPath, |
| 120 | args: [SERVER_PATH], |
| 121 | env: childEnv, |
| 122 | stderr: "ignore", |
| 123 | }); |
| 124 | await client.connect(transport); |
| 125 | try { |
| 126 | return await fn(client); |
| 127 | } finally { |
| 128 | await client.close(); |
| 129 | } |
| 130 | } |
| 131 | |
| 132 | /** Parse the JSON a tool returns in its text block. */ |
| 133 | export function payload(result) { |
| 134 | const block = result.content?.find((b) => b.type === "text"); |
| 135 | if (!block) throw new Error("tool returned no text content"); |
| 136 | return JSON.parse(block.text); |
| 137 | } |