Add import-safe API/MCP contract tests

This commit is contained in:
Rbanh 2026-02-18 20:40:56 -05:00
parent 7b6b176b1b
commit 925f8072e9
3 changed files with 124 additions and 23 deletions

View File

@ -1,4 +1,5 @@
import { compile, analyze } from "./compile.js";
import { pathToFileURL } from "node:url";
const API_VERSION = "0.3.0";
const SCHEMA_VERSION = "1.0.0";
@ -38,7 +39,7 @@ function decodePayload(value) {
throw new Error("payload must be a JSON object or JSON string");
}
function toolListResult() {
export function toolListResult() {
return {
tools: [
{
@ -89,7 +90,7 @@ function toolListResult() {
};
}
function withEnvelopeMeta(payload) {
export function withEnvelopeMeta(payload) {
return {
api_version: API_VERSION,
schema_version: SCHEMA_VERSION,
@ -97,7 +98,7 @@ function withEnvelopeMeta(payload) {
};
}
function uiBundleDescriptor() {
export function uiBundleDescriptor() {
return {
name: "schemeta-workspace",
version: "0.2.0",
@ -109,7 +110,7 @@ function uiBundleDescriptor() {
};
}
function handleToolCall(name, args) {
export function handleToolCall(name, args) {
if (!args || typeof args !== "object") {
args = {};
}
@ -146,7 +147,7 @@ function handleToolCall(name, args) {
throw new Error(`Unknown tool '${name}'`);
}
function handleRequest(message) {
export function handleRequest(message) {
const { id, method, params } = message;
if (method === "initialize") {
@ -226,13 +227,16 @@ function tryParseBuffer() {
}
}
process.stdin.on("data", (chunk) => {
const isMain = process.argv[1] ? import.meta.url === pathToFileURL(process.argv[1]).href : false;
if (isMain) {
process.stdin.on("data", (chunk) => {
stdinBuffer = Buffer.concat([stdinBuffer, Buffer.from(chunk)]);
tryParseBuffer();
});
});
process.stdin.on("error", (err) => {
process.stdin.on("error", (err) => {
process.stderr.write(`stdin error: ${String(err)}\n`);
});
});
process.stdin.resume();
process.stdin.resume();
}

View File

@ -1,6 +1,7 @@
import { createServer } from "node:http";
import { readFile } from "node:fs/promises";
import { extname, join, normalize } from "node:path";
import { pathToFileURL } from "node:url";
import { analyze, compile } from "./compile.js";
import { applyLayoutToModel } from "./layout.js";
import { validateModel } from "./validate.js";
@ -10,8 +11,8 @@ const MAX_BODY_BYTES = Number(process.env.MAX_BODY_BYTES ?? 2 * 1024 * 1024);
const CORS_ORIGIN = process.env.CORS_ORIGIN ?? "*";
const MAX_REQUESTS_PER_MINUTE = Number(process.env.MAX_REQUESTS_PER_MINUTE ?? 120);
const FRONTEND_ROOT = join(process.cwd(), "frontend");
const API_VERSION = "0.3.0";
const SCHEMA_VERSION = "1.0.0";
export const API_VERSION = "0.3.0";
export const SCHEMA_VERSION = "1.0.0";
const RATE_WINDOW_MS = 60_000;
const rateByClient = new Map();
@ -36,7 +37,7 @@ function json(res, status, payload) {
res.end(JSON.stringify(payload, null, 2));
}
function errorEnvelope(code, message) {
export function errorEnvelope(code, message) {
return {
ok: false,
error: {
@ -114,7 +115,7 @@ async function serveStatic(urlPath, res) {
}
}
function parsePayloadOptions(body) {
export function parsePayloadOptions(body) {
if (body && typeof body === "object" && Object.prototype.hasOwnProperty.call(body, "payload")) {
return {
payload: body.payload,
@ -128,7 +129,7 @@ function parsePayloadOptions(body) {
};
}
function withEnvelopeMeta(payload) {
export function withEnvelopeMeta(payload) {
return {
api_version: API_VERSION,
schema_version: SCHEMA_VERSION,
@ -172,7 +173,8 @@ function withinRateLimit(req) {
return true;
}
const server = createServer(async (req, res) => {
export function createRequestHandler() {
return async (req, res) => {
if (!req.url || !req.method) {
return json(res, 400, errorEnvelope("invalid_request", "Invalid request."));
}
@ -312,8 +314,18 @@ const server = createServer(async (req, res) => {
}
return json(res, 404, errorEnvelope("not_found", "Not found."));
});
};
}
server.listen(PORT, () => {
console.log(`Schemeta server listening on http://localhost:${PORT}`);
});
export const server = createServer(createRequestHandler());
export function startServer(port = PORT) {
server.listen(port, () => {
console.log(`Schemeta server listening on http://localhost:${port}`);
});
}
const isMain = process.argv[1] ? import.meta.url === pathToFileURL(process.argv[1]).href : false;
if (isMain) {
startServer(PORT);
}

View File

@ -0,0 +1,85 @@
import test from "node:test";
import assert from "node:assert/strict";
import fixture from "../examples/esp32-audio.json" with { type: "json" };
import { compile, analyze } from "../src/compile.js";
import {
API_VERSION as REST_API_VERSION,
SCHEMA_VERSION as REST_SCHEMA_VERSION,
withEnvelopeMeta as withRestEnvelopeMeta,
parsePayloadOptions,
errorEnvelope
} from "../src/server.js";
import {
handleToolCall,
uiBundleDescriptor,
toolListResult
} from "../src/mcp-server.js";
test("REST compile contract shape is stable with version metadata", () => {
const parsed = parsePayloadOptions({ payload: fixture, options: { render_mode: "schematic_stub" } });
const body = withRestEnvelopeMeta(compile(parsed.payload, parsed.options));
assert.equal(body.ok, true);
assert.equal(body.api_version, REST_API_VERSION);
assert.equal(body.schema_version, REST_SCHEMA_VERSION);
assert.ok(Array.isArray(body.errors));
assert.ok(Array.isArray(body.warnings));
assert.ok(Array.isArray(body.bus_groups));
assert.equal(typeof body.render_mode_used, "string");
assert.equal(typeof body.svg, "string");
});
test("REST analyze contract shape is stable with version metadata", () => {
const parsed = parsePayloadOptions({ payload: fixture });
const body = withRestEnvelopeMeta(analyze(parsed.payload, parsed.options));
assert.equal(body.ok, true);
assert.equal(body.api_version, REST_API_VERSION);
assert.equal(body.schema_version, REST_SCHEMA_VERSION);
assert.ok(body.topology);
assert.ok(Array.isArray(body.topology.power_domains));
assert.ok(Array.isArray(body.topology.signal_paths));
});
test("REST error envelope exposes stable code/message fields", () => {
const body = errorEnvelope("rate_limited", "Rate limit exceeded. Try again shortly.");
assert.equal(body.ok, false);
assert.equal(body.error.code, "rate_limited");
assert.equal(typeof body.error.message, "string");
});
test("MCP schemeta_compile returns structured content with version metadata", () => {
const result = handleToolCall("schemeta_compile", { payload: fixture });
const body = result.structuredContent;
assert.equal(result.isError, false);
assert.equal(body.ok, true);
assert.equal(typeof body.api_version, "string");
assert.equal(typeof body.schema_version, "string");
assert.equal(typeof body.svg, "string");
});
test("MCP schemeta_analyze returns structured topology fields", () => {
const result = handleToolCall("schemeta_analyze", { payload: fixture });
const body = result.structuredContent;
assert.equal(result.isError, false);
assert.equal(body.ok, true);
assert.equal(typeof body.api_version, "string");
assert.equal(typeof body.schema_version, "string");
assert.ok(body.topology);
assert.ok(Array.isArray(body.topology.power_domains));
});
test("MCP UI bundle and tool list contracts remain stable", () => {
const ui = uiBundleDescriptor();
assert.equal(ui.transport, "iframe");
assert.equal(typeof ui.api_version, "string");
assert.equal(typeof ui.schema_version, "string");
const tools = toolListResult();
assert.ok(Array.isArray(tools.tools));
assert.ok(tools.tools.some((t) => t.name === "schemeta_compile"));
assert.ok(tools.tools.some((t) => t.name === "schemeta_analyze"));
assert.ok(tools.tools.some((t) => t.name === "schemeta_ui_bundle"));
});