diff --git a/packages/cli/src/ui/hooks/atCommandProcessor.test.ts b/packages/cli/src/ui/hooks/atCommandProcessor.test.ts index ca2ecf7bc1..8076d8a2be 100644 --- a/packages/cli/src/ui/hooks/atCommandProcessor.test.ts +++ b/packages/cli/src/ui/hooks/atCommandProcessor.test.ts @@ -17,6 +17,7 @@ import { handleAtCommand, escapeAtSymbols, unescapeLiteralAt, + checkPermissions, } from './atCommandProcessor.js'; import { FileDiscoveryService, @@ -1539,4 +1540,23 @@ describe('unescapeLiteralAt', () => { const input = 'user@example.com and @scope/pkg'; expect(unescapeLiteralAt(escapeAtSymbols(input))).toBe(input); }); + + describe('checkPermissions', () => { + it('should handle ENAMETOOLONG gracefully in checkPermissions', async () => { + const longPath = 'a'.repeat(5000); + const query = `@${longPath}`; + + const localMockConfig = { + getTargetDir: () => '.', + validatePathAccess: () => true, + getResourceRegistry: () => ({ + findResourceByUri: () => undefined, + }), + } as unknown as Config; + + // checkPermissions should not throw ENAMETOOLONG + const permissions = await checkPermissions(query, localMockConfig); + expect(permissions).toEqual([]); + }); + }); }); diff --git a/packages/cli/src/ui/hooks/atCommandProcessor.ts b/packages/cli/src/ui/hooks/atCommandProcessor.ts index 512fe952ba..38cb541f1b 100644 --- a/packages/cli/src/ui/hooks/atCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/atCommandProcessor.ts @@ -188,9 +188,15 @@ export async function checkPermissions( const pathName = part.content.substring(1); if (!pathName) continue; - const resolvedPathName = resolveToRealPath( - path.resolve(config.getTargetDir(), pathName), - ); + let resolvedPathName: string; + try { + resolvedPathName = resolveToRealPath( + path.resolve(config.getTargetDir(), pathName), + ); + } catch { + // If path resolution fails (e.g. ENAMETOOLONG), skip this path + continue; + } if (config.validatePathAccess(resolvedPathName, 'read')) { if (await fileExists(resolvedPathName)) { diff --git a/packages/core/src/utils/paths.test.ts b/packages/core/src/utils/paths.test.ts index bb2801a9ad..4d08c85548 100644 --- a/packages/core/src/utils/paths.test.ts +++ b/packages/core/src/utils/paths.test.ts @@ -602,6 +602,19 @@ describe('resolveToRealPath', () => { /Infinite recursion detected/, ); }); + + it('should handle ENAMETOOLONG gracefully', () => { + const longPath = path.resolve('/' + 'a'.repeat(5000)); + + vi.spyOn(fs, 'realpathSync').mockImplementation(() => { + const err = new Error('ENAMETOOLONG') as NodeJS.ErrnoException; + err.code = 'ENAMETOOLONG'; + throw err; + }); + + // Should return the path itself if realpathSync fails with ENAMETOOLONG + expect(resolveToRealPath(longPath)).toBe(longPath); + }); }); describe('makeRelative', () => { diff --git a/packages/core/src/utils/paths.ts b/packages/core/src/utils/paths.ts index 70afe289fa..064db93a74 100644 --- a/packages/core/src/utils/paths.ts +++ b/packages/core/src/utils/paths.ts @@ -440,7 +440,10 @@ function robustRealpath(p: string, visited = new Set()): string { e && typeof e === 'object' && 'code' in e && - (e.code === 'ENOENT' || e.code === 'EISDIR') + (e.code === 'ENOENT' || + e.code === 'EISDIR' || + e.code === 'ENAMETOOLONG' || + e.code === 'EINVAL') ) { try { const stat = fs.lstatSync(p); @@ -457,7 +460,10 @@ function robustRealpath(p: string, visited = new Set()): string { lstatError && typeof lstatError === 'object' && 'code' in lstatError && - (lstatError.code === 'ENOENT' || lstatError.code === 'EISDIR') + (lstatError.code === 'ENOENT' || + lstatError.code === 'EISDIR' || + lstatError.code === 'ENAMETOOLONG' || + lstatError.code === 'EINVAL') ) ) { throw lstatError;