Files
gbrain/test/sync-all-missing-path.test.ts
10079efe40 feat(sync): --missing-path skip — classify absent-local_path sources in --all instead of failing (#3426)
sources.local_path is machine-specific state in a brain-wide table. Any
brain whose sources were registered from more than one machine — or a
sanctioned setup mid-migration (topologies.md Topology 2, or the
system-of-record git flow before every repo is cloned) — has sources
whose checkout is not present on the machine running sync --all. Each
surfaced as a hard failure and forced rc=1 every run; on one observed
fleet that was 12 phantom failures per hour, training operators to
ignore the exit code.

--missing-path skip classifies them honestly: ⊘ in the human aggregate,
status skipped_missing_path + local_path in the --json envelope, new
skipped_count, excluded from error_count and the rc=1 gate. Using the
flag outside --all warns instead of silently no-oping.

Default stays fail: on a single-machine brain a missing local_path
usually means an unmounted volume or deleted checkout, and silently
skipping would hide data loss. Skip is explicit opt-in.

Pure helpers (parseMissingPathMode, partitionMissingPathSources)
exported and unit-tested in the sync-all-parallel style — no DB, no fs.
Docs: sync --help, docs/TESTING.md inventory, KEY_FILES.md sync entry.
CHANGELOG/VERSION deliberately untouched per the release process.

Co-authored-by: Ziggy <lazyclaw137@gmail.com>
Co-authored-by: Garry Tan <garrytan@gmail.com>
Co-authored-by: Lazydayz137 <Lazydayz137@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 14:16:36 -07:00

112 lines
4.9 KiB
TypeScript

/**
* Tests for `sync --all --missing-path <fail|skip>`.
*
* Why this exists:
* `sources.local_path` is machine-specific state in a brain-wide table.
* Any brain whose sources were registered from more than one machine —
* or a sanctioned setup mid-migration (docs/architecture/topologies.md
* Topology 2, or the system-of-record git flow before every repo is
* cloned) — has sources whose checkout is simply not present on the
* machine running `sync --all`. Today each of those is reported as a
* hard per-source failure ("Not a git repository: <path>") and the run
* exits rc=1, every run. On one observed fleet that was 12 phantom
* failures per hour with zero actionable signal, which trains operators
* to ignore the exit code — the worst possible property for a cron.
*
* `--missing-path skip` classifies those sources honestly instead:
* status `skipped_missing_path` in the --json envelope, a ⊘ line in the
* human aggregate, excluded from error_count and from the rc=1 gate.
*
* The DEFAULT stays `fail`: on a single-machine brain a missing
* local_path usually means an unmounted volume or a deleted checkout,
* and silently skipping it would hide real data loss. Skip is opt-in.
*
* These tests pin the two pure helpers (same style as
* sync-all-parallel.test.ts — no DB, no fs):
* 1. parseMissingPathMode(): flag parsing, default, loud rejection of
* bad values (paste-ready hint names the flag and both values).
* 2. partitionMissingPathSources(): classification is driven ONLY by
* the injected pathExists predicate; null local_path passes through
* as runnable (pure-DB sources are already excluded from --all by
* the local_path IS NOT NULL SELECT — defensive, not load-bearing).
*/
import { describe, expect, test } from 'bun:test';
import {
parseMissingPathMode,
partitionMissingPathSources,
} from '../src/commands/sync.ts';
// ── parseMissingPathMode ────────────────────────────────────────────
describe('parseMissingPathMode', () => {
test('defaults to fail when the flag is absent', () => {
expect(parseMissingPathMode([])).toBe('fail');
expect(parseMissingPathMode(['--all', '--no-embed'])).toBe('fail');
});
test('parses skip and fail explicitly', () => {
expect(parseMissingPathMode(['--all', '--missing-path', 'skip'])).toBe('skip');
expect(parseMissingPathMode(['--all', '--missing-path', 'fail'])).toBe('fail');
});
test('rejects an unknown value with a paste-ready hint', () => {
expect(() => parseMissingPathMode(['--missing-path', 'ignore']))
.toThrow(/--missing-path.*(fail|skip)/);
});
test('rejects a dangling flag (no value)', () => {
expect(() => parseMissingPathMode(['--all', '--missing-path']))
.toThrow(/--missing-path/);
});
test('does not swallow a following flag as its value', () => {
// `--missing-path --json` is a mistake, not "mode --json".
expect(() => parseMissingPathMode(['--missing-path', '--json']))
.toThrow(/--missing-path/);
});
});
// ── partitionMissingPathSources ─────────────────────────────────────
type Src = { id: string; local_path: string | null };
const src = (id: string, local_path: string | null): Src => ({ id, local_path });
describe('partitionMissingPathSources', () => {
test('splits sources by the injected pathExists predicate', () => {
const sources = [src('here', '/present'), src('elsewhere', '/absent')];
const { runnable, missing } = partitionMissingPathSources(
sources, (p) => p === '/present');
expect(runnable.map((s) => s.id)).toEqual(['here']);
expect(missing.map((s) => s.id)).toEqual(['elsewhere']);
});
test('null local_path stays runnable (pure-DB sources are not "missing")', () => {
const { runnable, missing } = partitionMissingPathSources(
[src('db-only', null)], () => false);
expect(runnable.map((s) => s.id)).toEqual(['db-only']);
expect(missing).toEqual([]);
});
test('all-missing and empty inputs are well-formed', () => {
const allMissing = partitionMissingPathSources(
[src('a', '/x'), src('b', '/y')], () => false);
expect(allMissing.runnable).toEqual([]);
expect(allMissing.missing.map((s) => s.id)).toEqual(['a', 'b']);
const empty = partitionMissingPathSources([], () => true);
expect(empty.runnable).toEqual([]);
expect(empty.missing).toEqual([]);
});
test('never consults the predicate for null paths and preserves order', () => {
const asked: string[] = [];
const sources = [src('a', '/1'), src('b', null), src('c', '/2')];
const { runnable } = partitionMissingPathSources(sources, (p) => {
asked.push(p);
return true;
});
expect(asked).toEqual(['/1', '/2']);
expect(runnable.map((s) => s.id)).toEqual(['a', 'b', 'c']);
});
});