feat(git-sync): vendor IO engine (pull/push/git/settings) with GitSyncClient seam (Phase A.3)

Vendor the IO engine from docmost-sync into packages/git-sync/src/engine:
- git.ts (VaultGit, execFile shell-out — verbatim)
- pull.ts (readExisting, computePullActions, applyPullActions)
- push.ts (classifyRenameMoves, computePushActions, applyPushActions, runPush)
- settings.ts adapted (pure parseSettings + Settings type; no process.env binding
  — the server builds Settings from EnvironmentService later), config-errors.ts.
CLI main()/import.meta entrypoints dropped (server drives in-process).

Client seam: new engine/client.types.ts defines GitSyncClient; pull.ts/push.ts
now use Pick<GitSyncClient, ...> instead of the non-vendored DocmostClient. Engine
logic byte-identical except a zod4-compat fix in config-errors (zod4 dropped the
issue.received==='undefined' signal; match /received undefined/ on the message).

Ported the engine unit tests (compute/apply pull+push actions, classify-rename-
moves, run-push, settings, config-errors) incl. real-git temp-repo tests: 431
pass / 3 expected-fail (was 314/3). REST/CLI-coupled upstream tests skipped
(noted). CJS build clean. No apps/server wiring yet (next step).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
claude code agent 227
2026-06-21 14:39:38 +03:00
parent 5aaeaaae3c
commit d79807802c
32 changed files with 6222 additions and 2728 deletions

View File

@@ -0,0 +1,56 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { z } from 'zod';
import { loadSettingsOrExit } from '../src/engine/config-errors';
describe('loadSettingsOrExit', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it('returns the factory value and does not exit on success', () => {
const exitSpy = vi
.spyOn(process, 'exit')
.mockImplementation((() => undefined) as never);
const result = loadSettingsOrExit(() => ({ ok: true }));
expect(result).toEqual({ ok: true });
expect(exitSpy).not.toHaveBeenCalled();
});
it('prints a named-variable message and exits(1) on a ZodError', () => {
// Mock process.exit to throw so control stops at the exit point, mirroring
// the real exit-the-process behaviour without killing the test runner.
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(((
code?: number,
) => {
throw new Error(`exit:${code}`);
}) as never);
const writeSpy = vi
.spyOn(process.stderr, 'write')
.mockImplementation(() => true);
expect(() =>
loadSettingsOrExit(() => z.object({ FOO: z.string() }).parse({})),
).toThrow('exit:1');
expect(exitSpy).toHaveBeenCalledWith(1);
const written = writeSpy.mock.calls.map((c) => String(c[0])).join('');
expect(written).toContain('Missing required variable(s)');
expect(written).toContain('FOO');
});
it('propagates a non-ZodError without exiting', () => {
const exitSpy = vi
.spyOn(process, 'exit')
.mockImplementation((() => undefined) as never);
const boom = new Error('x');
expect(() =>
loadSettingsOrExit(() => {
throw boom;
}),
).toThrow(boom);
expect(exitSpy).not.toHaveBeenCalled();
});
});