- footnote-sync: remove the now-dead `refReids` (CollisionPlan field, local, return, the 6a consumer loop) — references are never re-id'd under reuse, so it was dead structure on the hot reconciliation path. Rewrite the stale comments (plugin header, step 0, refOccurrences field) that still described the old "duplicates re-id'd so both survive" model to the reuse model. - Shared footnote lexer: new packages/mcp/src/lib/footnote-lex.ts (lexFootnoteLines + forEachFootnoteReference). extractFootnotes (collaboration) and analyzeFootnotes now consume the SAME fence-aware lexer, so "the analyzer sees exactly what the importer keeps/strips" is structural, not comment-kept. Removed the duplicated DEF_RE/fence machine from both consumers. - Tests: new mock test for the footnoteWarnings plumbing on createPage (problems -> field present; clean -> omitted); new paste-reuse case for TWO colliding pasted definitions (reservation -> distinct ids). Updated the derive-id golden test header (no MCP copy / parity test anymore). - CHANGELOG: [Unreleased] entries for footnote reuse (Changed, supersedes 0.93.0) and footnoteWarnings (Added). editor-ext 129, MCP 301, server roundtrip 2; client+server tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
111 lines
3.7 KiB
JavaScript
111 lines
3.7 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 (`analyzeFootnotes(...)` + 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 has footnote problems", async () => {
|
|
const baseURL = await spawn(pageHandler());
|
|
const client = new DocmostClient(baseURL, "user@example.com", "pw");
|
|
// A dangling reference + a duplicate definition + a table marker.
|
|
const content = [
|
|
"Intro[^missing] and| cell[^t] |.",
|
|
"",
|
|
"[^d]: one",
|
|
"[^d]: two",
|
|
"[^t]: in table",
|
|
].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, /no matching definition/); // dangling [^missing]
|
|
assert.match(joined, /defined more than once/); // duplicate [^d]
|
|
// The page itself is still returned.
|
|
assert.equal(result.success, true);
|
|
});
|
|
|
|
test("createPage omits footnoteWarnings when the content is clean", async () => {
|
|
const baseURL = await spawn(pageHandler());
|
|
const client = new DocmostClient(baseURL, "user@example.com", "pw");
|
|
const content = ["A[^a] and reuse[^a].", "", "[^a]: fine"].join("\n");
|
|
const result = await client.createPage("T", content, "sp-1");
|
|
assert.equal(
|
|
"footnoteWarnings" in result,
|
|
false,
|
|
"no footnoteWarnings field on clean input",
|
|
);
|
|
assert.equal(result.success, true);
|
|
});
|