diff --git a/packages/core/src/utils/paths.test.ts b/packages/core/src/utils/paths.test.ts index 09b58d41870..ebeb0a7ff64 100644 --- a/packages/core/src/utils/paths.test.ts +++ b/packages/core/src/utils/paths.test.ts @@ -561,6 +561,36 @@ describe('resolveToRealPath', () => { expect(resolveToRealPath(input)).toBe(expected); }); + it('should return input path even if fs.realpathSync fails with ENAMETOOLONG', () => { + // Regression test for #26010: pasting long strings that the at-mention + // regex misinterprets as a path must not throw an unhandled ENAMETOOLONG + // up to the caller. + vi.spyOn(fs, 'realpathSync').mockImplementationOnce(() => { + const err = new Error('name too long') as NodeJS.ErrnoException; + err.code = 'ENAMETOOLONG'; + throw err; + }); + + const longInput = path.resolve('/tmp', 'a'.repeat(4096)); + + expect(resolveToRealPath(longInput)).toBe(longInput); + }); + + it('should return input path even if fs.realpathSync fails with ENOTDIR', () => { + // Regression test for #26010: an intermediate path component being a + // regular file (e.g. @path/to/file.json/extra) surfaces ENOTDIR from + // realpathSync — handle it the same way as a non-existent path. + vi.spyOn(fs, 'realpathSync').mockImplementationOnce(() => { + const err = new Error('not a directory') as NodeJS.ErrnoException; + err.code = 'ENOTDIR'; + throw err; + }); + + const input = path.resolve('/tmp', 'file.json', 'extra'); + + expect(resolveToRealPath(input)).toBe(input); + }); + it('should recursively resolve symlinks for non-existent child paths', () => { const parentPath = path.resolve('/some/parent/path'); const resolvedParentPath = path.resolve('/resolved/parent/path'); diff --git a/packages/core/src/utils/paths.ts b/packages/core/src/utils/paths.ts index fee8b8d8556..12163ea482d 100644 --- a/packages/core/src/utils/paths.ts +++ b/packages/core/src/utils/paths.ts @@ -412,6 +412,14 @@ export function resolveToRealPath(pathStr: string): string { return robustRealpath(path.resolve(resolvedPath)); } +function hasErrorCode(e: unknown, codes: readonly string[]): boolean { + if (!e || typeof e !== 'object' || !('code' in e)) { + return false; + } + const code = e.code; + return typeof code === 'string' && codes.includes(code); +} + function robustRealpath(p: string, visited = new Set()): string { const key = process.platform === 'win32' ? p.toLowerCase() : p; if (visited.has(key)) { @@ -421,12 +429,7 @@ function robustRealpath(p: string, visited = new Set()): string { try { return fs.realpathSync(p); } catch (e: unknown) { - if ( - e && - typeof e === 'object' && - 'code' in e && - (e.code === 'ENOENT' || e.code === 'EISDIR') - ) { + if (hasErrorCode(e, ['ENOENT', 'EISDIR'])) { try { const stat = fs.lstatSync(p); if (stat.isSymbolicLink()) { @@ -437,14 +440,7 @@ function robustRealpath(p: string, visited = new Set()): string { } catch (lstatError: unknown) { // Not a symlink, or lstat failed. Re-throw if it's not an expected // ENOENT (e.g., a permissions error), otherwise resolve parent. - if ( - !( - lstatError && - typeof lstatError === 'object' && - 'code' in lstatError && - (lstatError.code === 'ENOENT' || lstatError.code === 'EISDIR') - ) - ) { + if (!hasErrorCode(lstatError, ['ENOENT', 'EISDIR'])) { throw lstatError; } } @@ -452,6 +448,14 @@ function robustRealpath(p: string, visited = new Set()): string { if (parent === p) return p; return path.join(robustRealpath(parent, visited), path.basename(p)); } + if (hasErrorCode(e, ['ENAMETOOLONG', 'ENOTDIR'])) { + // The input is not a resolvable real path — for example a caller + // is probing whether a pasted string is a file reference (see the + // @-mention path parsing in atCommandProcessor). Returning the + // input unchanged lets downstream fileExists / fs.stat calls + // reject it normally instead of surfacing an unhandled rejection. + return p; + } throw e; } }