Files
openrusrouter-mcp/test/tools.test.ts
T
vasyanskandClaude Opus 5.5 d7d02267e2 Добавь HTTP-клиент, каталог моделей и шесть read-only инструментов MCP
Клиент — замороженный объект с семью методами GET, без универсального
запроса и тела; денежные поля проверяются на строковый тип (fail-closed).
Каталог джойнит /api/models и /v1/models: модальность и рубли из первого,
USD и доступность ключу из второго. Инструменты: list_models, pick_model,
estimate_cost, get_balance, get_usage, list_api_keys. Точка входа stdio
уводит console.log/info в stderr первым импортом.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 10:59:06 +07:00

101 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Вызов инструментов через настоящий MCP-протокол (in-memory транспорт) поверх
// клиента с фейковым fetch на фикстурах: проверяет связку zod-схем, дефолтов,
// кэша каталога и формы результатов.
import { test } from "node:test";
import assert from "node:assert/strict";
import { Client as McpClient } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { createTtlCache } from "../src/cache.js";
import { CATALOG_TTL_MS, buildCatalog } from "../src/catalog.js";
import { createClient } from "../src/client.js";
import { registerAllTools } from "../src/tools/index.js";
import { fixture } from "./helpers.js";
const KEY = "orr_live_Ab3dEfSECRETSECRETSECRETSECRET";
const FIXTURES: Record<string, string> = {
"/v1/models": "v1-models",
"/api/models": "api-models",
"/api/balance": "api-balance",
"/api/usage": "api-usage",
"/api/keys": "api-keys",
};
async function setup(status = 200) {
const counts: Record<string, number> = {};
const fetchImpl = (async (input: string | URL | Request) => {
const path = new URL(String(input)).pathname;
counts[path] = (counts[path] ?? 0) + 1;
if (status !== 200) return new Response(JSON.stringify({ error: { code: "invalid_key" } }), { status });
return new Response(JSON.stringify(fixture(FIXTURES[path] ?? "missing")), { status: 200 });
}) as typeof fetch;
const client = createClient({ apiKey: KEY, baseUrl: "https://example.test" }, fetchImpl);
const server = new McpServer({ name: "openrusrouter", version: "0.0.0" });
registerAllTools(server, {
client,
catalog: createTtlCache(CATALOG_TTL_MS, () => buildCatalog(client)),
apiKey: KEY,
});
const [a, b] = InMemoryTransport.createLinkedPair();
const mcp = new McpClient({ name: "t", version: "0" });
await Promise.all([server.connect(a), mcp.connect(b)]);
return { mcp, counts };
}
type Result = { isError?: boolean; structuredContent?: Record<string, unknown>; content: { text: string }[] };
async function call(mcp: McpClient, name: string, args: Record<string, unknown> = {}): Promise<Result> {
return (await mcp.callTool({ name, arguments: args })) as unknown as Result;
}
test("list_models с дефолтами: только text, по цене, каталог кэшируется", async () => {
const { mcp, counts } = await setup();
const r = await call(mcp, "list_models");
assert.notEqual(r.isError, true);
const sc = r.structuredContent as { total: number; items: { id: string; modality: string }[] };
assert.equal(sc.total, 6);
assert.ok(sc.items.every((m) => m.modality === "text"));
assert.equal(sc.items[0]?.id, "nvidia/nemotron-3.5-super-vl:free");
assert.deepEqual(JSON.parse(r.content[0]?.text ?? ""), sc);
await call(mcp, "list_models", { modality: "image", limit: 1, offset: 1 });
assert.equal(counts["/api/models"], 1);
assert.equal(counts["/v1/models"], 1);
});
test("pick_model и estimate_cost", async () => {
const { mcp } = await setup();
const p = await call(mcp, "pick_model", { max_prompt_rub_1m: "9", limit: 1 });
const items = (p.structuredContent as { items: { id: string; why: string }[] }).items;
assert.equal(items.length, 1);
assert.equal(items[0]?.id, "nvidia/nemotron-3.5-super-vl:free");
const e = await call(mcp, "estimate_cost", { model_id: "openai/whisper-1", prompt_tokens: 10 });
assert.equal(e.isError, true);
const missing = await call(mcp, "estimate_cost", { model_id: "no/such", prompt_tokens: 10 });
assert.equal(missing.isError, true);
assert.match(missing.content[0]?.text ?? "", /не найдена в каталоге/);
});
test("get_balance, get_usage, list_api_keys", async () => {
const { mcp } = await setup();
const b = (await call(mcp, "get_balance")).structuredContent ?? {};
assert.equal(b.balance_rub, "1234.56789012");
assert.equal(b.referral_balance_rub, "0");
const u = (await call(mcp, "get_usage", { days: 7 })).structuredContent ?? {};
assert.equal(u.total_rub, "1.29459022");
const k = await call(mcp, "list_api_keys");
assert.ok(!(k.content[0]?.text ?? "").includes(KEY));
const keys = (k.structuredContent as { keys: { is_current_key: boolean }[] }).keys;
assert.deepEqual(keys.map((x) => x.is_current_key), [true, false, false]);
});
test("ошибка API превращается в isError без утечки ключа", async () => {
const { mcp } = await setup(401);
const r = await call(mcp, "get_balance");
assert.equal(r.isError, true);
assert.match(r.content[0]?.text ?? "", /ключ недействителен/);
assert.ok(!(r.content[0]?.text ?? "").includes(KEY));
});