mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-01 02:01:24 +00:00
* 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>
78 lines
3 KiB
TypeScript
78 lines
3 KiB
TypeScript
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
|
|
import fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import os from 'node:os';
|
|
|
|
const { acquire, release, pipeline, resolvePlacement } = vi.hoisted(() => ({
|
|
acquire: vi.fn(),
|
|
release: vi.fn(),
|
|
pipeline: vi.fn(),
|
|
resolvePlacement: vi.fn(async (): Promise<{ branch?: string }> => ({})),
|
|
}));
|
|
vi.mock('../../src/storage/index-lock.js', async (original) => ({
|
|
...(await original<typeof import('../../src/storage/index-lock.js')>()),
|
|
acquireIndexLock: acquire,
|
|
}));
|
|
vi.mock('../../src/core/ingestion/pipeline.js', () => ({ runPipelineFromRepo: pipeline }));
|
|
vi.mock('../../src/storage/repo-manager.js', async (original) => ({
|
|
...(await original<typeof import('../../src/storage/repo-manager.js')>()),
|
|
resolveBranchPlacement: (...args: unknown[]) => resolvePlacement(...args),
|
|
}));
|
|
|
|
import { runFullAnalysis } from '../../src/core/run-analyze.js';
|
|
import { unregisterRepo } from '../../src/storage/repo-manager.js';
|
|
|
|
let home: string;
|
|
beforeEach(async () => {
|
|
home = await fs.mkdtemp(path.join(os.tmpdir(), 'gnx-required-lock-'));
|
|
vi.stubEnv('GITNEXUS_HOME', home);
|
|
vi.stubEnv('GITNEXUS_FTS_STEMMER', 'porter');
|
|
vi.clearAllMocks();
|
|
acquire.mockResolvedValue({ lockFree: true, release });
|
|
resolvePlacement.mockReset().mockResolvedValue({});
|
|
});
|
|
afterEach(async () => {
|
|
vi.unstubAllEnvs();
|
|
await fs.rm(home, { recursive: true, force: true });
|
|
});
|
|
|
|
it('refuses analysis without exclusive ownership before invoking the pipeline', async () => {
|
|
await expect(runFullAnalysis(home, {}, { onProgress: () => {} })).rejects.toThrow(
|
|
'refusing an unlocked analysis',
|
|
);
|
|
expect(acquire).toHaveBeenCalled();
|
|
expect(pipeline).not.toHaveBeenCalled();
|
|
expect(release).toHaveBeenCalledOnce();
|
|
await expect(fs.stat(path.join(home, '.gitnexus', 'lbug'))).rejects.toMatchObject({
|
|
code: 'ENOENT',
|
|
});
|
|
});
|
|
|
|
it('refuses a registry transaction without exclusive ownership', async () => {
|
|
const registry = path.join(home, 'registry.json');
|
|
const original = JSON.stringify([
|
|
{
|
|
name: 'keep',
|
|
path: home,
|
|
storagePath: path.join(home, '.gitnexus'),
|
|
indexedAt: '',
|
|
lastCommit: '',
|
|
},
|
|
]);
|
|
await fs.writeFile(registry, original);
|
|
await expect(unregisterRepo(home)).rejects.toThrow('refusing an unlocked registry transaction');
|
|
expect(await fs.readFile(registry, 'utf8')).toBe(original);
|
|
expect(release).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('refuses analysis without exclusive ownership after the write slot moves', async () => {
|
|
resolvePlacement.mockResolvedValueOnce({}).mockResolvedValueOnce({ branch: 'moved' });
|
|
acquire.mockResolvedValueOnce({ release }).mockResolvedValueOnce({ lockFree: true, release });
|
|
|
|
await expect(
|
|
runFullAnalysis(home, { branch: 'moved' }, { onProgress: () => {} }),
|
|
).rejects.toThrow('refusing an unlocked analysis');
|
|
expect(acquire).toHaveBeenCalledTimes(2);
|
|
expect(pipeline).not.toHaveBeenCalled();
|
|
expect(release).toHaveBeenCalled();
|
|
});
|