4b2af3d34a
Аудит при подготовке #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>
105 lines
3.6 KiB
JavaScript
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);
|
|
});
|