Files
gbrain/test/sync-failures.test.ts
T
08746b06d2 v0.22.9 feat: structured error code summary for sync --skip-failed (#501)
* feat: structured error code summary for sync --skip-failed (#500)

When sync encounters per-file failures, the blocked/skip-failed messages
now include a breakdown by error code (SLUG_MISMATCH, YAML_PARSE, etc.)
instead of just a raw count. This makes it immediately obvious *why*
files failed without requiring manual investigation.

Changes:
- Add classifyErrorCode() — maps error messages to ParseValidationCode
- Add summarizeFailuresByCode() — groups failures into sorted code summary
- SyncFailure now carries a 'code' field (backfilled on acknowledge)
- acknowledgeSyncFailures() returns AcknowledgeResult {count, summary}
- sync blocked + skip-failed messages show code breakdown
- doctor sync_failures check shows code breakdown for both unacked and historical
- 12 new tests for classifyErrorCode, summarizeFailuresByCode, and structured returns

Before:
  Sync blocked: 2688 file(s) failed to parse.

After:
  Sync blocked: 2688 file(s) failed to parse:
    SLUG_MISMATCH: 2685
    YAML_DUPLICATE_KEY: 3

Closes #500

* fix: eng-review fixes for sync error-code classification

- Reorder classifyErrorCode() so DB-layer errors (DB_DUPLICATE_KEY,
  STATEMENT_TIMEOUT) check BEFORE YAML patterns. Postgres "duplicate key
  value violates unique constraint" no longer mislabels as YAML_DUPLICATE_KEY.
- Rewrite MISSING_OPEN/MISSING_CLOSE/EMPTY_FRONTMATTER/NULL_BYTES/NESTED_QUOTES
  regexes to match the canonical messages emitted by collectValidationErrors()
  in src/core/markdown.ts. Previous patterns (e.g. /missing.*open/i) never
  fired because the upstream throw site emits prose ("File is empty...",
  "No closing --- delimiter found"), not the code name.
- Extract formatCodeBreakdown() helper that accepts either raw failures or
  pre-summarized {code, count}[] input. Replaces 3 duplicate inline builders
  in src/commands/sync.ts.
- 15 new tests (37/37 pass on test/sync-failures.test.ts):
  - DB vs YAML duplicate-key disambiguation (3 cases)
  - Canonical-message coverage for the 5 frontmatter codes (7 cases)
  - acknowledgeSyncFailures() legacy-entry backfill branch (2 cases)
  - formatCodeBreakdown() dual-input shape (3 cases)
- TODOS.md: file 3 follow-ups (P2 plumb structured ParseValidationCode;
  P0-at-ship CHANGELOG migration note for AcknowledgeResult; P3 concurrent-
  safe ack of sync-failures.jsonl).

Eng-review plan: ~/.claude/plans/then-codex-synchronous-toucan.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: bump version and changelog (v0.22.9)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci: 16-core runner + 4-way matrix shard for test job

The unit test suite ran 22m17s on ubuntu-latest (2-core/7GB) because:
- 187 test files run with bun test parallelism bounded by core count
- 23 of those files spin up a fresh PGLiteEngine + initSchema in beforeEach,
  paying ~22s WASM cold-start per test on the small runner

This commit fixes the runner side:
- runs-on: ubuntu-latest-16-cores (16 vCPU / 64 GB RAM)
- strategy.matrix.shard splits 4 parallel jobs, each running ~40 of 158 unit
  test files. Single-file wall-time floor is ~3 min after the test refactor,
  so 4 shards × 16 cores hits the floor quickly without wasting cores past it.
- pre-test gates (typecheck, check-jsonb, check-progress, check-wasm) only run
  on shard 1 — they're not test files and don't benefit from sharding.

scripts/test-shard.sh partitions test files by stable FNV-1a hash mod N. Same
file always lands in the same shard, so retries are reproducible. Pure shell,
portable to bash 3.2 (macOS) and bash 5.x (CI). Excludes test/e2e/ which runs
via bun run test:e2e separately and needs DATABASE_URL.

Also: ignore .claude/ harness state files (scheduled_tasks.lock etc) instead
of just .claude/skills/.

Cost: ~$0.19/run vs $0 (public repo, default runner is free). At 50 PRs/month
that's ~$10/month for ~5x faster CI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: refactor top-3 PGLite-heavy files to share one engine per file

Three test files were spinning up a fresh PGLiteEngine + connect + initSchema
in beforeEach. PGLite WASM cold-start is ~22s on the small CI runner; doing
this per test multiplied wall-time across the suite. The 3 files alone
accounted for ~6.5 min of the 22m CI run (177s + 132s + 87s).

Refactor: move PGLite setup to beforeAll (one engine per file), wipe data
in beforeEach via the new test/helpers/reset-pglite.ts helper.

The reset helper:
- TRUNCATEs every public table CASCADE, including sources (so tests that
  register their own sources don't leak rows into the next test).
- Re-seeds the default source row that pages.source_id's DEFAULT FKs against.
  Without this, the next page insert would fail FK validation.
- Preserves schema_version so migration helpers don't think the brain is on v0.

Files refactored:
- test/extract-incremental.test.ts (8 tests, was 177s on CI)
- test/brain-writer.test.ts (16 tests; only the scanBrainSources block uses
  PGLite, was 132s on CI)
- test/sync.test.ts (37 tests; only the performSync dry-run block uses PGLite,
  was 87s on CI)

All 61 tests still pass locally. The remaining 20 PGLite-heavy files use the
same beforeEach anti-pattern; this commit only refactors the proven worst
offenders. Sweep the rest in a follow-up if CI numbers indicate it's worth it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci: fall back to ubuntu-latest for matrix shard

The ubuntu-latest-16-cores label requires a provisioned larger-runner pool in
repo/org settings. Without that setup, jobs queue indefinitely waiting for a
runner that doesn't exist (verified: 4 shards stuck in 'queued' status with
empty runner_name for 5+ min).

Drop back to the default 2-core ubuntu-latest. The 4-way matrix shard still
delivers ~5-6x speedup via parallelism alone — 4 jobs running in parallel,
each handling ~40 of 158 unit test files. Cost stays $0 (default runner is
free for public repos).

If we ever provision a larger-runner pool, flip this label back to
ubuntu-latest-16-cores. The matrix + sharder will use the bigger boxes
unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Wintermute <wintermute@garrytan.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 10:12:10 -07:00

433 lines
18 KiB
TypeScript

/**
* Bug 9 regression — sync silently drops files with broken YAML.
*
* Before the fix, sync.ts caught per-file parse errors, printed a warning,
* and still advanced sync.last_commit. The failed file was never retried
* because it was behind the bookmark. Silent data loss.
*
* After the fix:
* - failures append to ~/.gbrain/sync-failures.jsonl (with dedup)
* - incremental + full-sync + import git-continuity paths gate the
* sync.last_commit advance on "no failures"
* - `gbrain sync --skip-failed` acknowledges the current set
* - `gbrain doctor` surfaces unacknowledged failures
*
* This suite exercises the helper + the dedup behavior. The full CLI
* round-trip is covered by E2E tests.
*/
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
import { mkdtempSync, rmSync, readFileSync, existsSync, writeFileSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
// Point HOME at a tmpdir so we don't stomp the real ~/.gbrain/sync-failures.jsonl
let tmpHome: string;
const originalHome = process.env.HOME;
beforeEach(async () => {
tmpHome = mkdtempSync(join(tmpdir(), 'gbrain-sync-failures-'));
process.env.HOME = tmpHome;
// Belt-and-suspenders: explicitly clear the jsonl at the resolved path.
const { syncFailuresPath } = await import('../src/core/sync.ts');
try { rmSync(syncFailuresPath(), { force: true }); } catch { /* none */ }
});
afterEach(() => {
if (originalHome) process.env.HOME = originalHome;
else delete process.env.HOME;
try { rmSync(tmpHome, { recursive: true, force: true }); } catch { /* ignore */ }
});
describe('Bug 9 — sync-failures JSONL helpers', () => {
test('recordSyncFailures appends one line per failure with dedup', async () => {
const { recordSyncFailures, loadSyncFailures, syncFailuresPath } = await import('../src/core/sync.ts');
recordSyncFailures([
{ path: 'people/alice.md', error: 'YAML: unexpected colon in title' },
{ path: 'notes/broken.md', error: 'YAML: duplicated key' },
], 'abc123def456');
expect(existsSync(syncFailuresPath())).toBe(true);
const entries = loadSyncFailures();
expect(entries.length).toBe(2);
expect(entries[0].path).toBe('people/alice.md');
expect(entries[0].commit).toBe('abc123def456');
expect(entries[0].acknowledged).toBeUndefined();
// Same failure on same commit should NOT re-append.
recordSyncFailures([
{ path: 'people/alice.md', error: 'YAML: unexpected colon in title' },
], 'abc123def456');
expect(loadSyncFailures().length).toBe(2);
// Different commit → new entry.
recordSyncFailures([
{ path: 'people/alice.md', error: 'YAML: unexpected colon in title' },
], 'zzz999');
expect(loadSyncFailures().length).toBe(3);
});
test('acknowledgeSyncFailures marks unacked entries, leaves acked alone', async () => {
const { recordSyncFailures, acknowledgeSyncFailures, loadSyncFailures } = await import('../src/core/sync.ts');
recordSyncFailures([
{ path: 'a.md', error: 'err1' },
{ path: 'b.md', error: 'err2' },
], 'commit1');
const result = acknowledgeSyncFailures();
expect(result.count).toBe(2);
expect(result.summary.length).toBeGreaterThan(0);
const after = loadSyncFailures();
expect(after.every(e => e.acknowledged === true)).toBe(true);
expect(after.every(e => typeof e.acknowledged_at === 'string')).toBe(true);
// Second ack: nothing new to mark.
expect(acknowledgeSyncFailures().count).toBe(0);
// Adding a fresh failure then ack: only the new one flips.
recordSyncFailures([{ path: 'c.md', error: 'err3' }], 'commit2');
expect(acknowledgeSyncFailures().count).toBe(1);
expect(loadSyncFailures().length).toBe(3);
expect(loadSyncFailures().every(e => e.acknowledged === true)).toBe(true);
});
test('unacknowledgedSyncFailures filters correctly', async () => {
const { recordSyncFailures, acknowledgeSyncFailures, unacknowledgedSyncFailures } = await import('../src/core/sync.ts');
recordSyncFailures([{ path: 'a.md', error: 'err1' }], 'c1');
acknowledgeSyncFailures();
recordSyncFailures([{ path: 'b.md', error: 'err2' }], 'c2');
const unacked = unacknowledgedSyncFailures();
expect(unacked.length).toBe(1);
expect(unacked[0].path).toBe('b.md');
});
test('loadSyncFailures returns [] when file is missing', async () => {
const { loadSyncFailures } = await import('../src/core/sync.ts');
expect(loadSyncFailures()).toEqual([]);
});
test('loadSyncFailures tolerates malformed lines', async () => {
const { loadSyncFailures, syncFailuresPath, recordSyncFailures } = await import('../src/core/sync.ts');
// Seed one valid entry.
recordSyncFailures([{ path: 'a.md', error: 'err1' }], 'c1');
// Append garbage.
writeFileSync(syncFailuresPath(), readFileSync(syncFailuresPath(), 'utf-8') + 'NOT-JSON\n', { flag: 'w' });
const out = loadSyncFailures();
expect(out.length).toBe(1);
expect(out[0].path).toBe('a.md');
});
});
describe('Bug 9 — doctor surfaces sync failures', () => {
test('doctor source contains sync_failures check', async () => {
const source = await Bun.file(new URL('../src/commands/doctor.ts', import.meta.url)).text();
expect(source).toContain('sync_failures');
expect(source).toContain('unacknowledgedSyncFailures');
expect(source).toContain("'gbrain sync --skip-failed'");
});
});
describe('Bug 9 — sync.ts CLI flag wiring', () => {
test('runSync parses --skip-failed and --retry-failed flags', async () => {
const source = await Bun.file(new URL('../src/commands/sync.ts', import.meta.url)).text();
expect(source).toContain("args.includes('--skip-failed')");
expect(source).toContain("args.includes('--retry-failed')");
expect(source).toContain('skipFailed');
expect(source).toContain('retryFailed');
});
test('performSync gates sync.last_commit on failedFiles.length', async () => {
const source = await Bun.file(new URL('../src/commands/sync.ts', import.meta.url)).text();
// The gate exists and references the failure set.
expect(source).toContain('failedFiles.length > 0');
expect(source).toContain('blocked_by_failures');
});
test('performFullSync gates on result.failures from runImport', async () => {
const source = await Bun.file(new URL('../src/commands/sync.ts', import.meta.url)).text();
expect(source).toContain('result.failures.length > 0');
});
test('runImport returns RunImportResult with failures list', async () => {
const source = await Bun.file(new URL('../src/commands/import.ts', import.meta.url)).text();
expect(source).toContain('RunImportResult');
expect(source).toContain('failures: Array<{ path: string; error: string }>');
expect(source).toContain('recordSyncFailures');
});
});
describe('classifyErrorCode — error message to code mapping', () => {
test('classifies SLUG_MISMATCH from error message', async () => {
const { classifyErrorCode } = await import('../src/core/sync.ts');
expect(classifyErrorCode(
'Frontmatter slug "my-friend-mike" does not match path-derived slug "2008-03-20-my-friend-mike"'
)).toBe('SLUG_MISMATCH');
});
test('classifies YAML_PARSE from error message', async () => {
const { classifyErrorCode } = await import('../src/core/sync.ts');
expect(classifyErrorCode('YAML parse failed: unexpected colon in title')).toBe('YAML_PARSE');
});
test('classifies YAML_DUPLICATE_KEY', async () => {
const { classifyErrorCode } = await import('../src/core/sync.ts');
expect(classifyErrorCode('YAMLException: duplicated mapping key')).toBe('YAML_DUPLICATE_KEY');
});
test('classifies STATEMENT_TIMEOUT', async () => {
const { classifyErrorCode } = await import('../src/core/sync.ts');
expect(classifyErrorCode('canceling statement due to statement timeout')).toBe('STATEMENT_TIMEOUT');
});
test('classifies NULL_BYTES', async () => {
const { classifyErrorCode } = await import('../src/core/sync.ts');
expect(classifyErrorCode('invalid UTF-8: null byte at position 3770')).toBe('NULL_BYTES');
});
test('classifies INVALID_UTF8', async () => {
const { classifyErrorCode } = await import('../src/core/sync.ts');
expect(classifyErrorCode('invalid UTF-8 sequence at position 500')).toBe('INVALID_UTF8');
});
test('returns UNKNOWN for unrecognized errors', async () => {
const { classifyErrorCode } = await import('../src/core/sync.ts');
expect(classifyErrorCode('something completely different')).toBe('UNKNOWN');
});
});
describe('summarizeFailuresByCode — grouped summary', () => {
test('groups failures by classified code', async () => {
const { summarizeFailuresByCode } = await import('../src/core/sync.ts');
const summary = summarizeFailuresByCode([
{ error: 'Frontmatter slug "a" does not match path-derived slug "b"' },
{ error: 'Frontmatter slug "c" does not match path-derived slug "d"' },
{ error: 'YAML parse failed: bad colon' },
{ error: 'something unknown' },
]);
expect(summary).toEqual([
{ code: 'SLUG_MISMATCH', count: 2 },
{ code: 'YAML_PARSE', count: 1 },
{ code: 'UNKNOWN', count: 1 },
]);
});
test('respects pre-classified code field', async () => {
const { summarizeFailuresByCode } = await import('../src/core/sync.ts');
const summary = summarizeFailuresByCode([
{ error: 'anything', code: 'SLUG_MISMATCH' },
{ error: 'anything', code: 'SLUG_MISMATCH' },
{ error: 'anything', code: 'YAML_PARSE' },
]);
expect(summary).toEqual([
{ code: 'SLUG_MISMATCH', count: 2 },
{ code: 'YAML_PARSE', count: 1 },
]);
});
test('returns empty array for no failures', async () => {
const { summarizeFailuresByCode } = await import('../src/core/sync.ts');
expect(summarizeFailuresByCode([])).toEqual([]);
});
});
describe('acknowledgeSyncFailures — structured return', () => {
test('returns count and code summary', async () => {
const { recordSyncFailures, acknowledgeSyncFailures } = await import('../src/core/sync.ts');
recordSyncFailures([
{ path: 'a.md', error: 'Frontmatter slug "x" does not match path-derived slug "y"' },
{ path: 'b.md', error: 'Frontmatter slug "p" does not match path-derived slug "q"' },
{ path: 'c.md', error: 'YAML parse failed: bad' },
], 'commit1');
const result = acknowledgeSyncFailures();
expect(result.count).toBe(3);
expect(result.summary).toEqual([
{ code: 'SLUG_MISMATCH', count: 2 },
{ code: 'YAML_PARSE', count: 1 },
]);
});
});
describe('recordSyncFailures — code field', () => {
test('records classified code alongside error message', async () => {
const { recordSyncFailures, loadSyncFailures } = await import('../src/core/sync.ts');
recordSyncFailures([
{ path: 'a.md', error: 'Frontmatter slug "x" does not match path-derived slug "y"' },
], 'commit1');
const entries = loadSyncFailures();
expect(entries[0].code).toBe('SLUG_MISMATCH');
});
});
// classifyErrorCode disambiguates Postgres unique-constraint errors from
// YAML duplicate-key errors. Pre-fix, every "duplicate.*key" string mapped
// to YAML_DUPLICATE_KEY, which mislabels DB-layer failures during sync.
describe('classifyErrorCode — DB vs YAML duplicate-key disambiguation', () => {
test('Postgres unique-constraint violation classifies as DB_DUPLICATE_KEY', async () => {
const { classifyErrorCode } = await import('../src/core/sync.ts');
expect(classifyErrorCode(
'duplicate key value violates unique constraint "pages_slug_key"'
)).toBe('DB_DUPLICATE_KEY');
});
test('YAML duplicated mapping key still classifies as YAML_DUPLICATE_KEY', async () => {
const { classifyErrorCode } = await import('../src/core/sync.ts');
expect(classifyErrorCode('YAMLException: duplicated mapping key "title"'))
.toBe('YAML_DUPLICATE_KEY');
});
test('DB pattern is checked BEFORE YAML so DB errors are not mislabeled', async () => {
// Both patterns historically matched /duplicate.*key/i — order matters now.
const { classifyErrorCode } = await import('../src/core/sync.ts');
expect(classifyErrorCode(
'duplicate key value violates unique constraint on table "pages"'
)).toBe('DB_DUPLICATE_KEY');
expect(classifyErrorCode(
'duplicate key value violates unique constraint on table "pages"'
)).not.toBe('YAML_DUPLICATE_KEY');
});
});
// classifyErrorCode matches the canonical messages emitted by
// collectValidationErrors() in src/core/markdown.ts. Pre-fix, the regexes
// keyed off "missing open" / "missing close" / "empty frontmatter" — none
// of which are produced upstream. Today these all classify correctly.
describe('classifyErrorCode — canonical message coverage', () => {
test('MISSING_OPEN matches "File is empty or whitespace-only"', async () => {
const { classifyErrorCode } = await import('../src/core/sync.ts');
expect(classifyErrorCode(
'File is empty or whitespace-only; expected frontmatter starting with ---'
)).toBe('MISSING_OPEN');
});
test('MISSING_OPEN matches "Frontmatter must start with ---"', async () => {
const { classifyErrorCode } = await import('../src/core/sync.ts');
expect(classifyErrorCode(
'Frontmatter must start with --- on the first non-empty line'
)).toBe('MISSING_OPEN');
});
test('MISSING_CLOSE matches "No closing --- delimiter"', async () => {
const { classifyErrorCode } = await import('../src/core/sync.ts');
expect(classifyErrorCode('No closing --- delimiter found')).toBe('MISSING_CLOSE');
});
test('MISSING_CLOSE matches "Heading at line N found inside frontmatter"', async () => {
const { classifyErrorCode } = await import('../src/core/sync.ts');
expect(classifyErrorCode(
'Heading at line 5 found inside frontmatter zone (closing --- comes after)'
)).toBe('MISSING_CLOSE');
});
test('EMPTY_FRONTMATTER matches "Frontmatter block is empty"', async () => {
const { classifyErrorCode } = await import('../src/core/sync.ts');
expect(classifyErrorCode('Frontmatter block is empty')).toBe('EMPTY_FRONTMATTER');
});
test('NULL_BYTES matches "Content contains null bytes"', async () => {
const { classifyErrorCode } = await import('../src/core/sync.ts');
expect(classifyErrorCode('Content contains null bytes (likely binary corruption)'))
.toBe('NULL_BYTES');
});
test('NESTED_QUOTES matches "Nested double quotes"', async () => {
const { classifyErrorCode } = await import('../src/core/sync.ts');
expect(classifyErrorCode('Nested double quotes in YAML value at line 3'))
.toBe('NESTED_QUOTES');
});
});
// acknowledgeSyncFailures backfills `code` on legacy entries that were
// recorded before the code field existed (~/.gbrain/sync-failures.jsonl
// from pre-PR brains). Without this branch, upgraded users see "UNKNOWN"
// for every previously-recorded failure even when the message is parseable.
describe('acknowledgeSyncFailures — backfill on legacy entries', () => {
test('backfills code on entries that predate the code field', async () => {
const { acknowledgeSyncFailures, loadSyncFailures, syncFailuresPath } =
await import('../src/core/sync.ts');
// Hand-write a legacy entry with no `code` field. Mimics a pre-PR
// ~/.gbrain/sync-failures.jsonl row that exists on real upgrades.
const { mkdirSync } = await import('fs');
const { dirname } = await import('path');
mkdirSync(dirname(syncFailuresPath()), { recursive: true });
writeFileSync(
syncFailuresPath(),
JSON.stringify({
path: 'a.md',
error: 'Frontmatter slug "x" does not match path-derived slug "y"',
commit: 'old',
ts: '2025-01-01T00:00:00Z',
}) + '\n',
);
const result = acknowledgeSyncFailures();
expect(result.count).toBe(1);
expect(result.summary).toEqual([{ code: 'SLUG_MISMATCH', count: 1 }]);
const after = loadSyncFailures();
expect(after).toHaveLength(1);
expect(after[0].code).toBe('SLUG_MISMATCH');
expect(after[0].acknowledged).toBe(true);
});
test('preserves existing code field; never reclassifies', async () => {
const { acknowledgeSyncFailures, loadSyncFailures, syncFailuresPath } =
await import('../src/core/sync.ts');
const { mkdirSync } = await import('fs');
const { dirname } = await import('path');
mkdirSync(dirname(syncFailuresPath()), { recursive: true });
// Pre-classified entry — should NOT be re-run through classifier.
writeFileSync(
syncFailuresPath(),
JSON.stringify({
path: 'a.md',
error: 'some message that would otherwise classify as UNKNOWN',
code: 'CUSTOM_CODE',
commit: 'x',
ts: '2025-01-01T00:00:00Z',
}) + '\n',
);
const result = acknowledgeSyncFailures();
expect(result.summary).toEqual([{ code: 'CUSTOM_CODE', count: 1 }]);
expect(loadSyncFailures()[0].code).toBe('CUSTOM_CODE');
});
});
// formatCodeBreakdown is the DRY helper used by both the failures-array
// path (sync.ts blocked-by-failures + full-sync stderr) and the pre-summarized
// AcknowledgeResult.summary path (--skip-failed ack message). One renderer,
// two input shapes.
describe('formatCodeBreakdown — dual input shape', () => {
test('renders raw failures by classifying internally', async () => {
const { formatCodeBreakdown } = await import('../src/core/sync.ts');
const out = formatCodeBreakdown([
{ error: 'Frontmatter slug "a" does not match path-derived slug "b"' },
{ error: 'Frontmatter slug "c" does not match path-derived slug "d"' },
{ error: 'YAML parse failed: bad' },
]);
expect(out).toBe(' SLUG_MISMATCH: 2\n YAML_PARSE: 1');
});
test('renders pre-summarized {code, count} input directly', async () => {
const { formatCodeBreakdown } = await import('../src/core/sync.ts');
const out = formatCodeBreakdown([
{ code: 'SLUG_MISMATCH', count: 5 },
{ code: 'YAML_PARSE', count: 2 },
]);
expect(out).toBe(' SLUG_MISMATCH: 5\n YAML_PARSE: 2');
});
test('returns empty string for empty input', async () => {
const { formatCodeBreakdown } = await import('../src/core/sync.ts');
expect(formatCodeBreakdown([])).toBe('');
});
});