Files
gitmost/packages/git-sync/test/read-existing.test.ts
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

122 lines
4.3 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { readExisting } from '../src/engine/pull';
import { serializePageFile } from '../src/lib/page-file';
// R-Pull-1 (test-strategy report §5): `readExisting` now takes injectable IO
// (`listTracked` / `readFile`), so its parsing + skip rules are unit-testable
// without a real git repo or filesystem. These tests pass fakes only — no git,
// no fs, no network. Identity is recovered from the native `gitmost_id`
// frontmatter (no more `docmost:meta`).
/** Build a valid native page file with a `gitmost_id` frontmatter. */
function withId(id: string, body = '# Title\nbody\n'): string {
return serializePageFile(id, body);
}
/** A fake `readFile` backed by an in-memory map (rejects on a missing key). */
function fakeReadFile(files: Record<string, string>) {
return async (rel: string): Promise<string> => {
if (!(rel in files)) {
throw Object.assign(new Error(`ENOENT: ${rel}`), { code: 'ENOENT' });
}
return files[rel];
};
}
describe('readExisting (R-Pull-1, injected IO)', () => {
it('recovers { pageId, relPath } for valid tracked files', async () => {
const files = {
'Space/A.md': withId('p1'),
'Space/Sub/B.md': withId('p2'),
};
const result = await readExisting({
listTracked: async () => Object.keys(files),
readFile: fakeReadFile(files),
});
expect(result).toEqual([
{ pageId: 'p1', relPath: 'Space/A.md' },
{ pageId: 'p2', relPath: 'Space/Sub/B.md' },
]);
});
it('SKIPS a file with no frontmatter (plain hand-written markdown)', async () => {
const files = {
'tracked.md': withId('p1'),
'stray.md': '# Just a hand-written note\n\nNo frontmatter here.\n',
};
const result = await readExisting({
listTracked: async () => Object.keys(files),
readFile: fakeReadFile(files),
});
// Only the engine-tracked file (with a gitmost_id) survives.
expect(result).toEqual([{ pageId: 'p1', relPath: 'tracked.md' }]);
});
it('SKIPS a file whose frontmatter has no gitmost_id key', async () => {
const files = {
'has-id.md': withId('keep'),
// A user's own frontmatter, but no gitmost_id -> not engine-tracked.
'no-id.md': '---\ntags: [note]\ntitle: untitled\n---\n\nbody\n',
};
const result = await readExisting({
listTracked: async () => Object.keys(files),
readFile: fakeReadFile(files),
});
expect(result).toEqual([{ pageId: 'keep', relPath: 'has-id.md' }]);
});
it('SKIPS a file with an EMPTY gitmost_id value, does not throw', async () => {
const files = {
'good.md': withId('good'),
'blank.md': '---\ngitmost_id:\n---\n\nbody\n',
};
const result = await readExisting({
listTracked: async () => Object.keys(files),
readFile: fakeReadFile(files),
});
expect(result).toEqual([{ pageId: 'good', relPath: 'good.md' }]);
});
it('does NOT throw when readFile REJECTS (tracked but missing) — treats it as skipped', async () => {
const files = {
'present.md': withId('present'),
// "ghost.md" is listed as tracked but absent from the file map -> reject.
};
const result = await readExisting({
listTracked: async () => ['present.md', 'ghost.md'],
readFile: fakeReadFile(files),
});
// The rejection is swallowed; the present file still comes through.
expect(result).toEqual([{ pageId: 'present', relPath: 'present.md' }]);
});
it('returns an empty list when nothing is tracked', async () => {
const result = await readExisting({
listTracked: async () => [],
readFile: async () => {
throw new Error('should not be called');
},
});
expect(result).toEqual([]);
});
it('combines all skip rules in one listing (only the valid files survive)', async () => {
const files = {
'ok1.md': withId('a'),
'no-meta.md': 'plain\n',
'no-id.md': '---\ntags: [x]\n---\n\nbody\n',
'blank.md': '---\ngitmost_id:\n---\n\nbody\n',
'ok2.md': withId('b'),
// missing.md rejects on read.
};
const result = await readExisting({
listTracked: async () => [...Object.keys(files), 'missing.md'],
readFile: fakeReadFile(files),
});
expect(result).toEqual([
{ pageId: 'a', relPath: 'ok1.md' },
{ pageId: 'b', relPath: 'ok2.md' },
]);
});
});