Files
gitmost/packages/git-sync/test/config-errors-invalid.test.ts
claude code agent 227 7fe8492e0b 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>
2026-06-24 16:49:59 +03:00

140 lines
5.3 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest';
import { z, ZodError } from 'zod';
import { loadSettingsOrExit } from '../src/engine/config-errors';
import { envSchema } from '../src/engine/settings';
// Companion to test/config-errors.test.ts. That file covers the success path,
// the MISSING-required (undefined -> invalid_type) -> exit branch, and the
// non-ZodError passthrough. This file fills the remaining GAP: the
// INVALID-VALUE branch (config-errors.ts lines ~20, 27-30). A ZodError whose
// issue is a CONSTRAINT violation (bad URL, bad enum, too-short string) is NOT
// a missing key, so it must be routed into the `invalid` bucket and reported
// under the "Invalid value(s)" heading with a `<name>: <message>` line — a
// distinct, operator-facing message from the missing-variable case.
describe('loadSettingsOrExit — invalid-value branch', () => {
afterEach(() => {
vi.restoreAllMocks();
});
// Stub process.exit so it throws (control stops at the exit point without
// killing the runner) and capture everything written to stderr. Mirrors the
// approach in the existing config-errors.test.ts.
function stubExitAndStderr() {
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);
const written = () => writeSpy.mock.calls.map((c) => String(c[0])).join('');
return { exitSpy, writeSpy, written };
}
it('exits(1) and reports an invalid value (bad URL) under "Invalid value(s)"', () => {
const { exitSpy, written } = stubExitAndStderr();
// A present-but-invalid DOCMOST_API_URL: the value exists (so it is NOT a
// missing-key issue), but fails the .url() constraint -> goes to `invalid`.
expect(() =>
loadSettingsOrExit(() =>
envSchema.parse({
DOCMOST_API_URL: 'not-a-url',
DOCMOST_EMAIL: 'ops@example.com',
DOCMOST_PASSWORD: 'hunter2',
DOCMOST_SPACE_ID: 'space-1',
}),
),
).toThrow('exit:1');
expect(exitSpy).toHaveBeenCalledWith(1);
const out = written();
// The invalid-value heading must appear...
expect(out).toContain('Invalid value(s)');
// ...and it must name the offending variable on a `<name>: <message>` line.
expect(out).toContain('DOCMOST_API_URL:');
// The header line is always present.
expect(out).toContain('Configuration error in environment / .env:');
// It must NOT misreport an invalid value as a missing one.
expect(out).not.toContain('Missing required variable(s)');
});
it('exits(1) and reports an invalid enum value (LOG_LEVEL)', () => {
const { exitSpy, written } = stubExitAndStderr();
// All required vars present and valid; only LOG_LEVEL violates the enum.
expect(() =>
loadSettingsOrExit(() =>
envSchema.parse({
DOCMOST_API_URL: 'https://docs.example.com/api',
DOCMOST_EMAIL: 'ops@example.com',
DOCMOST_PASSWORD: 'hunter2',
DOCMOST_SPACE_ID: 'space-1',
LOG_LEVEL: 'verbose', // not in ['debug','info','warn','error']
}),
),
).toThrow('exit:1');
expect(exitSpy).toHaveBeenCalledWith(1);
const out = written();
expect(out).toContain('Invalid value(s)');
expect(out).toContain('LOG_LEVEL:');
expect(out).not.toContain('Missing required variable(s)');
});
it('routes a hand-built constraint-violation ZodError into the invalid bucket', () => {
const { exitSpy, written } = stubExitAndStderr();
// Construct the ZodError directly from a min-length violation so the test
// does not depend on the project schema's exact field set. The issue has a
// non-empty path (so a variable name is printed) and code "too_small"
// (NOT invalid_type/undefined), so config-errors.ts classifies it as
// invalid rather than missing.
const zerr = new ZodError([
{
code: 'too_small',
minimum: 1,
type: 'string',
inclusive: true,
path: ['DOCMOST_PASSWORD'],
message: 'String must contain at least 1 character(s)',
} as z.ZodIssue,
]);
expect(() =>
loadSettingsOrExit(() => {
throw zerr;
}),
).toThrow('exit:1');
expect(exitSpy).toHaveBeenCalledWith(1);
const out = written();
expect(out).toContain('Invalid value(s)');
expect(out).toContain('DOCMOST_PASSWORD: String must contain at least 1');
expect(out).not.toContain('Missing required variable(s)');
});
it('reports missing AND invalid in their own sections when both occur', () => {
const { exitSpy, written } = stubExitAndStderr();
// DOCMOST_API_URL present but invalid (-> invalid section); the three other
// required vars absent (-> missing section). Confirms the two branches are
// populated and emitted independently.
expect(() =>
loadSettingsOrExit(() =>
envSchema.parse({
DOCMOST_API_URL: 'not-a-url',
}),
),
).toThrow('exit:1');
expect(exitSpy).toHaveBeenCalledWith(1);
const out = written();
expect(out).toContain('Missing required variable(s)');
expect(out).toContain('Invalid value(s)');
expect(out).toContain('DOCMOST_API_URL:');
expect(out).toContain('DOCMOST_EMAIL');
});
});