Files
gitmost/packages/git-sync/test/strip-empty-paragraphs-validity.test.ts
claude code agent 227 1bc664d25d test(git-sync): exhaustive converter coverage + fix 3 round-trip data-loss bugs
Coder↔reviewer design loop (9 rounds, reviewer verdict: exhaustive) produced
92 specs; implemented +123 tests (465 -> 588 passing). The new round-trip
coverage exposed three genuine data-loss bugs in the Markdown<->ProseMirror
converter, all now FIXED (round-trip is lossless for these):

1. pageBreak was lost on export (no converter case -> rendered to "" and the
   node vanished). Now emits <div data-type="pageBreak"></div>, which the schema
   parses back -> round-trips.
2. A block image between blocks left an empty <p> artifact after import-hoisting,
   producing a phantom blank-gap diff on every sync. markdownToProseMirror now
   strips content-less paragraphs after generateJSON — with a schema-validity
   guard that keeps the obligatory single empty paragraph in `content: "block+"`
   containers (tableCell/tableHeader/blockquote/column/callout/doc), so empty
   cells/quotes never become an invalid `content: []`.
3. The `code` mark combined with another mark was not byte-stable (emitted nested
   HTML that the schema's `code` `excludes:"_"` collapsed on import). The
   converter now emits code-only when `code` co-occurs, matching the editor.

New coverage spans media/diagram/details/columns/math/mention attribute
round-trips, converter emission branches, git error paths, and engine decision
branches. A dedicated test pins the empty-container schema validity (the review
catch on the bug-2 fix).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 05:30:28 +03:00

58 lines
2.5 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { getSchema } from "@tiptap/core";
import { markdownToProseMirror } from "../src/lib/markdown-to-prosemirror";
import { docmostExtensions } from "../src/lib/docmost-schema";
// REGRESSION LOCK for the stripEmptyParagraphs schema-validity guard.
//
// markdownToProseMirror removes empty `paragraph` nodes that the import leaves
// behind when a block atom (e.g. a block image) is hoisted out of marked's
// wrapping <p> — they cause phantom blank-gap diffs on every sync. But several
// schema nodes REQUIRE non-empty block content (`content: "block+"`): tableCell,
// tableHeader, blockquote, column, callout, and the doc root. For an empty one of
// those, generateJSON materializes a single empty paragraph as its OBLIGATORY
// content. Stripping that would produce a schema-INVALID doc (`content: []`),
// which crashes any consumer that validates the public markdownToProseMirror
// output via ProseMirror's Node.check() / nodeFromJSON. The guard keeps one empty
// paragraph when removal would empty such a container; these tests pin that.
const schema = getSchema(docmostExtensions as any);
/** Throws if the JSON doc is not valid against the Docmost schema. */
function assertSchemaValid(doc: unknown): void {
schema.nodeFromJSON(doc).check();
}
describe("stripEmptyParagraphs keeps the import schema-valid", () => {
it("an empty GFM table cell round-trips to a schema-valid doc", async () => {
const doc = await markdownToProseMirror(
"| a | |\n|---|---|\n| x | y |\n",
);
expect(() => assertSchemaValid(doc)).not.toThrow();
});
it("an empty blockquote stays schema-valid", async () => {
const doc = await markdownToProseMirror("> \n");
expect(() => assertSchemaValid(doc)).not.toThrow();
});
it("an empty document stays schema-valid", async () => {
const doc = await markdownToProseMirror("\n\n");
expect(() => assertSchemaValid(doc)).not.toThrow();
});
it("still removes the empty hoist-artifact paragraph beside a block image", async () => {
const doc = await markdownToProseMirror("p\n\n![x](http://a.aa)\n\nq\n");
const emptyParas = ((doc as { content?: any[] }).content ?? []).filter(
(n: any) =>
n.type === "paragraph" &&
(!Array.isArray(n.content) || n.content.length === 0),
);
// The artifact paragraph must be gone (no phantom blank-gap on re-export)...
expect(emptyParas).toHaveLength(0);
// ...and the result is still a valid doc.
expect(() => assertSchemaValid(doc)).not.toThrow();
});
});