Files
gitmost/packages/mcp/test/mock/footnote-warnings.test.mjs
T
agent_coder 4b2af3d34a refactor(mcp): дедупликация конвертер-смежных хелперов (node-ops форк, footnote-*, parse-node-arg)
Аудит при подготовке #413 нашёл дрейфующие дубли между packages/mcp и
packages/prosemirror-markdown. Четыре дедупа (поведение тулов не меняется):

1. node-ops: форк ~960 строк сведён в ОДНУ копию в prosemirror-markdown (живая
   mcp-версия — строгое надмножество замороженного #293-seed'а пакета; сверено по
   git-истории, новая пакет-копия байт-в-байт == прежней mcp-копии). Barrel-экспорт
   полной поверхности; mcp/client.ts/page-search.ts/transforms.ts/collaboration.ts
   импортируют из пакета; тесты переехали. node-ops тянет stripInlineMarkdown ->
   пакет-локальная text-normalize.ts несёт только этот примитив (mcp-версия —
   домен #408; заголовок документирует дубликацию + источник истины).
2. footnote-lex/footnote-analyze (vestigial legacy [^id]: диагностика): сведены к
   одному fence-aware предупреждению 'reference-style footnotes -> use ^[...]'
   (полезно для класса #410); footnote-lex удалён.
3. footnote-authoring -> примитивы (footnoteContentKey/makeFootnoteDefinition/
   generateFootnoteId) перенесены в пакетный footnote.ts, одна реализация конвенции.
4. parse-node-arg -> перенесён в prosemirror-markdown (не mcp: сервер CommonJS не
   импортирует ESM-only @docmost/mcp, но нативно импортирует пакет), обе копии
   удалены, консьюмеры перенаправлены.

canonicalizeFootnotes/ENFORCEMENT RULE #228 и comment-anchor/json-edit/text-normalize
(mcp) не тронуты. API-поверхность node-ops оставлена чистой для #409/#413.

closes #414

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 01:06:31 +03:00

105 lines
3.6 KiB
JavaScript

// Mock-HTTP test for the footnoteWarnings plumbing (#166). createPage is the
// representative path that is fully plain-HTTP (import + getPage) and so is
// mockable here; updatePage / importPageMarkdown attach footnoteWarnings with the
// IDENTICAL wiring (`footnoteWarningsField(...)` spread-when-non-empty) but run their
// mutation over the Hocuspocus collab WebSocket, which this plain-HTTP harness
// does not stand up. The analyzer itself is unit-tested in footnote-analyze.test.
import { test, after } from "node:test";
import assert from "node:assert/strict";
import http from "node:http";
import { DocmostClient } from "../../build/client.js";
function readBody(req) {
return new Promise((resolve) => {
let raw = "";
req.on("data", (c) => (raw += c));
req.on("end", () => resolve(raw));
});
}
function sendJson(res, status, obj, extraHeaders = {}) {
res.writeHead(status, { "Content-Type": "application/json", ...extraHeaders });
res.end(JSON.stringify(obj));
}
const openServers = [];
function spawn(handler) {
return new Promise((resolve) => {
const server = http.createServer(handler);
openServers.push(server);
server.listen(0, "127.0.0.1", () => {
const { port } = server.address();
resolve(`http://127.0.0.1:${port}/api`);
});
});
}
after(async () => {
await Promise.all(
openServers.map((s) => new Promise((r) => s.close(r))),
);
});
// A handler that imports a page, lets getPage read it back, and 404s everything
// else (listSidebarPages fails gracefully inside getPage).
function pageHandler() {
return async (req, res) => {
await readBody(req);
if (req.url === "/api/auth/login") {
sendJson(res, 200, { success: true }, {
"Set-Cookie": "authToken=t; Path=/; HttpOnly",
});
return;
}
if (req.url === "/api/pages/import") {
sendJson(res, 200, { data: { id: "new-1" } });
return;
}
if (req.url === "/api/pages/update") {
// The title-restore step after import.
sendJson(res, 200, { data: { id: "new-1" } });
return;
}
if (req.url === "/api/pages/info") {
sendJson(res, 200, {
data: {
id: "new-1",
slugId: "slug-1",
title: "T",
spaceId: "sp-1",
content: { type: "doc", content: [] },
},
});
return;
}
sendJson(res, 404, { message: "not found" });
};
}
test("createPage attaches footnoteWarnings when the content uses legacy footnote syntax", async () => {
const baseURL = await spawn(pageHandler());
const client = new DocmostClient(baseURL, "user@example.com", "pw");
// Legacy reference-style `[^id]:` definitions — inert on import since #293.
const content = ["Intro[^a].", "", "[^a]: a definition"].join("\n");
const result = await client.createPage("T", content, "sp-1");
assert.ok(Array.isArray(result.footnoteWarnings), "footnoteWarnings present");
const joined = result.footnoteWarnings.join("\n");
assert.match(joined, /reference-style footnotes/i);
assert.match(joined, /\^\[footnote text\]/); // nudge to the inline form
// The page itself is still returned.
assert.equal(result.success, true);
});
test("createPage omits footnoteWarnings when the content uses the inline form", async () => {
const baseURL = await spawn(pageHandler());
const client = new DocmostClient(baseURL, "user@example.com", "pw");
const content = "A note.^[the body] and reuse.^[the body]";
const result = await client.createPage("T", content, "sp-1");
assert.equal(
"footnoteWarnings" in result,
false,
"no footnoteWarnings field on inline-footnote input",
);
assert.equal(result.success, true);
});