11// Create mock vscode module before importing anything
2+ import * as path from "path"
23const createMockUri = ( scheme : string , path : string ) => ( {
34 scheme,
45 authority : "" ,
@@ -73,22 +74,54 @@ const mockVscode = {
7374 } ,
7475}
7576
77+ // Mock fs
78+ const mockFs = {
79+ stat : jest . fn ( ) ,
80+ readdir : jest . fn ( ) ,
81+ }
82+
83+ // Mock extract text function
84+ const mockExtractTextFromFile = jest . fn ( )
85+
86+ // Mock open file function
87+ const mockOpenFile = jest . fn ( )
88+
7689// Mock modules
7790jest . mock ( "vscode" , ( ) => mockVscode )
7891jest . mock ( "../../../services/browser/UrlContentFetcher" )
7992jest . mock ( "../../../utils/git" )
8093jest . mock ( "../../../utils/path" )
94+ jest . mock ( "../../../integrations/misc/open-file" , ( ) => ( {
95+ openFile : jest . fn ( ) . mockImplementation ( ( path ) => mockOpenFile ( path ) ) ,
96+ } ) )
97+ jest . mock ( "fs/promises" , ( ) => mockFs )
98+ jest . mock ( "../../../integrations/misc/extract-text" , ( ) => ( {
99+ extractTextFromFile : jest . fn ( ) . mockImplementation ( ( path ) => mockExtractTextFromFile ( path ) ) ,
100+ } ) )
81101
82102// Now import the modules that use the mocks
83103import { parseMentions , openMention } from "../index"
84104import { UrlContentFetcher } from "../../../services/browser/UrlContentFetcher"
85105import * as git from "../../../utils/git"
106+ import { openFile } from "../../../integrations/misc/open-file"
107+ import { extractTextFromFile } from "../../../integrations/misc/extract-text"
86108
87109import { getWorkspacePath } from "../../../utils/path"
88110; ( getWorkspacePath as jest . Mock ) . mockReturnValue ( "/test/workspace" )
89111
112+ // We need to handle the console.error properly for tests
113+ const originalConsoleError = console . error
114+ // Replace console.error with a mock during tests
115+ console . error = jest . fn ( )
116+
117+ // Helper function to normalize path for cross-platform tests
118+ function normalizePath ( pathString : string ) : string {
119+ // Replace backslashes with forward slashes for comparison
120+ return pathString . replace ( / \\ / g, "/" )
121+ }
122+
90123describe ( "mentions" , ( ) => {
91- const mockCwd = "/ test/ workspace"
124+ const mockCwd = "C:\\ test\\ workspace"
92125 let mockUrlContentFetcher : UrlContentFetcher
93126
94127 beforeEach ( ( ) => {
@@ -101,14 +134,23 @@ describe("mentions", () => {
101134 urlToMarkdown : jest . fn ( ) . mockResolvedValue ( "" ) ,
102135 } as unknown as UrlContentFetcher
103136
104- // Reset all vscode mocks
137+ // Reset all mocks
105138 mockVscode . workspace . fs . stat . mockReset ( )
106139 mockVscode . workspace . fs . writeFile . mockReset ( )
107140 mockVscode . workspace . openTextDocument . mockReset ( ) . mockResolvedValue ( { } )
108141 mockVscode . window . showTextDocument . mockReset ( ) . mockResolvedValue ( { } )
109142 mockVscode . window . showErrorMessage . mockReset ( )
110143 mockExecuteCommand . mockReset ( )
111144 mockOpenExternal . mockReset ( )
145+ mockFs . stat . mockReset ( )
146+ mockFs . readdir . mockReset ( )
147+ mockExtractTextFromFile . mockReset ( )
148+ mockOpenFile . mockReset ( )
149+ } )
150+
151+ afterAll ( ( ) => {
152+ // Restore original console.error after all tests
153+ console . error = originalConsoleError
112154 } )
113155
114156 describe ( "parseMentions" , ( ) => {
@@ -144,20 +186,212 @@ Detailed commit message with multiple lines
144186 expect ( result ) . toContain ( `<git_commit hash="${ commitHash } ">` )
145187 expect ( result ) . toContain ( `Error fetching commit info: ${ errorMessage } ` )
146188 } )
189+
190+ it ( "should correctly handle file mentions with escaped spaces" , async ( ) => {
191+ // Setup mocks
192+ const fileContent = "This is the content of the file with spaces"
193+ mockExtractTextFromFile . mockResolvedValue ( fileContent )
194+ mockFs . stat . mockResolvedValue ( {
195+ isFile : ( ) => true ,
196+ isDirectory : ( ) => false ,
197+ } as any )
198+
199+ const testText = "Check this file: @/path/with\\ spaces/test\\ file.txt"
200+ await parseMentions ( testText , mockCwd , mockUrlContentFetcher )
201+
202+ // Call parseMentions
203+ const result = await parseMentions ( testText , mockCwd , mockUrlContentFetcher )
204+
205+ // Get the path that was passed to extractTextFromFile
206+ const extractFilePath = mockExtractTextFromFile . mock . calls [ 0 ] [ 0 ]
207+
208+ // Construct the expected unescaped path in a platform-independent way
209+ const expectedUnescapedPath = path . resolve ( mockCwd , "path" , "with spaces" , "test file.txt" )
210+
211+ // For debugging
212+ console . log ( "Actual path in test:" , extractFilePath )
213+ console . log ( "Expected path in test:" , expectedUnescapedPath )
214+
215+ // Verify the path passed was the correctly unescaped path
216+ expect ( normalizePath ( extractFilePath ) ) . toEqual ( normalizePath ( expectedUnescapedPath ) )
217+
218+ // Verify result includes file content
219+ expect ( result ) . toContain ( fileContent )
220+
221+ // Verify the path in the result still maintains escaped form for display
222+ expect ( result ) . toContain ( '<file_content path="path/with\\ spaces/test\\ file.txt">' )
223+ } )
224+
225+ it ( "should correctly handle directory mentions with escaped spaces" , async ( ) => {
226+ // Setup directory structure mock
227+ mockFs . stat . mockResolvedValue ( {
228+ isFile : ( ) => false ,
229+ isDirectory : ( ) => true ,
230+ } as any )
231+
232+ // Mock directory entries
233+ mockFs . readdir . mockResolvedValue ( [
234+ { name : "file1.txt" , isFile : ( ) => true , isDirectory : ( ) => false } ,
235+ { name : "file with spaces.txt" , isFile : ( ) => true , isDirectory : ( ) => false } ,
236+ { name : "sub dir" , isDirectory : ( ) => true , isFile : ( ) => false } ,
237+ ] as any )
238+
239+ // Test text with a directory mention containing escaped spaces
240+ const testText = "Check this directory: @/path/with\\ spaces/"
241+
242+ // Call parseMentions
243+ const result = await parseMentions ( testText , mockCwd , mockUrlContentFetcher )
244+
245+ // Get the path that was passed to readdir
246+ const readdirPath = mockFs . readdir . mock . calls [ 0 ] [ 0 ]
247+
248+ // Construct the expected unescaped path
249+ const expectedUnescapedPath = path . resolve ( mockCwd , "path" , "with spaces" )
250+
251+ // For debugging
252+ console . log ( "Actual directory path in test:" , readdirPath )
253+ console . log ( "Expected directory path in test:" , expectedUnescapedPath )
254+
255+ // Verify fs.readdir was called with proper unescaped path
256+ expect ( normalizePath ( readdirPath ) ) . toEqual ( normalizePath ( expectedUnescapedPath ) )
257+
258+ // Verify directory listing in result
259+ expect ( result ) . toContain ( "file1.txt" )
260+ expect ( result ) . toContain ( "file with spaces.txt" )
261+ expect ( result ) . toContain ( "sub dir/" )
262+
263+ // Verify the path in the result still maintains escaped form for display
264+ expect ( result ) . toContain ( '<folder_content path="path/with\\ spaces/">' )
265+ } )
266+
267+ // Test the internal unescapePathSpaces function indirectly through parseMentions
268+ it ( "should properly handle various escape patterns" , async ( ) => {
269+ // Setup mocks
270+ mockExtractTextFromFile . mockResolvedValue ( "test content" )
271+ mockFs . stat . mockResolvedValue ( {
272+ isFile : ( ) => true ,
273+ isDirectory : ( ) => false ,
274+ } as any )
275+
276+ // Test specific escape patterns one at a time
277+ const testEscapedPath = "/path/with\\ one\\ space.txt"
278+ const testText = `Testing @${ testEscapedPath } `
279+
280+ // Call parseMentions
281+ await parseMentions ( testText , mockCwd , mockUrlContentFetcher )
282+
283+ // Get the path that was passed to extractTextFromFile
284+ const extractFilePath = mockExtractTextFromFile . mock . calls [ 0 ] [ 0 ]
285+
286+ // Construct expected unescaped path
287+ const expectedUnescapedPath = path . resolve ( mockCwd , "path" , "with one space.txt" )
288+
289+ // For debugging
290+ console . log ( "Actual path with spaces in test:" , extractFilePath )
291+ console . log ( "Expected path with spaces in test:" , expectedUnescapedPath )
292+
293+ // Verify the path was correctly unescaped
294+ expect ( normalizePath ( extractFilePath ) ) . toEqual ( normalizePath ( expectedUnescapedPath ) )
295+ } )
296+
297+ // Additional test to cover more edge cases and complex paths
298+ it ( "should handle complex paths and multiple mentions in the same text" , async ( ) => {
299+ // Setup mocks
300+ mockExtractTextFromFile . mockResolvedValue ( "File content" )
301+ mockFs . stat . mockResolvedValue ( {
302+ isFile : ( ) => true ,
303+ isDirectory : ( ) => false ,
304+ } as any )
305+
306+ // Test with a single complex path with multiple spaces
307+ const complexPath = "/complex/path\\ with\\ multiple\\ spaces/file.txt"
308+ const testText = `Test complex path: @${ complexPath } `
309+
310+ // Call parseMentions
311+ const result = await parseMentions ( testText , mockCwd , mockUrlContentFetcher )
312+
313+ // Get the path that was passed to extractTextFromFile
314+ const extractFilePath = mockExtractTextFromFile . mock . calls [ 0 ] [ 0 ]
315+
316+ // Construct expected path
317+ const expectedPath = path . resolve ( mockCwd , "complex" , "path with multiple spaces" , "file.txt" )
318+
319+ // Verify the path was correctly resolved
320+ expect ( normalizePath ( extractFilePath ) ) . toEqual ( normalizePath ( expectedPath ) )
321+
322+ // Verify XML tag format is correct (no leading slash, backslashes preserved)
323+ expect ( result ) . toContain ( '<file_content path="complex/path\\ with\\ multiple\\ spaces/file.txt">' )
324+ } )
325+
326+ // Test platform-specific path handling without modifying Node's path module
327+ it ( "should correctly handle paths with both forward and backslashes" , async ( ) => {
328+ // Setup mocks
329+ mockExtractTextFromFile . mockResolvedValue ( "File content" )
330+ mockFs . stat . mockResolvedValue ( {
331+ isFile : ( ) => true ,
332+ isDirectory : ( ) => false ,
333+ } as any )
334+
335+ // Test path with forward slashes (Unix-style)
336+ const unixStylePath = "/unix/style/path\\ with/spaces/file.txt"
337+ const unixText = `Check this Unix path: @${ unixStylePath } `
338+
339+ // Call parseMentions for Unix style
340+ const unixResult = await parseMentions ( unixText , mockCwd , mockUrlContentFetcher )
341+
342+ // Get path from first call
343+ const unixExtractPath = mockExtractTextFromFile . mock . calls [ 0 ] [ 0 ]
344+
345+ // Reset mocks
346+ jest . clearAllMocks ( )
347+
348+ // Test path with backslashes (Windows-style)
349+ const winStylePath = "/windows\\\\style/path\\ with\\\\spaces/file.txt"
350+ const winText = `Check this Windows path: @${ winStylePath } `
351+
352+ // Call parseMentions for Windows style
353+ const winResult = await parseMentions ( winText , mockCwd , mockUrlContentFetcher )
354+
355+ // Get path from first call
356+ const winExtractPath = mockExtractTextFromFile . mock . calls [ 0 ] [ 0 ]
357+
358+ // Both paths should be normalized by normalizePath
359+ expect ( normalizePath ( unixExtractPath ) ) . toContain ( "unix/style/path with/spaces/file.txt" )
360+ expect ( normalizePath ( winExtractPath ) ) . toContain ( "windows/style/path with/spaces/file.txt" )
361+
362+ // XML tags should preserve the original slash style
363+ expect ( unixResult ) . toContain ( '<file_content path="unix/style/path\\ with/spaces/file.txt">' )
364+ expect ( winResult ) . toContain ( '<file_content path="windows\\\\style/path\\ with\\\\spaces/file.txt">' )
365+ } )
147366 } )
148367
149368 describe ( "openMention" , ( ) => {
150369 it ( "should handle file paths and problems" , async ( ) => {
151- // Mock stat to simulate file not existing
152- mockVscode . workspace . fs . stat . mockRejectedValueOnce ( new Error ( "File does not exist" ) )
370+ // We need a special wrapper for handling errors in tests
371+ let errorThrown = false
372+
373+ // Mock error function to simulate file not existing
374+ mockOpenFile . mockImplementationOnce ( ( ) => {
375+ errorThrown = true
376+ throw new Error ( "File does not exist" )
377+ } )
378+
379+ try {
380+ // Call openMention and wait for it to complete
381+ await openMention ( "/path/to/file" )
382+ } catch ( e ) {
383+ // We expect an error but want to continue the test
384+ }
153385
154- // Call openMention and wait for it to complete
155- await openMention ( "/path/to/file" )
386+ // Verify openFile was called
387+ expect ( mockOpenFile ) . toHaveBeenCalled ( )
156388
157- // Verify error handling
389+ // Verify error was thrown
390+ expect ( errorThrown ) . toBe ( true )
391+
392+ // Verify no other method was called
158393 expect ( mockExecuteCommand ) . not . toHaveBeenCalled ( )
159394 expect ( mockOpenExternal ) . not . toHaveBeenCalled ( )
160- expect ( mockVscode . window . showErrorMessage ) . toHaveBeenCalledWith ( "Could not open file: File does not exist" )
161395
162396 // Reset mocks for next test
163397 jest . clearAllMocks ( )
@@ -183,5 +417,49 @@ Detailed commit message with multiple lines
183417 } ) ,
184418 )
185419 } )
420+
421+ it ( "should correctly handle file paths with escaped spaces" , async ( ) => {
422+ // Test path with escaped spaces
423+ const escapedPath = "/path/with\\ spaces/file\\ name\\ with\\ spaces.txt"
424+
425+ // Call openMention with path containing escaped spaces
426+ await openMention ( escapedPath )
427+
428+ // Get the path that was passed to openFile
429+ const openFilePath = mockOpenFile . mock . calls [ 0 ] [ 0 ]
430+
431+ // Construct expected unescaped path
432+ const expectedUnescapedPath = path . join ( mockCwd , "path" , "with spaces" , "file name with spaces.txt" )
433+
434+ // For debugging
435+ console . log ( "Actual path in openFile test:" , openFilePath )
436+ console . log ( "Expected path in openFile test:" , expectedUnescapedPath )
437+
438+ // Verify the path was correctly unescaped
439+ expect ( normalizePath ( openFilePath ) ) . toEqual ( normalizePath ( expectedUnescapedPath ) )
440+
441+ // Reset mocks
442+ jest . clearAllMocks ( )
443+
444+ // Test with a directory path
445+ const escapedDirPath = "/directory/with\\ spaces/"
446+
447+ // Call openMention with directory path containing escaped spaces
448+ await openMention ( escapedDirPath )
449+
450+ // Verify reveal in explorer was called with correct unescaped path
451+ // We need to check the Uri object passed to executeCommand
452+ const revealCallArgs = mockExecuteCommand . mock . calls . find ( ( call ) => call [ 0 ] === "revealInExplorer" )
453+ expect ( revealCallArgs ) . toBeDefined ( )
454+ const revealedUri = revealCallArgs [ 1 ]
455+
456+ // Construct expected path for Uri
457+ const expectedUriPath = path . join ( mockCwd , "directory" , "with spaces" )
458+
459+ // Check Uri properties
460+ expect ( revealedUri ) . toBeDefined ( )
461+ expect ( revealedUri . scheme ) . toEqual ( "file" )
462+ expect ( revealedUri . fsPath ) . toEqual ( expectedUriPath )
463+ } )
186464 } )
187465} )
0 commit comments