Must-fix: - REAL BUG: insertInlineFootnote could splice a footnoteReference (inline atom) into a codeBlock or an existing footnoteDefinition, persisting a schema-invalid doc (insert_footnote skips validateDocStructure). Now the search is bounded to the BODY (before the first footnotesList) and the insertNodesAfterAnchor core refuses textblocks that can't hold the atom (codeBlock); when the only match is in such a place the insert returns inserted:false and the write aborts cleanly. Reachable via docmost_transform too. Added codeBlock / definition / fall-through tests. - Fixed the deepEqualJson doc comment in both copies: arrays are order-SENSITIVE (correctness depends on it), only object keys are order-insensitive. - README.ru.md MCP tool count 38 -> 39 (lines 36/47/63), matching README.md/AGENTS. - CHANGELOG [Unreleased] Added entry for insert_footnote + server-side footnote canonicalization on non-editor write paths (#228). Suggestions: - canonicalize step 5/7 now strips footnotesList at ANY depth (both copies), so a schema-valid list nested in a callout/blockquote can't leave duplicate defs. - Exclude the test-only footnote-corpus.ts fixture from the editor-ext build (tsconfig), so it no longer ships in dist/. - Removed the duplicate manual canonicalize cases from the MCP unit test (the shared corpus covers them via full deepEqual); kept idempotence + immutability. - insertInlineFootnote dedup key now keys off the inline array directly (footnoteContentKey({ content: inline })) instead of a throwaway node. Tests / architecture: - New client-wrapper test (#9): overrides a small mutatePage seam to assert the not-found path throws and persists NOTHING, and the success path shapes footnoteId/reused/message/verify and writes the right content. Fixed the misleading comment in footnote-write.test.mjs. - B: cross-copy corpus parity guard test (loads both corpora, asserts deep-equal) so a typo in one copy can't pass both suites green. - A: declined — the full-vs-fragment decision lives at the call site, so a prepareDocForPersist wrapper would be a bare alias for canonicalizeFootnotes; kept the existing per-call-site comments instead. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
50 lines
2.2 KiB
JavaScript
50 lines
2.2 KiB
JavaScript
// CI guard for architecture item B: the shared golden corpus is duplicated (the
|
|
// canonical TS copy in editor-ext + the MCP .mjs mirror), so a typo in one copy
|
|
// would otherwise pass BOTH per-package suites green while silently breaking the
|
|
// cross-copy invariant. This test loads BOTH copies and asserts they are
|
|
// deep-equal, turning "the two corpora stay identical" into a checked property.
|
|
//
|
|
// The editor-ext copy is a .ts module (not importable from node:test), so it is
|
|
// read as text and its array literal — which is pure JSON produced by
|
|
// JSON.stringify — is parsed out directly.
|
|
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import { fileURLToPath } from "node:url";
|
|
import { dirname, resolve } from "node:path";
|
|
|
|
import { FOOTNOTE_CORPUS as MCP_CORPUS } from "./footnote-corpus.mjs";
|
|
|
|
function loadEditorExtCorpus() {
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
const tsPath = resolve(
|
|
here,
|
|
"../../../editor-ext/src/lib/footnote/footnote-corpus.ts",
|
|
);
|
|
const src = readFileSync(tsPath, "utf8");
|
|
// The value is `export const FOOTNOTE_CORPUS: FootnoteCorpusCase[] = [ ... ];`
|
|
// where `[ ... ]` is strict JSON (JSON.stringify output). Slice from the
|
|
// assignment's opening bracket to the final closing bracket and parse.
|
|
const assignAt = src.indexOf("] = ");
|
|
assert.ok(assignAt >= 0, "could not locate the editor-ext corpus assignment");
|
|
const jsonStart = src.indexOf("[", assignAt + 3);
|
|
const jsonEnd = src.lastIndexOf("]");
|
|
assert.ok(jsonStart >= 0 && jsonEnd > jsonStart, "could not bound the corpus array");
|
|
return JSON.parse(src.slice(jsonStart, jsonEnd + 1));
|
|
}
|
|
|
|
test("the editor-ext and MCP golden corpora are byte-for-byte identical", () => {
|
|
const editorExt = loadEditorExtCorpus();
|
|
assert.ok(Array.isArray(editorExt) && editorExt.length > 0, "editor-ext corpus is non-empty");
|
|
assert.equal(
|
|
MCP_CORPUS.length,
|
|
editorExt.length,
|
|
"the two corpora must have the same number of cases",
|
|
);
|
|
assert.deepEqual(
|
|
MCP_CORPUS,
|
|
editorExt,
|
|
"the MCP corpus mirror has drifted from the editor-ext canonical copy — re-sync them",
|
|
);
|
|
});
|