Files
gitmost/packages/git-sync/test/diagram-roundtrip.test.ts
T
claude code agent 227 24b903aaf3 build(git-sync): land the @docmost/git-sync package into develop, code-only (#326 step 1 / PR-A)
The git-sync converter + engine source lived only on the #119 branch; develop
had just the dead compiled build/. Bring the whole package (src + ~700 tests)
onto develop under CI, with NO consumer wired — git-sync stays fully inert in
develop (nothing in apps/server imports it), so runtime behavior is unchanged.
This unblocks #293 (extract the shared converter package from the landed source)
and lets #119's functionality land LAST, already writing the canonical format
(per the #326 landing order).

- packages/git-sync: src (lib converter + engine) + test corpus + configs.
- Remove develop's dead committed packages/git-sync/build/; gitignore it
  (built in CI/Docker via pnpm build, never committed — no src/build drift).
- pnpm-lock.yaml: add the @docmost/git-sync importer (a missing workspace
  package in the lock is a CI blocker). `pnpm install --frozen-lockfile` passes.
- NO server integration / loader / Dockerfile runtime changes (those come with
  #119 at step 6).

Verified: tsc clean; vitest 711 passed | 1 expected-fail, 0 failures, 0 type
errors; pnpm --frozen-lockfile EXIT 0; apps/server has no git-sync import.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 06:21:41 +03:00

110 lines
4.6 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
convertProseMirrorToMarkdown,
markdownToProseMirror,
docsCanonicallyEqual,
} from 'docmost-client';
// Helper mirroring the convention in markdown-converter.test.ts: wrap atoms in
// a top-level doc node so convertProseMirrorToMarkdown (which requires
// content.content) walks them.
const doc = (...nodes: any[]) => ({ type: 'doc', content: nodes });
describe('diagram round-trip (docmost-schema diagramAttributes)', () => {
// SPEC case 1: drawio carrying the full numeric-attr surface
// (data-width/data-height/data-size/data-aspect-ratio) that it shares with
// audio/video/pdf but which no fixture exercises on a diagram node.
it('drawio round-trips numeric attrs, coercing number -> string via getAttribute', async () => {
const input = doc({
type: 'drawio',
attrs: {
src: '/d.drawio',
attachmentId: 'att-1',
width: 640,
height: 480,
size: 1234,
aspectRatio: 1.777,
align: 'center',
},
});
const md1 = convertProseMirrorToMarkdown(input);
const doc2 = await markdownToProseMirror(md1);
const md2 = convertProseMirrorToMarkdown(doc2);
// Exact serialized form: numbers render as bare data-* values; attribute
// order follows the converter's emit order (src, then width/height/size/
// aspect-ratio/align, then attachment-id).
expect(md1).toBe(
'<div data-type="drawio" data-src="/d.drawio" data-width="640" data-height="480" data-size="1234" data-aspect-ratio="1.777" data-align="center" data-attachment-id="att-1"></div>',
);
// A second export reproduces the first byte-for-byte (drawio align default
// is already "center", so nothing new materializes on import).
expect(md2).toBe(md1);
// Re-import coerces every numeric attr to a STRING because parseHTML reads
// them via getAttribute(). This is the gap the reviewer flagged: the
// number -> string coercion on a diagram node is otherwise untested.
const attrs2 = doc2.content[0].attrs;
expect(attrs2.width).toBe('640');
expect(attrs2.height).toBe('480');
expect(attrs2.size).toBe('1234');
expect(attrs2.aspectRatio).toBe('1.777');
expect(typeof attrs2.width).toBe('string');
expect(typeof attrs2.aspectRatio).toBe('string');
// String attrs pass through unchanged.
expect(attrs2.align).toBe('center');
expect(attrs2.attachmentId).toBe('att-1');
// Canonically NOT equal: the numeric -> string coercion survives
// canonicalization (only align='center' is normalized away via
// KNOWN_DEFAULTS.drawio), so 640 !== '640' makes the docs differ.
expect(docsCanonicallyEqual(input, doc2)).toBe(false);
});
// SPEC case 2: minimal excalidraw atom with ONLY string attrs (no align, no
// numeric attrs). Locks the one-time export divergence (align='center'
// default materializes only on import) plus escapeAttr of title/alt through
// the data-title/data-alt path.
it('excalidraw materializes align default only on import and escapes title/alt', async () => {
const input = doc({
type: 'excalidraw',
attrs: {
src: '/e.excalidraw',
title: 'My "Diagram"',
alt: 'a&b',
},
});
const md1 = convertProseMirrorToMarkdown(input);
const doc2 = await markdownToProseMirror(md1);
const md2 = convertProseMirrorToMarkdown(doc2);
// First export: no align emitted (the input doc carries no align), and the
// " in title becomes &quot;, the & in alt becomes &amp; via escapeAttr.
expect(md1).toBe(
'<div data-type="excalidraw" data-src="/e.excalidraw" data-title="My &quot;Diagram&quot;" data-alt="a&amp;b"></div>',
);
// Second export: align='center' has now materialized (the schema's
// diagramAttributes default), so md2 gains a data-align="center" suffix and
// is NOT byte-equal to md1. This one-time divergence is the diagram quirk.
expect(md2).toBe(
'<div data-type="excalidraw" data-src="/e.excalidraw" data-title="My &quot;Diagram&quot;" data-alt="a&amp;b" data-align="center"></div>',
);
expect(md2).not.toBe(md1);
// Re-import decodes the escaped entities back to the original characters.
const attrs2 = doc2.content[0].attrs;
expect(attrs2.title).toBe('My "Diagram"');
expect(attrs2.alt).toBe('a&b');
expect(attrs2.align).toBe('center');
// Canonically EQUAL: align='center' is normalized away via
// KNOWN_DEFAULTS.excalidraw, and title/alt are non-default strings that
// survive on both sides, so the docs are semantically equal.
expect(docsCanonicallyEqual(input, doc2)).toBe(true);
});
});