11import { describe , expect , it , mock } from 'bun:test'
22
3+ import path from 'path'
4+
35import { listDirectory } from '../tools/list-directory'
46
57import type { CodebuffFileSystem } from '@codebuff/common/types/filesystem'
68import type { Dirent , PathLike } from 'node:fs'
79
8- const PROJECT_ROOT = '/ workspace/ project'
10+ const PROJECT_ROOT = path . resolve ( ' workspace' , ' project')
911
1012function createFs ( realpaths : Record < string , string > ) {
1113 const readdir = mock ( async ( _path : PathLike ) => {
@@ -55,9 +57,10 @@ describe('listDirectory', () => {
5557 } )
5658
5759 it ( 'lists a directory inside the project and preserves the requested path' , async ( ) => {
60+ const childPath = path . join ( PROJECT_ROOT , 'src' )
5861 const { fs, readdir } = createFs ( {
5962 [ PROJECT_ROOT ] : PROJECT_ROOT ,
60- [ ` ${ PROJECT_ROOT } /src` ] : ` ${ PROJECT_ROOT } /src` ,
63+ [ childPath ] : childPath ,
6164 } )
6265
6366 const result = await listDirectory ( {
@@ -76,13 +79,13 @@ describe('listDirectory', () => {
7679 } ,
7780 } ,
7881 ] )
79- expect ( readdir ) . toHaveBeenCalledWith ( ` ${ PROJECT_ROOT } /src` , {
82+ expect ( readdir ) . toHaveBeenCalledWith ( childPath , {
8083 withFileTypes : true ,
8184 } )
8285 } )
8386
8487 it ( 'rejects sibling paths that only share the project prefix' , async ( ) => {
85- const siblingPath = '/workspace/ project-evil'
88+ const siblingPath = path . resolve ( PROJECT_ROOT , '..' , ' project-evil')
8689 const { fs, readdir } = createFs ( {
8790 [ PROJECT_ROOT ] : PROJECT_ROOT ,
8891 [ siblingPath ] : siblingPath ,
@@ -107,7 +110,7 @@ describe('listDirectory', () => {
107110 } )
108111
109112 it ( 'rejects the project parent directory' , async ( ) => {
110- const parentPath = '/workspace'
113+ const parentPath = path . dirname ( PROJECT_ROOT )
111114 const { fs, readdir } = createFs ( {
112115 [ PROJECT_ROOT ] : PROJECT_ROOT ,
113116 [ parentPath ] : parentPath ,
@@ -132,10 +135,11 @@ describe('listDirectory', () => {
132135 } )
133136
134137 it ( 'rejects directories that escape through a symlink' , async ( ) => {
135- const symlinkPath = `${ PROJECT_ROOT } /link`
138+ const symlinkPath = path . join ( PROJECT_ROOT , 'link' )
139+ const outsidePath = path . resolve ( PROJECT_ROOT , '..' , 'outside' )
136140 const { fs, readdir } = createFs ( {
137141 [ PROJECT_ROOT ] : PROJECT_ROOT ,
138- [ symlinkPath ] : '/outside' ,
142+ [ symlinkPath ] : outsidePath ,
139143 } )
140144
141145 const result = await listDirectory ( {
@@ -155,4 +159,33 @@ describe('listDirectory', () => {
155159 ] )
156160 expect ( readdir ) . not . toHaveBeenCalled ( )
157161 } )
162+
163+ it ( 'allows a symlink that resolves inside the project' , async ( ) => {
164+ const symlinkPath = path . join ( PROJECT_ROOT , 'link' )
165+ const realTarget = path . join ( PROJECT_ROOT , 'src' )
166+ const { fs, readdir } = createFs ( {
167+ [ PROJECT_ROOT ] : PROJECT_ROOT ,
168+ [ symlinkPath ] : realTarget ,
169+ } )
170+
171+ const result = await listDirectory ( {
172+ directoryPath : 'link' ,
173+ projectPath : PROJECT_ROOT ,
174+ fs,
175+ } )
176+
177+ expect ( result ) . toEqual ( [
178+ {
179+ type : 'json' ,
180+ value : {
181+ files : [ 'index.ts' ] ,
182+ directories : [ ] ,
183+ path : 'link' ,
184+ } ,
185+ } ,
186+ ] )
187+ expect ( readdir ) . toHaveBeenCalledWith ( realTarget , {
188+ withFileTypes : true ,
189+ } )
190+ } )
158191} )
0 commit comments