benjivers/jev-mcp Public Public: anyone can read this; nobody can change it.

workbenchthis machine
Files 70d5d708f3dbeab8fa42d6f7ad0e6733dfd66f3b
137 lines · 4.8 KB · ~1.4k tok Raw History
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
}
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts