fix(sync): address adversarial review findings on the failure ledger (#1939)

- #1: a parse-failed file that is later deleted/renamed-away no longer leaves
  a permanent open ledger row. Removed paths (filtered.deleted, renamed-from,
  and the "gone from disk" forward-delete skip branch) are treated as resolved
  so the ledger self-heals instead of aging doctor to a stuck FAIL.
- #3: decideSyncFailureSeverity escalates to FAIL on OPEN (blocking) failures
  only — auto_skipped rows already advanced the bookmark, so they stay
  WARN-visible regardless of count, matching the state-machine contract.
This commit is contained in:
Garry Tan
2026-06-07 12:41:49 -07:00
parent e137893094
commit 7d78bc593d
4 changed files with 55 additions and 4 deletions
+26
View File
@@ -628,4 +628,30 @@ describeE2E('E2E: sync --skip-failed structured summary loop (v0.22.12, issue #5
else process.env.GBRAIN_SYNC_AUTOSKIP_AFTER = prevThreshold;
}
});
// issue #1939 adversarial finding #1: a parse-failed file that is later DELETED
// from the repo must not leave a permanent open ledger row (which would age
// doctor to FAIL forever). The incremental gate treats removed paths as resolved.
test('deleting a failed file clears its ledger row (self-heal, no stuck FAIL)', async () => {
const { performSync } = await import('../../src/commands/sync.ts');
const { loadSyncFailures } = await import('../../src/core/sync.ts');
const engine = getEngine();
writeFileSync(join(repoPath, 'people/gonepoison.md'), [
'---', 'type: person', 'title: Gone', 'slug: wrong-derived-slug', '---', '', 'Body.',
].join('\n'));
execSync('git add -A && git commit -m "add a file that fails to parse"', { cwd: repoPath, stdio: 'pipe' });
// Sync blocks; an open ledger row exists for the bad file.
let result = await performSync(engine, { repoPath, noPull: true, noEmbed: true });
expect(result.status).toBe('blocked_by_failures');
expect(loadSyncFailures().some(f => f.path.includes('gonepoison'))).toBe(true);
// Delete the file and sync. The removed path is treated as resolved, so the
// ledger row is cleared and the bookmark advances.
execSync('git rm people/gonepoison.md && git commit -m "delete the bad file"', { cwd: repoPath, stdio: 'pipe' });
result = await performSync(engine, { repoPath, noPull: true, noEmbed: true });
expect(result.status).not.toBe('blocked_by_failures');
expect(loadSyncFailures().some(f => f.path.includes('gonepoison'))).toBe(false);
});
});
+8 -1
View File
@@ -189,10 +189,17 @@ describe('#1 severity — auto_skipped stays visible (WARN)', () => {
nowMs: now, failHours,
}).status).toBe('fail');
// 10 recent open → fail (count)
// 10 recent OPEN → fail (count of blocking failures)
const ten = Array.from({ length: 10 }, (_, i) => ({ ...base, path: `a${i}.md`, state: 'open' as const, ts: '2026-06-06T23:00:00Z' }));
expect(decideSyncFailureSeverity({ entries: ten, nowMs: now, failHours }).status).toBe('fail');
// 10 recent AUTO_SKIPPED → still warn (#3): the valve already advanced the
// bookmark, so indexing is not wedged. Visible, not gating.
const tenSkipped = Array.from({ length: 10 }, (_, i) => ({ ...base, path: `s${i}.md`, state: 'auto_skipped' as const, ts: '2026-06-06T23:00:00Z' }));
const skSev = decideSyncFailureSeverity({ entries: tenSkipped, nowMs: now, failHours });
expect(skSev.status).toBe('warn');
expect(skSev.auto_skipped).toBe(10);
// auto_skipped only → warn (still visible), counted
const sev = decideSyncFailureSeverity({
entries: [{ ...base, state: 'auto_skipped', ts: '2026-06-01T00:00:00Z' }],