diff --git a/packages/editor-ext/src/lib/unique-id/unique-id.util.test.ts b/packages/editor-ext/src/lib/unique-id/unique-id.util.test.ts index eddc1dc9..24d30408 100644 --- a/packages/editor-ext/src/lib/unique-id/unique-id.util.test.ts +++ b/packages/editor-ext/src/lib/unique-id/unique-id.util.test.ts @@ -2,6 +2,7 @@ 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. @@ -10,12 +11,35 @@ const extensions = [ 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); @@ -52,4 +76,28 @@ describe("addUniqueIdsToDoc", () => { 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(); + }); });