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>
This commit is contained in:
@@ -234,3 +234,92 @@ describe('VaultGit integration gaps (temp repo)', () => {
|
||||
expect(globalName).not.toBe(LOCAL_NAME);
|
||||
});
|
||||
});
|
||||
|
||||
// Parser/error-fallback gaps for `git.ts` exercised WITHOUT a real git binary by
|
||||
// monkey-patching the private `runRaw` primitive (every git invocation funnels
|
||||
// through it, per the module header). These pin defensive arms the accepted
|
||||
// integration specs above could not reach: the unknown-status consume in the
|
||||
// `-z` walk, and the `|| r.stdout` empty-stderr error-detail fallbacks.
|
||||
describe('VaultGit parser/error-fallback gaps (runRaw stubbed)', () => {
|
||||
// --- 1. diffNameStatus: unknown status (T) sandwiched between A and M --------
|
||||
//
|
||||
// Protects the default arm of the status switch (git.ts ~lines 497-502): an
|
||||
// unknown status like `T` (type-change) consumes ONE path token defensively
|
||||
// but emits nothing. If the walk pulled the wrong count here it would desync
|
||||
// and misclassify the trailing M row.
|
||||
it('diffNameStatus swallows an unknown T status mid-stream and stays aligned', async () => {
|
||||
const git = new VaultGit('/tmp/any');
|
||||
(git as any).runRaw = async () => ({
|
||||
code: 0,
|
||||
// A\0a.md T\0t.md M\0m.md — T is the unknown status mid-stream.
|
||||
stdout: 'A\0a.md\0T\0t.md\0M\0m.md\0',
|
||||
stderr: '',
|
||||
});
|
||||
|
||||
const entries = await git.diffNameStatus('X', 'Y');
|
||||
|
||||
// The T row's path token 't.md' is consumed but NOT emitted; the walk stays
|
||||
// aligned so the trailing M/m.md parses cleanly (no off-by-one).
|
||||
expect(entries).toEqual([
|
||||
{ status: 'A', path: 'a.md' },
|
||||
{ status: 'M', path: 'm.md' },
|
||||
]);
|
||||
expect(entries.length).toBe(2);
|
||||
expect(entries.some((e) => e.status === ('T' as any))).toBe(false);
|
||||
expect(entries.some((e) => e.path === 't.md')).toBe(false);
|
||||
});
|
||||
|
||||
// --- 2. diffNameStatus: unknown status (T) FIRST in the stream --------------
|
||||
//
|
||||
// Leading-position variant: a `T` at the head must consume its own path token
|
||||
// without swallowing the following real A entry.
|
||||
it('diffNameStatus swallows a leading unknown T status and parses the next A', async () => {
|
||||
const git = new VaultGit('/tmp/any');
|
||||
(git as any).runRaw = async () => ({
|
||||
code: 0,
|
||||
stdout: 'T\0t.md\0A\0a.md\0',
|
||||
stderr: '',
|
||||
});
|
||||
|
||||
const entries = await git.diffNameStatus('X', 'Y');
|
||||
|
||||
expect(entries.length).toBe(1);
|
||||
expect(entries[0]).toEqual({ status: 'A', path: 'a.md' });
|
||||
});
|
||||
|
||||
// --- 3. listTrackedFiles: non-zero exit, EMPTY stderr, stdout carries detail -
|
||||
//
|
||||
// The thrown message is built from `(r.stderr || r.stdout || '')`. This pins
|
||||
// the `|| r.stdout` arm (empty stderr, non-empty stdout) — distinct from the
|
||||
// non-empty-stderr and spawn-ENOENT paths the accepted specs cover.
|
||||
it('listTrackedFiles uses stdout in the error message when stderr is empty', async () => {
|
||||
const git = new VaultGit('/tmp/any');
|
||||
(git as any).runRaw = async () => ({
|
||||
code: 1,
|
||||
stderr: '',
|
||||
stdout: 'some detail',
|
||||
});
|
||||
|
||||
await expect(git.listTrackedFiles()).rejects.toThrow(
|
||||
'git ls-files failed: some detail',
|
||||
);
|
||||
});
|
||||
|
||||
// --- 4. diffNameStatus: non-zero exit, EMPTY stderr, stdout carries detail ---
|
||||
//
|
||||
// diffNameStatus has its OWN independent `(r.stderr || r.stdout || '').trim()`
|
||||
// fallback (git.ts ~line 469), separate from listTrackedFiles. Pin the
|
||||
// empty-stderr/non-empty-stdout arm of THIS branch.
|
||||
it('diffNameStatus uses stdout in the error message when stderr is empty', async () => {
|
||||
const git = new VaultGit('/tmp/any');
|
||||
(git as any).runRaw = async () => ({
|
||||
code: 1,
|
||||
stderr: '',
|
||||
stdout: 'diff detail',
|
||||
});
|
||||
|
||||
await expect(git.diffNameStatus('X', 'Y')).rejects.toThrow(
|
||||
'git diff --name-status failed: diff detail',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user