fix(core): prevent ENOENT crash due to proper-lockfile race condition

This commit is contained in:
Spencer
2026-04-23 20:41:07 +00:00
parent 1f73ec70c5
commit bf4017b9c0
5 changed files with 63 additions and 2 deletions
@@ -294,6 +294,29 @@ describe('ProjectRegistry', () => {
);
});
it('safely handles ECOMPROMISED errors from proper-lockfile', async () => {
const registry = new ProjectRegistry(registryPath);
await registry.initialize();
let compromisedHandler: ((err: Error) => void) | undefined;
vi.mocked(lock).mockImplementation(async (file, options) => {
// @ts-expect-error onCompromised is not typed in the mock but is passed
compromisedHandler = options.onCompromised;
return vi.fn().mockResolvedValue(undefined);
});
await registry.getShortId('/foo');
expect(compromisedHandler).toBeDefined();
// Calling the handler should not throw
expect(() =>
compromisedHandler!(
Object.assign(new Error('Compromised'), { code: 'ECOMPROMISED' }),
),
).not.toThrow();
});
it('throws if not initialized', async () => {
const registry = new ProjectRegistry(registryPath);
await expect(registry.getShortId('/foo')).rejects.toThrow(
@@ -179,6 +179,12 @@ export class ProjectRegistry {
retries: Math.floor(LOCK_TIMEOUT_MS / LOCK_RETRY_DELAY_MS),
minTimeout: LOCK_RETRY_DELAY_MS,
},
onCompromised: (err) => {
debugLogger.debug(
'Project registry lock was compromised (likely released concurrently):',
err,
);
},
});
try {
+4 -2
View File
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { Config } from '../config/config.js';
import { MessageBus } from '../confirmation-bus/message-bus.js';
import type { PolicyEngine } from '../policy/policy-engine.js';
@@ -37,7 +37,9 @@ describe('Tracker Tools Integration', () => {
model: 'gemini-3-flash',
debugMode: false,
});
await config.initialize();
vi.spyOn(config.storage, 'getProjectTempTrackerDir').mockReturnValue(
tempDir,
);
messageBus = new MessageBus(null as unknown as PolicyEngine, false);
});
+22
View File
@@ -119,6 +119,28 @@ describe('Trust Utility (Core)', () => {
expect(savedContent[finalKey]).toBe(TrustLevel.TRUST_FOLDER);
});
it('safely handles ECOMPROMISED errors from proper-lockfile', async () => {
let compromisedHandler: ((err: Error) => void) | undefined;
vi.mocked(lock).mockImplementation(async (file, options) => {
// @ts-expect-error onCompromised is not typed in the mock but is passed
compromisedHandler = options.onCompromised;
return vi.fn().mockResolvedValue(undefined);
});
const folders = loadTrustedFolders();
const testPath = path.resolve('/new/trusted/path');
await folders.setValue(testPath, TrustLevel.TRUST_FOLDER);
expect(compromisedHandler).toBeDefined();
// Calling the handler should not throw
expect(() =>
compromisedHandler!(
Object.assign(new Error('Compromised'), { code: 'ECOMPROMISED' }),
),
).not.toThrow();
});
it('should handle comments in JSON', () => {
const content = `
{
+8
View File
@@ -14,6 +14,7 @@ import { normalizePath, isSubpath } from './paths.js';
import { FatalConfigError, getErrorMessage } from './errors.js';
import { coreEvents } from './events.js';
import { ideContextStore } from '../ide/ideContext.js';
import { debugLogger } from './debugLogger.js';
export enum TrustLevel {
TRUST_FOLDER = 'TRUST_FOLDER',
@@ -217,6 +218,13 @@ export class LoadedTrustedFolders {
retries: 10,
minTimeout: 100,
},
onCompromised: (err) => {
// Ignore compromised lock since we expect another process might release it concurrently
debugLogger.debug(
'Trusted folders lock was compromised (likely released concurrently):',
err,
);
},
});
const normalizedPath = normalizePath(folderPath);