diff --git a/packages/core/src/config/projectRegistry.test.ts b/packages/core/src/config/projectRegistry.test.ts index e7e532bb2b..c1e9c81709 100644 --- a/packages/core/src/config/projectRegistry.test.ts +++ b/packages/core/src/config/projectRegistry.test.ts @@ -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( diff --git a/packages/core/src/config/projectRegistry.ts b/packages/core/src/config/projectRegistry.ts index 1aec0b7ad2..3e57032bea 100644 --- a/packages/core/src/config/projectRegistry.ts +++ b/packages/core/src/config/projectRegistry.ts @@ -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 { diff --git a/packages/core/src/tools/trackerTools.test.ts b/packages/core/src/tools/trackerTools.test.ts index 5ec59cdf60..516f652dd2 100644 --- a/packages/core/src/tools/trackerTools.test.ts +++ b/packages/core/src/tools/trackerTools.test.ts @@ -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); }); diff --git a/packages/core/src/utils/trust.test.ts b/packages/core/src/utils/trust.test.ts index f5930972ff..fe40318d97 100644 --- a/packages/core/src/utils/trust.test.ts +++ b/packages/core/src/utils/trust.test.ts @@ -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 = ` { diff --git a/packages/core/src/utils/trust.ts b/packages/core/src/utils/trust.ts index bf78746908..eb5791e8ec 100644 --- a/packages/core/src/utils/trust.ts +++ b/packages/core/src/utils/trust.ts @@ -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);