Files
gitmost/packages/editor-ext/src/lib/unique-id/unique-id.util.test.ts
claude code agent 227 d32ad73158 test(editor-ext): cover the transclusionSource NO_REASSIGN branch in id dedupe (#206)
A colliding transclusionSource id is deliberately NOT reassigned (its id is a
cross-reference key), while a missing id is still filled. Add coverage for both:
two sources sharing an id keep it (red if the NO_REASSIGN guard is removed), and
a source with no id gets a fresh one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 17:01:02 +03:00

104 lines
4.1 KiB
TypeScript

import { describe, it, expect } from "vitest";
import StarterKit from "@tiptap/starter-kit";
import { addUniqueIdsToDoc } from "./unique-id.util";
import { UniqueID } from "./unique-id";
import { TransclusionSource } from "../transclusion/transclusion-source";
// Minimal extension set: StarterKit (paragraph/heading) + the UniqueID config
// the server uses for the addressing anchors.
const extensions = [
StarterKit,
UniqueID.configure({ types: ["heading", "paragraph"] }),
];
// `transclusionSource` is also an addressed type, but its id is a cross-reference
// KEY (a transclusionReference / the page_transclusions table resolves a source
// by it), so it lives in the NO_REASSIGN set: a missing id is filled, a colliding
// id is NOT reassigned (rewriting it would orphan its references).
const extensionsWithSource = [
StarterKit,
// Narrow the content expression to `paragraph+` so the schema builds from
// StarterKit alone (the real allow-list references image/table/etc. nodes this
// minimal harness doesn't register). The node name — what NO_REASSIGN keys on
// — is unchanged.
TransclusionSource.extend({ content: "paragraph+" }),
UniqueID.configure({
types: ["heading", "paragraph", "transclusionSource"],
}),
];
const para = (id: string | undefined, text: string) => ({
type: "paragraph",
...(id !== undefined ? { attrs: { id } } : {}),
content: [{ type: "text", text }],
});
const source = (id: string | undefined, text: string) => ({
type: "transclusionSource",
...(id !== undefined ? { attrs: { id } } : {}),
// The schema requires at least one block child (content expression is `+`).
content: [{ type: "paragraph", content: [{ type: "text", text }] }],
});
const ids = (doc: any): (string | undefined)[] =>
(doc.content ?? []).map((n: any) => n.attrs?.id);
describe("addUniqueIdsToDoc", () => {
it("fills ids on nodes that are missing one", () => {
const doc = { type: "doc", content: [para(undefined, "a"), para(undefined, "b")] };
const out = addUniqueIdsToDoc(doc, extensions);
const [a, b] = ids(out);
expect(a).toBeTruthy();
expect(b).toBeTruthy();
expect(a).not.toBe(b);
});
it("deduplicates two nodes that share the same id (#206 editor-pm-7)", () => {
// A copy/paste or bulk-JSON duplicate keeps the original id on both nodes.
const doc = {
type: "doc",
content: [para("dup", "first"), para("dup", "second")],
};
const out = addUniqueIdsToDoc(doc, extensions);
const [first, second] = ids(out);
// The first occurrence keeps the id (stable anchor); the duplicate is
// reassigned a fresh one so MCP addressing can't hit the wrong/both nodes.
expect(first).toBe("dup");
expect(second).toBeTruthy();
expect(second).not.toBe("dup");
});
it("leaves already-unique ids untouched", () => {
const doc = {
type: "doc",
content: [para("x1", "first"), para("x2", "second")],
};
const out = addUniqueIdsToDoc(doc, extensions);
expect(ids(out)).toEqual(["x1", "x2"]);
});
it("does NOT reassign a colliding transclusionSource id — BOTH keep it (NO_REASSIGN)", () => {
// Two sync-block sources sharing an id: rewriting either would orphan the
// transclusionReferences / page_transclusions rows that resolve a source by
// this key, so the dedupe MUST leave both ids intact. If the NO_REASSIGN
// guard is removed, the second source is reassigned a fresh id and this fails.
const doc = {
type: "doc",
content: [source("src", "first"), source("src", "second")],
};
const out = addUniqueIdsToDoc(doc, extensionsWithSource);
const [first, second] = ids(out);
expect(first).toBe("src");
expect(second).toBe("src");
});
it("still FILLS a missing id on a transclusionSource (only reassignment is suppressed)", () => {
// NO_REASSIGN suppresses dedupe of an EXISTING id, not filling a missing one:
// a source with no id still needs a key its references can resolve.
const doc = { type: "doc", content: [source(undefined, "only")] };
const out = addUniqueIdsToDoc(doc, extensionsWithSource);
const [id] = ids(out);
expect(id).toBeTruthy();
});
});