GitNexus/gitnexus/test/unit/watch-failure-policy.test.ts
azizur100389 3f5ca8cdb7
fix(storage): guard stale file-lock reclamation (#3234)
* fix(storage): guard stale file-lock reclamation

* fix(storage): close lock recovery failure paths

* Address PR review feedback (#3234)

- Flush the lock-child stderr diagnostic before process.exit
- Treat explicit NaN timeouts as the default ceiling
- Document non-retryable guard timeouts on the worker IPC contract

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(storage): stop mislabeling live lock waits as orphan recovery

A brief peer inspect must not attach guardPath or send operators to
RUNBOOK delete steps. Refuse lock-free embeddings sync, stop --watch
only on a true guard timeout, and drop an unreadable self-created
guard before failing closed.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(autofix): apply prettier + eslint fixes via /autofix command

* Address PR review feedback (#3234)

- Verify lock/guard absence before degrading a denied main-lock create
- Launch the third contender from unlinkSync, not the dead rename path
- Document group-lock timeouts for unrecoverable guard leftovers

Co-authored-by: Cursor <cursoragent@cursor.com>

* style: prettier index-lock reclaim guard tests

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(storage): treat O_EXCL as the lock-file presence check

CodeQL flagged existsSync-then-wx on analyze.lock. Create with wx first and
only reclaim unreadable leftovers after grace, so a successor is never
unlinked from a lost race.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-09-12 11:47:12 +01:00

70 lines
2.2 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest';
const analyzeFailureMayHaveMutatedLiveIndex = vi.hoisted(() => vi.fn());
vi.mock('../../src/core/run-analyze.js', () => ({
analyzeFailureMayHaveMutatedLiveIndex,
runFullAnalysis: vi.fn(),
}));
import { shouldStopAfterWatchRefreshFailure } from '../../src/cli/analyze-watch.js';
import { IndexLockTimeoutError } from '../../src/storage/index-lock.js';
describe('watch refresh failure policy', () => {
beforeEach(() => analyzeFailureMayHaveMutatedLiveIndex.mockReset());
it('retries a queued pre-write failure even when incremental writes are in-place', () => {
const error = new Error('failed before live graph mutation');
analyzeFailureMayHaveMutatedLiveIndex.mockReturnValue(false);
expect(shouldStopAfterWatchRefreshFailure(error, ['src/a.ts'])).toBe(false);
});
it('stops only when a queued failure may have mutated the live graph', () => {
const error = new Error('failed during live graph mutation');
analyzeFailureMayHaveMutatedLiveIndex.mockReturnValue(true);
expect(shouldStopAfterWatchRefreshFailure(error, ['src/a.ts'])).toBe(true);
expect(shouldStopAfterWatchRefreshFailure(error, [])).toBe(false);
});
it('stops on an orphan-guard timeout even when the live index was not mutated', () => {
analyzeFailureMayHaveMutatedLiveIndex.mockReturnValue(false);
const error = new IndexLockTimeoutError(
{
v: 1,
pid: -1,
hostname: 'h',
startTime: null,
token: '',
invocationId: '<unreadable>',
acquiredAt: '',
},
30_000,
false,
'/tmp/analyze.lock.guard',
);
expect(shouldStopAfterWatchRefreshFailure(error, ['src/a.ts'])).toBe(true);
expect(shouldStopAfterWatchRefreshFailure(error, [])).toBe(true);
});
it('retries a live-holder lock timeout', () => {
analyzeFailureMayHaveMutatedLiveIndex.mockReturnValue(false);
const error = new IndexLockTimeoutError(
{
v: 1,
pid: 42,
hostname: 'h',
startTime: null,
token: 't',
invocationId: 'i',
acquiredAt: '',
},
5_000,
true,
);
expect(shouldStopAfterWatchRefreshFailure(error, ['src/a.ts'])).toBe(false);
});
});