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

98 lines
3.3 KiB
TypeScript

import { execFile } from 'node:child_process';
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { promisify } from 'node:util';
import { afterEach, beforeAll, describe, expect, it } from 'vitest';
import {
VaultGit,
BOT_AUTHOR_NAME,
BOT_AUTHOR_EMAIL,
} from '../src/engine/git';
/**
* QA #119 bug #3 — the smart-HTTP host advertises whatever `HEAD` resolves to as
* a clone's default branch. The engine transiently checks out the read-only
* `docmost` mirror during a cycle, so a clone racing a cycle could default to
* `docmost`. `VaultGit.pinHeadToMain()` pins the symref back to `main` so the
* advertised HEAD is deterministic. Verified against a REAL temp git repo,
* including the actual `git upload-pack --advertise-refs` HEAD symref capability
* a clone reads. Skips gracefully if git is unavailable.
*/
const execFileAsync = promisify(execFile);
async function gitAvailable(): Promise<boolean> {
try {
await execFileAsync('git', ['--version']);
return true;
} catch {
return false;
}
}
describe('VaultGit.pinHeadToMain — advertised HEAD is stably main (real git)', () => {
let available = false;
let dir: string;
beforeAll(async () => {
available = await gitAvailable();
});
afterEach(async () => {
if (dir) await rm(dir, { recursive: true, force: true });
});
async function headSymref(vault: string): Promise<string> {
const { stdout } = await execFileAsync(
'git',
['symbolic-ref', '--short', 'HEAD'],
{ cwd: vault },
);
return stdout.trim();
}
/** The HEAD symref a clone would read from `git upload-pack --advertise-refs`. */
async function advertisedHead(vault: string): Promise<string | null> {
const { stdout } = await execFileAsync(
'git',
['upload-pack', '--advertise-refs', vault],
{ cwd: vault },
);
// protocol v0/v2 advertise `symref=HEAD:refs/heads/<branch>` in the caps.
const m = stdout.match(/symref=HEAD:refs\/heads\/([^\s\0]+)/);
return m ? m[1] : null;
}
it('pins HEAD back to main after the engine checked out docmost', async () => {
if (!available) return;
dir = await mkdtemp(join(tmpdir(), 'docmost-head-'));
const git = new VaultGit(dir);
await git.ensureRepo();
await git.ensureBranch('docmost', 'main');
await writeFile(join(dir, 'A.md'), 'hello\n', 'utf8');
await git.stageAll();
await git.commit('seed', {
authorName: BOT_AUTHOR_NAME,
authorEmail: BOT_AUTHOR_EMAIL,
});
// Keep docmost reachable as a real branch ref.
await execFileAsync('git', ['branch', '-f', 'docmost', 'main'], { cwd: dir });
// Simulate a cycle mid-pull: the engine checks out the read-only mirror.
await git.checkout('docmost');
expect(await headSymref(dir)).toBe('docmost');
expect(await advertisedHead(dir)).toBe('docmost'); // the bug, pre-pin
// Pin: the advertised default branch must be `main` again.
await git.pinHeadToMain();
expect(await headSymref(dir)).toBe('main');
expect(await advertisedHead(dir)).toBe('main');
// Idempotent: pinning when already on main is a clean no-op.
await git.pinHeadToMain();
expect(await headSymref(dir)).toBe('main');
expect(await advertisedHead(dir)).toBe('main');
});
});