Files
gitmost/packages/mcp/test/unit/footnote-corpus-parity.test.mjs
a a77a0bc92b fix(footnotes): re-review #232 — refuse footnoteRef into codeBlock/definition, deep-strip nested lists, docs + cross-copy guard (#228)
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>
2026-06-27 21:41:10 +03:00

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",
);
});