markdown-review/server/test/smoke.mjs
TLC AI Lab 8eba380a3f markdown-review v0.0.1 spike: comment-on-markdown -> agent (MCP Apps)
Antigravity-style inline markdown commenting as an installable Claude plugin.
- MCP Apps webview (markdown-it renderer + comment UI)
- three lanes: Answer Now (sendMessage), Add to batch, Submit All (persist+send)
- edit/review mode toggle; sidecar comment persistence
- self-contained prebuilt server/dist (zero runtime node_modules)
- self-marketplace manifest for /plugin install
- 11/11 stdio smoke tests passing

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 00:58:36 -05:00

88 lines
3.5 KiB
JavaScript

/**
* End-to-end smoke test: launch the built server over stdio with a real MCP
* client, list tools, read the UI resource, and exercise open_markdown +
* submit_batch. No host/webview needed — this verifies the server contract.
*/
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { writeFile, mkdir, readFile, rm } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
const here = path.dirname(fileURLToPath(import.meta.url));
const root = path.join(here, "..");
const sample = path.join(here, "sample.md");
let failures = 0;
const ok = (c, m) => { console.log(`${c ? "PASS" : "FAIL"} ${m}`); if (!c) failures++; };
await writeFile(sample, "# Title\n\nFirst paragraph with **bold**.\n\n- item one\n- item two\n");
const transport = new StdioClientTransport({
command: process.execPath,
args: [path.join(root, "dist", "index.js"), "--stdio"],
});
const client = new Client({ name: "smoke", version: "0.0.0" });
await client.connect(transport);
// 1. tools/list — model-facing visibility
const { tools } = await client.listTools();
const names = tools.map((t) => t.name).sort();
ok(names.includes("open_markdown"), "open_markdown tool registered");
ok(names.includes("submit_batch"), "submit_batch tool registered");
const openTool = tools.find((t) => t.name === "open_markdown");
ok(
JSON.stringify(openTool?._meta ?? {}).includes("ui://markdown-review/mcp-app.html"),
"open_markdown carries ui.resourceUri meta",
);
// 2. resources — the UI html is served
const { resources } = await client.listResources();
ok(
resources.some((r) => r.uri === "ui://markdown-review/mcp-app.html"),
"UI resource listed",
);
const res = await client.readResource({ uri: "ui://markdown-review/mcp-app.html" });
ok(
(res.contents?.[0]?.text ?? "").includes("Markdown Review"),
"UI resource returns the app HTML",
);
// 3. open_markdown returns the file content + empty comment set
const opened = await client.callTool({
name: "open_markdown",
arguments: { path: sample, mode: "edit" },
});
const sc = opened.structuredContent ?? {};
ok(sc.markdown?.includes("First paragraph"), "open_markdown returns file content");
ok(Array.isArray(sc.existingComments) && sc.existingComments.length === 0, "no pre-existing comments");
ok(sc.mode === "edit", "mode echoed back");
// 4. submit_batch persists to the sidecar, then open_markdown reads it back
const comment = {
id: "c-test-1",
anchor: { line_start: 3, line_end: 3, char_start: 9, char_end: 24, quote: "First paragraph" },
body: "Tighten this sentence.",
store: "sidecar",
created: new Date().toISOString(),
};
const saved = await client.callTool({
name: "submit_batch",
arguments: { file: sc.path, comments: [comment], mode: "edit" },
});
ok(saved.structuredContent?.saved === true, "submit_batch reports saved");
ok(saved.structuredContent?.count === 1, "submit_batch count == 1");
const reopened = await client.callTool({
name: "open_markdown",
arguments: { path: sample, mode: "review" },
});
const round = reopened.structuredContent?.existingComments ?? [];
ok(round.length === 1 && round[0].body === "Tighten this sentence.", "sidecar comment round-trips on reopen");
await client.close();
// cleanup
await rm(sample, { force: true });
await rm(path.join(here, ".claude"), { recursive: true, force: true });
console.log(`\n${failures === 0 ? "ALL PASSED" : failures + " FAILED"}`);
process.exit(failures === 0 ? 0 : 1);