// Собранный сервер отвечает на initialize и tools/list, а в stdout не // пишет ничего, кроме JSON-RPC. В сеть не ходит: tools/list её не требует. import { test } from "node:test"; import assert from "node:assert/strict"; import { execFileSync, spawn } from "node:child_process"; import { fileURLToPath } from "node:url"; const root = fileURLToPath(new URL("..", import.meta.url)); test("dist/index.js: stdout — только валидный JSON-RPC, шесть read-only инструментов", { timeout: 60_000 }, async () => { execFileSync(process.execPath, ["node_modules/typescript/bin/tsc", "-p", "tsconfig.build.json"], { cwd: root, stdio: "inherit", }); const child = spawn(process.execPath, ["dist/index.js"], { cwd: root, env: { PATH: process.env.PATH ?? "", OPENRUSROUTER_API_KEY: "test", OPENRUSROUTER_BASE_URL: "http://localhost:1" }, stdio: ["pipe", "pipe", "pipe"], }); const lines: string[] = []; let buf = ""; const waiters = new Map) => void>(); child.stdout.setEncoding("utf8"); child.stdout.on("data", (chunk: string) => { buf += chunk; let i: number; while ((i = buf.indexOf("\n")) >= 0) { const line = buf.slice(0, i); buf = buf.slice(i + 1); lines.push(line); try { const msg = JSON.parse(line) as Record; if (typeof msg.id === "number") waiters.get(msg.id)?.(msg); } catch { // невалидная строка будет поймана проверкой ниже } } }); child.stderr.resume(); const send = (msg: object) => child.stdin.write(`${JSON.stringify(msg)}\n`); const request = (id: number, method: string, params: object) => new Promise>((resolve) => { waiters.set(id, resolve); send({ jsonrpc: "2.0", id, method, params }); }); try { const init = await request(1, "initialize", { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "test", version: "0" }, }); assert.equal((init.result as { serverInfo: { name: string } }).serverInfo.name, "openrusrouter"); send({ jsonrpc: "2.0", method: "notifications/initialized" }); const list = await request(2, "tools/list", {}); const tools = (list.result as { tools: { name: string; annotations?: Record }[] }).tools; assert.deepEqual( tools.map((t) => t.name).sort(), ["estimate_cost", "get_balance", "get_usage", "list_api_keys", "list_models", "pick_model"], ); for (const t of tools) { assert.equal(t.annotations?.readOnlyHint, true, t.name); assert.equal(t.annotations?.destructiveHint, false, t.name); } } finally { child.kill(); } assert.ok(lines.length >= 2); for (const line of lines) { assert.doesNotThrow(() => JSON.parse(line), `не JSON в stdout: ${line.slice(0, 100)}`); } });