Batch 2-4 of the test-strategy rollout. Test-only except one minimal, behaviour-preserving extraction in file.utils.ts. All suites green: server 82 suites/836+1todo, editor-ext 86, mcp 270, client (new files) 86. integrations (server): - file.utils.ts: extract pure `isEntryPathSafe(entryName, targetDir)` from extractZipInternal so the zip-slip/path-traversal guard is unit-testable; call site rerouted, behaviour identical (only a warn-message string merged). - file.utils.zip-safety.spec.ts: traversal/strip/__MACOSX/prefix-confusion cases (mutation-resistant: fails if containment loses the path.sep). - import-formatter / import.utils / table-utils / export utils / import.service extractTitleAndRemoveHeading: pure import/export transforms, Notion/XWiki formatting, table colspan widths (idempotent), slug/link rewriting. client: - safeRedirectPath: open-redirect guard, every reject branch independently. - buildChatMarkdown (fence anti-breakout), label-colors, normalize-label, share tree build, page URL builders, notification time-grouping (fake clock). packages: - editor-ext: deriveFootnoteId golden table, parseHtmlEmbedHeight crafted values, orphan footnote extraction. - mcp: deriveFootnoteId parity (drift guard vs editor-ext), applyTextEdits idempotency + cross-block replaceAll, diffDocs/summarizeChange on reorder. Reviewed (APPROVE): extraction behaviour-preserving, assertions mutation-resistant. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
89 lines
4.0 KiB
JavaScript
89 lines
4.0 KiB
JavaScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import { diffDocs, summarizeChange } from "../../build/lib/diff.js";
|
|
|
|
const t = (text, marks) => (marks ? { type: "text", text, marks } : { type: "text", text });
|
|
const para = (s) => ({ type: "paragraph", content: [t(s)] });
|
|
const doc = (...c) => ({ type: "doc", content: c });
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Block REORDER (A,B -> B,A): the two documents contain the SAME blocks in a
|
|
// different order. A naive set-based comparison would call this "no content
|
|
// change" (the multiset of blocks is identical), which is wrong: the reader's
|
|
// document order changed. The changeset-based diff must report it as a real
|
|
// change and the integrity-/value-based summary must NOT claim "no content
|
|
// change".
|
|
// ---------------------------------------------------------------------------
|
|
const A = para("Alpha paragraph content one");
|
|
const B = para("Beta paragraph content two");
|
|
const before = doc(A, B);
|
|
const after = doc(B, A); // identical blocks, swapped order
|
|
|
|
test("diffDocs on a block swap does NOT report 'no textual changes'", () => {
|
|
const r = diffDocs(before, after);
|
|
assert.doesNotMatch(
|
|
r.markdown,
|
|
/no textual changes/i,
|
|
"a reorder is a content change, not a no-op",
|
|
);
|
|
// The reorder surfaces as both an insertion and a deletion (text moved).
|
|
assert.ok(r.summary.inserted > 0, "reports inserted chars");
|
|
assert.ok(r.summary.deleted > 0, "reports deleted chars");
|
|
const ops = new Set(r.changes.map((c) => c.op));
|
|
assert.ok(ops.has("insert") && ops.has("delete"), "has both insert and delete changes");
|
|
});
|
|
|
|
test("diffDocs reorder: summary fields are coherent (blocksChanged > 0, counts > 0)", () => {
|
|
const r = diffDocs(before, after);
|
|
assert.ok(r.summary.blocksChanged > 0, "blocksChanged must be positive for a reorder");
|
|
// Symmetric move: the moved text is both inserted and deleted, so the two
|
|
// counts are equal. (The diff algorithm chooses ONE of the two equal-status
|
|
// blocks to represent as "moved", so we assert the count equals one of the
|
|
// block lengths rather than hard-coding which block moved.)
|
|
assert.equal(
|
|
r.summary.inserted,
|
|
r.summary.deleted,
|
|
"a pure move inserts and deletes the same number of chars",
|
|
);
|
|
const blockLens = ["Alpha paragraph content one".length, "Beta paragraph content two".length];
|
|
assert.ok(
|
|
blockLens.includes(r.summary.inserted),
|
|
`moved char count ${r.summary.inserted} should equal one of the block lengths ${JSON.stringify(blockLens)}`,
|
|
);
|
|
});
|
|
|
|
test("summarizeChange on a block swap reports changed:true, NOT 'no content change'", () => {
|
|
const rep = summarizeChange(before, after);
|
|
assert.equal(rep.changed, true, "a reorder is a change");
|
|
assert.notEqual(rep.summary, "no content change");
|
|
assert.match(rep.summary, /^changed:/, "summary is a 'changed: ...' line");
|
|
// blocksChanged is coherent with diffDocs.
|
|
assert.ok(rep.blocksChanged > 0, "blocksChanged > 0");
|
|
assert.equal(rep.textInserted, rep.textDeleted, "symmetric move");
|
|
assert.ok(rep.textInserted > 0, "text counts > 0");
|
|
});
|
|
|
|
test("control: an IDENTICAL doc (no reorder) reports no content change", () => {
|
|
// Guards the reorder assertions from being vacuously true: the same docs in
|
|
// the SAME order must still cleanly report no change.
|
|
const rep = summarizeChange(before, before);
|
|
assert.equal(rep.changed, false);
|
|
assert.equal(rep.summary, "no content change");
|
|
const r = diffDocs(before, before);
|
|
assert.equal(r.summary.blocksChanged, 0);
|
|
assert.equal(r.changes.length, 0);
|
|
});
|
|
|
|
test("a three-block rotation (A,B,C -> C,A,B) is reported as a change", () => {
|
|
const C = para("Gamma paragraph content three");
|
|
const d1 = doc(A, B, C);
|
|
const d2 = doc(C, A, B);
|
|
const rep = summarizeChange(d1, d2);
|
|
assert.equal(rep.changed, true);
|
|
assert.notEqual(rep.summary, "no content change");
|
|
const r = diffDocs(d1, d2);
|
|
assert.ok(r.summary.blocksChanged > 0);
|
|
assert.doesNotMatch(r.markdown, /no textual changes/i);
|
|
});
|