fix(git-sync): self-heal a stale .git lock that wedged a space forever (D3-N3)

An interrupted git operation (a hard crash / OOM-kill / abrupt container stop mid
`git add`/`commit`/`checkout`) leaves a `.git/index.lock` (or a ref `*.lock`).
Git then refuses EVERY subsequent operation ("Unable to create '…/index.lock':
File exists"), so every poll cycle failed and the space's sync wedged INDEFINITELY
with no self-heal — the whole space stopped syncing until a human ran `rm` on the
lock (found via web-test restart/corruption charter, reproduced deterministically).

The daemon holds the per-space Redis lock and is the vault's ONLY writer, so any
`*.lock` reaching a fresh cycle is necessarily stale (no live git process holds it).
Add `VaultGit.clearStaleGitLocks()` and call it in the cycle preflight, right after
ensureRepo and before the mid-merge recovery — clearing index/HEAD/config/packed-refs/
MERGE_HEAD/ORIG_HEAD and the engine's ref locks (best-effort, missing = no-op).

Verified on the stand: a planted stale index.lock is now cleared and the space
recovers (edit reaches the vault, 0 "File exists" errors) — was wedged forever.
Unit test (real temp repo: index.lock blocks git add -> clear -> git add works);
git-sync suite green (707).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-03 07:25:48 +03:00
parent 8f7da77939
commit f0778cb85a
5 changed files with 70 additions and 1 deletions
+25
View File
@@ -141,6 +141,31 @@ describe('VaultGit (integration; temp repo)', () => {
expect(count.trim()).toBe('1');
});
it('clearStaleGitLocks removes a leftover index.lock so git ops work again (bug D3-N3)', async () => {
if (!available) return;
const vault = await freshDir();
const git = new VaultGit(vault);
await git.ensureRepo();
// Simulate an interrupted git op: a stale index.lock left behind. Git now
// refuses index-touching operations.
await writeFile(join(vault, '.git', 'index.lock'), '');
await expect(
execFileAsync('git', ['add', '-A'], { cwd: vault }),
).rejects.toThrow(/index\.lock/);
// The preflight clears it (the daemon is the vault's sole writer, so it is stale).
await git.clearStaleGitLocks();
// The lock is gone and git ops succeed again.
await expect(
execFileAsync('git', ['add', '-A'], { cwd: vault }),
).resolves.toBeDefined();
// Idempotent / safe when no lock exists.
await expect(git.clearStaleGitLocks()).resolves.toBeUndefined();
});
it('ensureRepo neutralizes correctness-affecting LOCAL config', async () => {
if (!available) return;
const vault = await freshDir();