Files
gitmost/packages/git-sync/test/redteam-converter.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

90 lines
3.8 KiB
TypeScript

import { describe, expect, it } from 'vitest';
// Import the converter DIRECTLY from src (NOT the docmost-client barrel, which
// pulls in collaboration.ts and mutates the global DOM at import time), matching
// the other converter unit tests. markdownToProseMirror is imported for the
// round-trip cases; loading it mutates the global DOM via jsdom (required for
// @tiptap/html's generateJSON under Node) — this is expected.
import { convertProseMirrorToMarkdown } from '../src/lib/markdown-converter.js';
import { markdownToProseMirror } from '../src/lib/markdown-to-prosemirror.js';
const doc = (...nodes: any[]) => ({ type: 'doc', content: nodes });
// ---------------------------------------------------------------------------
// #1 editor-ext atoms dropped: the `default` branch (markdown-converter.ts
// ~584-586) collapses unknown atoms to "" by mapping their (empty) children.
// ---------------------------------------------------------------------------
describe('#1 editor-ext atoms dropped', () => {
it('preserves an inline status atom text', () => {
const d = doc({
type: 'paragraph',
content: [{ type: 'status', attrs: { text: 'Done' } }],
});
expect(convertProseMirrorToMarkdown(d)).toContain('Done');
});
it('preserves a block htmlEmbed atom', () => {
const d = doc({ type: 'htmlEmbed', attrs: { source: '<b>hi</b>' } });
expect(convertProseMirrorToMarkdown(d)).not.toBe('');
});
it('preserves a footnoteReference atom', () => {
const d = doc({
type: 'paragraph',
content: [{ type: 'footnoteReference', attrs: { id: 'fn1', referenceNumber: 1 } }],
});
expect(convertProseMirrorToMarkdown(d)).not.toBe('');
});
});
// ---------------------------------------------------------------------------
// #2 top-level image attrs lost: a top-level image emits markdown ![](src),
// which carries no width/height/align/attachmentId.
// ---------------------------------------------------------------------------
describe('#2 top-level image attrs lost', () => {
it('keeps width through export and re-import', async () => {
const d = doc({
type: 'image',
attrs: { src: '/files/x.png', width: '320', height: '200', align: 'right', attachmentId: 'a1' },
});
const md = convertProseMirrorToMarkdown(d);
expect(md).toContain('320');
const back = await markdownToProseMirror(md);
expect(back.content[0].attrs.width).toBe('320');
});
});
// ---------------------------------------------------------------------------
// #3 code-fence corruption: a code block whose TEXT contains a ``` fence must
// be emitted with a wider outer fence so the inner fence survives.
// ---------------------------------------------------------------------------
describe('#3 code-fence corruption', () => {
it('round-trips a code block containing an inner fence', async () => {
const code = '```js\nfoo()\n```';
const d = doc({
type: 'codeBlock',
attrs: { language: '' },
content: [{ type: 'text', text: code }],
});
const md1 = convertProseMirrorToMarkdown(d);
const back = await markdownToProseMirror(md1);
const md2 = convertProseMirrorToMarkdown(back);
expect(md2).toBe(md1);
});
});
// ---------------------------------------------------------------------------
// #16 depth guard: deep recursion in processNode overflows the stack (today a
// RangeError) instead of being guarded.
// ---------------------------------------------------------------------------
describe('#16 depth guard', () => {
it('does not throw on a deeply nested blockquote doc', () => {
const DEPTH = 50000;
let node: any = { type: 'paragraph', content: [{ type: 'text', text: 'x' }] };
for (let i = 0; i < DEPTH; i++) {
node = { type: 'blockquote', content: [node] };
}
const d = doc(node);
expect(() => convertProseMirrorToMarkdown(d)).not.toThrow();
});
});