From 38507d743729f172c61a602d039ef5dd69573823 Mon Sep 17 00:00:00 2001 From: iliya Date: Fri, 20 Mar 2026 14:26:47 +0200 Subject: [PATCH] fix: handle ENOENT error gracefully in file reading functions - Updated error handling in both registerUtilityRoutes and handleReadMentionedFile functions to return null for expected ENOENT errors, indicating that the file simply does not exist. This improves robustness in scenarios with stale or misdetected file references. - Enhanced the isValidFileRef function to require either 3+ segments or a file extension to avoid false positives for npm scoped packages, ensuring more accurate file reference validation. --- src/main/http/utility.ts | 4 + src/main/ipc/utility.ts | 4 + src/renderer/utils/groupTransformer.ts | 14 +- test/renderer/utils/fileReferences.test.ts | 158 +++++++++++++++++++++ 4 files changed, 178 insertions(+), 2 deletions(-) create mode 100644 test/renderer/utils/fileReferences.test.ts diff --git a/src/main/http/utility.ts b/src/main/http/utility.ts index b3aa982f..e3839e71 100644 --- a/src/main/http/utility.ts +++ b/src/main/http/utility.ts @@ -111,6 +111,10 @@ export function registerUtilityRoutes(app: FastifyInstance): void { estimatedTokens, }; } catch (error) { + // ENOENT is expected — file simply doesn't exist (e.g. stale or misdetected references) + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + return null; + } logger.error( `Error in POST /api/read-mentioned-file for ${request.body.absolutePath}:`, error diff --git a/src/main/ipc/utility.ts b/src/main/ipc/utility.ts index 2ff7642a..182beca9 100644 --- a/src/main/ipc/utility.ts +++ b/src/main/ipc/utility.ts @@ -256,6 +256,10 @@ async function handleReadMentionedFile( estimatedTokens, }; } catch (error) { + // ENOENT is expected — file simply doesn't exist (e.g. stale or misdetected references) + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + return null; + } logger.error(`Error in read-mentioned-file for ${absolutePath}:`, error); return null; } diff --git a/src/renderer/utils/groupTransformer.ts b/src/renderer/utils/groupTransformer.ts index 6682fbca..0a975adf 100644 --- a/src/renderer/utils/groupTransformer.ts +++ b/src/renderer/utils/groupTransformer.ts @@ -13,7 +13,12 @@ import { isEnhancedSystemChunk, isEnhancedUserChunk, } from '@renderer/types/data'; -import { getFirstSegment, hasPathSeparator, isRelativePath } from '@renderer/utils/pathUtils'; +import { + getFirstSegment, + hasPathSeparator, + isRelativePath, + splitPathSegments, +} from '@renderer/utils/pathUtils'; import { isCommandContent, sanitizeDisplayContent } from '@shared/utils/contentSanitizer'; import { createLogger } from '@shared/utils/logger'; @@ -369,8 +374,13 @@ function isValidFileRef(path: string): boolean { return true; } // Check if contains a path separator (indicates directory structure) + // Require either 3+ segments or a file extension to avoid matching + // npm scoped packages like @scope/name (extracted as "scope/name") if (hasPathSeparator(path) && path.length > 2) { - return true; + const segments = splitPathSegments(path); + if (segments.length > 2 || /\.[a-zA-Z0-9]+$/.test(path)) { + return true; + } } return false; } diff --git a/test/renderer/utils/fileReferences.test.ts b/test/renderer/utils/fileReferences.test.ts new file mode 100644 index 00000000..ee7e5c0c --- /dev/null +++ b/test/renderer/utils/fileReferences.test.ts @@ -0,0 +1,158 @@ +import { describe, expect, it } from 'vitest'; + +import { extractFileReferences } from '@renderer/utils/groupTransformer'; + +describe('extractFileReferences', () => { + describe('accepts valid file references', () => { + it('accepts paths starting with known directories', () => { + const refs = extractFileReferences('Check @src/components/App.tsx'); + expect(refs).toHaveLength(1); + expect(refs[0].path).toBe('src/components/App.tsx'); + }); + + it('accepts utils/ paths (known dir)', () => { + const refs = extractFileReferences('See @utils/helpers'); + expect(refs).toHaveLength(1); + expect(refs[0].path).toBe('utils/helpers'); + }); + + it('accepts hooks/ paths (known dir)', () => { + const refs = extractFileReferences('Use @hooks/auth'); + expect(refs).toHaveLength(1); + expect(refs[0].path).toBe('hooks/auth'); + }); + + it('accepts lib/ paths (known dir)', () => { + const refs = extractFileReferences('In @lib/core'); + expect(refs).toHaveLength(1); + expect(refs[0].path).toBe('lib/core'); + }); + + it('accepts config/ paths (known dir)', () => { + const refs = extractFileReferences('Edit @config/database'); + expect(refs).toHaveLength(1); + expect(refs[0].path).toBe('config/database'); + }); + + it('accepts relative paths with ./', () => { + const refs = extractFileReferences('See @./src/file.ts'); + expect(refs).toHaveLength(1); + expect(refs[0].path).toBe('./src/file.ts'); + }); + + it('accepts relative paths with ../', () => { + const refs = extractFileReferences('See @../lib/utils.ts'); + expect(refs).toHaveLength(1); + expect(refs[0].path).toBe('../lib/utils.ts'); + }); + + it('accepts unknown-dir paths with 3+ segments', () => { + const refs = extractFileReferences('See @custom/deep/path.ts'); + expect(refs).toHaveLength(1); + expect(refs[0].path).toBe('custom/deep/path.ts'); + }); + + it('accepts unknown-dir paths with file extension', () => { + const refs = extractFileReferences('See @foo/bar.ts'); + expect(refs).toHaveLength(1); + expect(refs[0].path).toBe('foo/bar.ts'); + }); + + it('accepts node_modules paths (known dir)', () => { + const refs = extractFileReferences('Check @node_modules/lodash/index.js'); + expect(refs).toHaveLength(1); + expect(refs[0].path).toBe('node_modules/lodash/index.js'); + }); + + it('accepts types/ paths (known dir)', () => { + const refs = extractFileReferences('See @types/node'); + expect(refs).toHaveLength(1); + expect(refs[0].path).toBe('types/node'); + }); + + it('extracts multiple references from one string', () => { + const refs = extractFileReferences('Compare @src/a.ts and @lib/b.ts'); + expect(refs).toHaveLength(2); + expect(refs[0].path).toBe('src/a.ts'); + expect(refs[1].path).toBe('lib/b.ts'); + }); + + it('accepts tilde paths', () => { + const refs = extractFileReferences('See @~/projects/foo'); + expect(refs).toHaveLength(1); + expect(refs[0].path).toBe('~/projects/foo'); + }); + }); + + describe('rejects npm scoped packages', () => { + it('rejects @babel/core', () => { + const refs = extractFileReferences('Use @babel/core for transpilation'); + expect(refs).toHaveLength(0); + }); + + it('rejects @xyflow/react', () => { + const refs = extractFileReferences('Install @xyflow/react'); + expect(refs).toHaveLength(0); + }); + + it('rejects @dagrejs/dagre', () => { + const refs = extractFileReferences('Use @dagrejs/dagre for layout'); + expect(refs).toHaveLength(0); + }); + + it('rejects @testing-library/react', () => { + const refs = extractFileReferences('Import from @testing-library/react'); + expect(refs).toHaveLength(0); + }); + + it('rejects @vitest/ui', () => { + const refs = extractFileReferences('Run @vitest/ui'); + expect(refs).toHaveLength(0); + }); + + it('rejects @radix-ui/react-dialog', () => { + const refs = extractFileReferences('Use @radix-ui/react-dialog'); + expect(refs).toHaveLength(0); + }); + + it('rejects @codemirror/lang-javascript', () => { + const refs = extractFileReferences('Import @codemirror/lang-javascript'); + expect(refs).toHaveLength(0); + }); + + it('rejects @emotion/react', () => { + const refs = extractFileReferences('Style with @emotion/react'); + expect(refs).toHaveLength(0); + }); + }); + + describe('edge cases', () => { + it('returns empty for text without @', () => { + const refs = extractFileReferences('No mentions here'); + expect(refs).toHaveLength(0); + }); + + it('returns empty for empty string', () => { + const refs = extractFileReferences(''); + expect(refs).toHaveLength(0); + }); + + it('rejects bare @ mentions without path separator', () => { + const refs = extractFileReferences('Hello @user'); + expect(refs).toHaveLength(0); + }); + + it('handles mixed valid and invalid refs', () => { + const refs = extractFileReferences( + 'Use @babel/core and check @src/components/App.tsx' + ); + expect(refs).toHaveLength(1); + expect(refs[0].path).toBe('src/components/App.tsx'); + }); + + it('rejects single-segment bare names', () => { + const refs = extractFileReferences('@react is a library'); + expect(refs).toHaveLength(0); + }); + }); +});