test(git-sync): add reviewer-requested coverage across engine, server, client

Implements the test cases called out in the PR #119 review threads
(code-review, test-strategy report, red-team) — TESTS ONLY, no production
code changes.

packages/git-sync (vitest):
- lib converter/markdown gaps: pageBreak data-loss (it.fails repro),
  subpages lossy round-trip, nested/fenced callouts, ol->taskList bridge,
  column.width number<->string drift, empty details.
- engine units: parentFolderFile, planReconciliation swap/chained move,
  buildVaultLayout last-resort-by-id, firstDivergence, applyPushActions /
  applyPullActions failure isolation.
- real temp-git integration: diffNameStatus -z rename+add/modify
  alignment, copy-line behavior, per-invocation committer identity (no
  leak into repo/global config).
- ENFORCED type-level GitSyncClient contract via vitest typecheck over a
  *.test-d.ts file (tsconfig.vitest.json; build tsconfig untouched).

apps/server (jest):
- orchestrator: delete-cap neutralization + fail-safe, Redis lock / mutex
  skip ladder + release-on-throw, merge guard, pull/push order, remote
  template substitution, poll lifecycle.
- page-change listener: loop-guard, debounce coalescing, id resolution,
  error swallowing.
- vault registry, controller authz (trigger + status), env
  validation/getters, page.service git-sync provenance stamping,
  persistence precedence (agent > git-sync > user) + no boundary snapshot,
  space.service audit-delta, space.repo jsonb-merge, converter-gate corpus
  extension (mention/math/details/marks).

apps/client (vitest + testing-library):
- history-item git-sync badge: render gating + non-clickable.
- edit-space-form toggle: initial state, optimistic payload, rollback on
  error, disabled states.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
claude_code
2026-06-21 17:51:35 +03:00
committed by claude code agent 227
parent 8d0f403745
commit 906c8c6d89
20 changed files with 3331 additions and 0 deletions
@@ -77,4 +77,70 @@ describe('EnvironmentService', () => {
expect(withEnv('not-a-number').getGitSyncDebounceMs()).toBe(2000);
});
});
// getGitSyncDataDir reads two distinct keys (GIT_SYNC_DATA_DIR and DATA_DIR),
// so this builder maps each key to a supplied value (and honours the fallback
// the getter passes for DATA_DIR's `|| './data'`).
describe('getGitSyncDataDir', () => {
const withEnv = (values: Record<string, string | undefined>) =>
new EnvironmentService({
get: (key: string, fallback?: string) => values[key] ?? fallback,
} as any);
it("defaults to './data/git-sync' when neither key is set", () => {
expect(withEnv({}).getGitSyncDataDir()).toBe('./data/git-sync');
});
it('derives from DATA_DIR with the /git-sync suffix', () => {
expect(
withEnv({ DATA_DIR: '/var/lib/docmost' }).getGitSyncDataDir(),
).toBe('/var/lib/docmost/git-sync');
});
it('strips trailing slashes from DATA_DIR before appending', () => {
expect(
withEnv({ DATA_DIR: '/var/lib/docmost///' }).getGitSyncDataDir(),
).toBe('/var/lib/docmost/git-sync');
});
it('lets an explicit GIT_SYNC_DATA_DIR override the DATA_DIR derivation', () => {
expect(
withEnv({
GIT_SYNC_DATA_DIR: '/custom/vault',
DATA_DIR: '/var/lib/docmost',
}).getGitSyncDataDir(),
).toBe('/custom/vault');
});
it('returns the explicit override verbatim (no /git-sync suffix, no slash strip)', () => {
expect(
withEnv({ GIT_SYNC_DATA_DIR: '/custom/vault/' }).getGitSyncDataDir(),
).toBe('/custom/vault/');
});
});
// isGitSyncEnabled is the `.toLowerCase() === 'true'` contract: only a
// case-insensitive "true" enables it; everything else (unset, "false",
// garbage) is false.
describe('isGitSyncEnabled', () => {
const withEnv = (value?: string) =>
new EnvironmentService({
get: (_key: string, fallback?: string) => value ?? fallback,
} as any);
it('is true for "true" and "TRUE" (case-insensitive)', () => {
expect(withEnv('true').isGitSyncEnabled()).toBe(true);
expect(withEnv('TRUE').isGitSyncEnabled()).toBe(true);
});
it('is false when unset (defaults to "false")', () => {
expect(withEnv().isGitSyncEnabled()).toBe(false);
});
it('is false for "false" and garbage values', () => {
expect(withEnv('false').isGitSyncEnabled()).toBe(false);
expect(withEnv('maybe').isGitSyncEnabled()).toBe(false);
expect(withEnv('1').isGitSyncEnabled()).toBe(false);
});
});
});
@@ -0,0 +1,74 @@
import { plainToInstance } from 'class-transformer';
import { validateSync } from 'class-validator';
import { EnvironmentVariables } from './environment.validation';
/**
* Validation-layer coverage for the git-sync env contract (test-strategy Module
* 4 / item #4). We drive the decorated class with `validateSync` directly — the
* exported `validate()` helper calls `process.exit(1)` on failure and so cannot
* be asserted in-process. We only assert the git-sync rules, providing the
* minimal always-required fields so unrelated validators do not add noise.
*/
describe('EnvironmentVariables — git-sync validation', () => {
// A baseline config that satisfies the unconditionally-required fields
// (DATABASE_URL, REDIS_URL, APP_SECRET) so the only errors we ever see come
// from the git-sync rules under test.
const baseConfig = {
DATABASE_URL: 'postgres://user:pass@localhost:5432/docmost',
REDIS_URL: 'redis://localhost:6379',
APP_SECRET: 'x'.repeat(32),
};
const validate = (extra: Record<string, unknown>) => {
const instance = plainToInstance(EnvironmentVariables, {
...baseConfig,
...extra,
});
return validateSync(instance);
};
const errorFor = (errors: ReturnType<typeof validateSync>, property: string) =>
errors.find((e) => e.property === property);
it('flags GIT_SYNC_SERVICE_USER_ID when GIT_SYNC_ENABLED="true" and the id is absent', () => {
const errors = validate({ GIT_SYNC_ENABLED: 'true' });
const err = errorFor(errors, 'GIT_SYNC_SERVICE_USER_ID');
expect(err).toBeDefined();
// @IsNotEmpty is the failing constraint (sync is on but no attributable
// author was configured).
expect(err?.constraints).toHaveProperty('isNotEmpty');
});
it('accepts GIT_SYNC_ENABLED="true" once GIT_SYNC_SERVICE_USER_ID is present', () => {
const errors = validate({
GIT_SYNC_ENABLED: 'true',
GIT_SYNC_SERVICE_USER_ID: 'service-user-1',
});
expect(errorFor(errors, 'GIT_SYNC_SERVICE_USER_ID')).toBeUndefined();
});
it('does not require the service user id when git-sync is disabled (unset)', () => {
const errors = validate({});
// The @ValidateIf gate (GIT_SYNC_ENABLED === "true") is not met, so the
// required-if-enabled rule is skipped entirely.
expect(errorFor(errors, 'GIT_SYNC_SERVICE_USER_ID')).toBeUndefined();
});
it('does not require the service user id when git-sync is explicitly "false"', () => {
const errors = validate({ GIT_SYNC_ENABLED: 'false' });
expect(errorFor(errors, 'GIT_SYNC_SERVICE_USER_ID')).toBeUndefined();
expect(errorFor(errors, 'GIT_SYNC_ENABLED')).toBeUndefined();
});
it('rejects a GIT_SYNC_ENABLED value outside the {true,false} set via @IsIn', () => {
const errors = validate({ GIT_SYNC_ENABLED: 'maybe' });
const err = errorFor(errors, 'GIT_SYNC_ENABLED');
expect(err).toBeDefined();
expect(err?.constraints).toHaveProperty('isIn');
});
});