Files
gitmost/packages/editor-ext/src/lib/footnote/footnote-util.derive-id.test.ts
claude_code 0b2af34029 test(integrations/client/packages): batch 2-4 unit coverage + zip-slip guard extraction
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>
2026-06-21 18:22:15 +03:00

92 lines
3.7 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { deriveFootnoteId } from "./footnote-util";
/**
* GOLDEN TABLE for `deriveFootnoteId` (and its private alphabetic `suffix`).
*
* deriveFootnoteId is DELIBERATELY duplicated in
* packages/mcp/src/lib/collaboration.ts
* and the two copies MUST stay byte-for-byte equivalent in behavior so the same
* markdown imported through the editor and through the MCP path yields identical
* footnote ids. This table is the SHARED contract: the parity test
* packages/mcp/test/unit/derive-id-parity.test.mjs
* pins the exact SAME (input -> expected) pairs against the COMPILED mcp build.
* If either copy drifts, one of the two tests goes red.
*
* Keep this constant in sync with GOLDEN in the mcp parity test.
*/
export const DERIVE_GOLDEN: Array<{
originalId: string;
occurrence: number;
taken: string[];
expected: string;
why: string;
}> = [
// Base candidate `${id}__${occurrence}` when nothing collides.
{ originalId: "d", occurrence: 2, taken: [], expected: "d__2", why: "plain base, second occurrence" },
{ originalId: "d", occurrence: 3, taken: [], expected: "d__3", why: "plain base, third occurrence" },
// The base is taken -> first alphabetic bump is "b" (NOT "a": suffix starts at 'b').
{ originalId: "d", occurrence: 2, taken: ["d__2"], expected: "d__2b", why: "base taken -> first bump 'b'" },
// Base + first bump taken -> "c".
{ originalId: "d", occurrence: 2, taken: ["d__2", "d__2b"], expected: "d__2c", why: "base+b taken -> 'c'" },
// A non-contiguous taken set still walks deterministically to the first free slot.
{
originalId: "d",
occurrence: 2,
taken: ["d__2", "d__2b", "d__2c", "d__2d"],
expected: "d__2e",
why: "base + b,c,d taken -> 'e'",
},
// >25 bump: base + b..z (the 25 single-letter suffixes) all taken -> "bb".
// suffix(26) === "bb" (base-25 over b..z, carrying to a two-letter suffix).
{
originalId: "d",
occurrence: 2,
taken: ["d__2", ...singleLetterSuffixes().map((s) => `d__2${s}`)],
expected: "d__2bb",
why: ">25 collisions -> two-letter suffix 'bb'",
},
];
/** The 25 single-letter suffixes the scheme uses: b, c, ..., z (n = 1..25). */
function singleLetterSuffixes(): string[] {
// Mirror of the production suffix() for n in 1..25 (all single letters).
// n=1 -> 'b' ... n=25 -> 'z'. Used only to BUILD the taken-set for the
// >25 row; the EXPECTED value (d__2bb) is asserted against the real function.
return Array.from({ length: 25 }, (_, i) => String.fromCharCode(98 + i));
}
describe("deriveFootnoteId golden table (cross-package drift guard)", () => {
for (const row of DERIVE_GOLDEN) {
it(`derive("${row.originalId}", ${row.occurrence}, {${row.taken.join(",")}}) === "${row.expected}" — ${row.why}`, () => {
const got = deriveFootnoteId(
row.originalId,
row.occurrence,
new Set(row.taken),
);
expect(got).toBe(row.expected);
});
}
it("the >25 row's taken-set really contains b..z (25 single letters) plus the base", () => {
// Sanity-pin the construction so a typo in singleLetterSuffixes() cannot make
// the >25 assertion pass for the wrong reason.
const letters = singleLetterSuffixes();
expect(letters).toHaveLength(25);
expect(letters[0]).toBe("b");
expect(letters[24]).toBe("z");
});
it("is a PURE function: it never mutates the taken set it is given", () => {
const taken = new Set(["d__2"]);
const before = [...taken];
deriveFootnoteId("d", 2, taken);
expect([...taken]).toEqual(before);
});
it("is deterministic: same input -> same output across calls", () => {
const mk = () => new Set(["d__2", "d__2b"]);
expect(deriveFootnoteId("d", 2, mk())).toBe(deriveFootnoteId("d", 2, mk()));
});
});