@@ -1812,55 +1812,67 @@ test('writes create missing parent directories inside a held workspace root', as
18121812 assert . equal ( ( await stat ( join ( root , 'src' , 'feature' , 'deep' ) ) ) . isDirectory ( ) , true ) ;
18131813} ) ;
18141814
1815- test ( 'parent creation never follows symlinks, crosses files, or leaves the workspace' , async ( t ) => {
1816- const parent = await realpath (
1817- await mkdtemp ( join ( tmpdir ( ) , 'librechat-code-workspace-parent-' ) ) ,
1818- ) ;
1819- t . after ( ( ) => rm ( parent , { recursive : true , force : true } ) ) ;
1820- const root = join ( parent , 'root' ) ;
1821- const outside = join ( parent , 'outside' ) ;
1822- await mkdir ( join ( root , 'src' ) , { recursive : true } ) ;
1823- await mkdir ( outside ) ;
1824- await writeFile ( join ( root , 'file.txt' ) , 'file' ) ;
1825- await symlink ( outside , join ( root , 'linked-outside' ) ) ;
1826- await symlink ( join ( root , 'src' ) , join ( root , 'alias' ) ) ;
1827- await symlink ( join ( parent , 'missing-target' ) , join ( root , 'dangling' ) ) ;
1828- const tools = await heldRootTools ( root ) ;
1815+ /** A root registered by path alone, as `--worker-dir`, `--workspace` and `--environment` do. */
1816+ async function pathnameRootTools ( root : string ) : Promise < LocalWorkspaceTools > {
1817+ return LocalWorkspaceTools . create ( {
1818+ workspaces : [ { id : 'primary' , root, writable : true } ] ,
1819+ } ) ;
1820+ }
18291821
1830- for ( const [ path , code ] of [
1831- [ 'linked-outside/new/notes.txt' , 'INVALID_PATH' ] ,
1832- [ 'alias/new/notes.txt' , 'INVALID_PATH' ] ,
1833- [ 'dangling/new/notes.txt' , 'INVALID_PATH' ] ,
1834- [ 'file.txt/new/notes.txt' , 'INVALID_PATH' ] ,
1835- [ '../outside/new/notes.txt' , 'INVALID_REQUEST' ] ,
1836- [ 'new/../../outside/notes.txt' , 'INVALID_REQUEST' ] ,
1837- ] as const ) {
1838- await assert . rejects (
1839- tools . execute ( {
1840- protocolVersion : 1 ,
1841- operation : 'write_file' ,
1842- workspaceId : 'primary' ,
1843- path,
1844- content : 'blocked' ,
1845- } ) ,
1846- ( error : unknown ) =>
1847- error instanceof WorkspaceToolError &&
1848- error . code === code &&
1849- ! error . mutationMayHaveCommitted ,
1850- path ,
1822+ for ( const [ kind , createTools ] of [
1823+ [ 'held' , heldRootTools ] ,
1824+ [ 'pathname-only' , pathnameRootTools ] ,
1825+ ] as const ) {
1826+ test ( `parent creation never follows symlinks, crosses files, or leaves a ${ kind } workspace` , async ( t ) => {
1827+ const parent = await realpath (
1828+ await mkdtemp ( join ( tmpdir ( ) , 'librechat-code-workspace-parent-' ) ) ,
18511829 ) ;
1852- }
1853- assert . deepEqual ( await readdir ( outside ) , [ ] ) ;
1854- assert . deepEqual ( await readdir ( join ( root , 'src' ) ) , [ ] ) ;
1855- assert . deepEqual ( ( await readdir ( root ) ) . sort ( ) , [
1856- 'alias' ,
1857- 'dangling' ,
1858- 'file.txt' ,
1859- 'linked-outside' ,
1860- 'src' ,
1861- ] ) ;
1862- await assert . rejects ( stat ( join ( parent , 'missing-target' ) ) ) ;
1863- } ) ;
1830+ t . after ( ( ) => rm ( parent , { recursive : true , force : true } ) ) ;
1831+ const root = join ( parent , 'root' ) ;
1832+ const outside = join ( parent , 'outside' ) ;
1833+ await mkdir ( join ( root , 'src' ) , { recursive : true } ) ;
1834+ await mkdir ( outside ) ;
1835+ await writeFile ( join ( root , 'file.txt' ) , 'file' ) ;
1836+ await symlink ( outside , join ( root , 'linked-outside' ) ) ;
1837+ await symlink ( join ( root , 'src' ) , join ( root , 'alias' ) ) ;
1838+ await symlink ( join ( parent , 'missing-target' ) , join ( root , 'dangling' ) ) ;
1839+ const tools = await createTools ( root ) ;
1840+
1841+ for ( const [ path , code ] of [
1842+ [ 'linked-outside/new/notes.txt' , 'INVALID_PATH' ] ,
1843+ [ 'alias/new/notes.txt' , 'INVALID_PATH' ] ,
1844+ [ 'dangling/new/notes.txt' , 'INVALID_PATH' ] ,
1845+ [ 'file.txt/new/notes.txt' , 'INVALID_PATH' ] ,
1846+ [ '../outside/new/notes.txt' , 'INVALID_REQUEST' ] ,
1847+ [ 'new/../../outside/notes.txt' , 'INVALID_REQUEST' ] ,
1848+ ] as const ) {
1849+ await assert . rejects (
1850+ tools . execute ( {
1851+ protocolVersion : 1 ,
1852+ operation : 'write_file' ,
1853+ workspaceId : 'primary' ,
1854+ path,
1855+ content : 'blocked' ,
1856+ } ) ,
1857+ ( error : unknown ) =>
1858+ error instanceof WorkspaceToolError &&
1859+ error . code === code &&
1860+ ! error . mutationMayHaveCommitted ,
1861+ path ,
1862+ ) ;
1863+ }
1864+ assert . deepEqual ( await readdir ( outside ) , [ ] ) ;
1865+ assert . deepEqual ( await readdir ( join ( root , 'src' ) ) , [ ] ) ;
1866+ assert . deepEqual ( ( await readdir ( root ) ) . sort ( ) , [
1867+ 'alias' ,
1868+ 'dangling' ,
1869+ 'file.txt' ,
1870+ 'linked-outside' ,
1871+ 'src' ,
1872+ ] ) ;
1873+ await assert . rejects ( stat ( join ( parent , 'missing-target' ) ) ) ;
1874+ } ) ;
1875+ }
18641876
18651877test ( 'a write finishes once it has created parents, even if cancelled' , async ( t ) => {
18661878 const root = await realpath ( await mkdtemp ( join ( tmpdir ( ) , 'librechat-code-workspace-' ) ) ) ;
@@ -1914,12 +1926,65 @@ test('a write cancelled before creating parents leaves the workspace unchanged',
19141926 assert . deepEqual ( await readdir ( root ) , [ ] ) ;
19151927} ) ;
19161928
1917- test ( 'a pathname-only workspace root reports a missing parent instead of creating it ' , async ( t ) => {
1918- const root = await mkdtemp ( join ( tmpdir ( ) , 'librechat-code-workspace-' ) ) ;
1929+ test ( 'a pathname-only workspace root creates missing parents like a held root ' , async ( t ) => {
1930+ const root = await realpath ( await mkdtemp ( join ( tmpdir ( ) , 'librechat-code-workspace-' ) ) ) ;
19191931 t . after ( ( ) => rm ( root , { recursive : true , force : true } ) ) ;
1920- const tools = await LocalWorkspaceTools . create ( {
1921- workspaces : [ { id : 'primary' , root, writable : true } ] ,
1922- } ) ;
1932+ const tools = await pathnameRootTools ( root ) ;
1933+
1934+ for ( const [ path , overwrite ] of [
1935+ [ '.librechat/attachments/.gitignore' , false ] ,
1936+ [ 'missing/deep/notes.txt' , undefined ] ,
1937+ ] as const ) {
1938+ const result = await tools . execute ( {
1939+ protocolVersion : 1 ,
1940+ operation : 'write_file' ,
1941+ workspaceId : 'primary' ,
1942+ path,
1943+ content : '*\n' ,
1944+ ...( overwrite === undefined ? { } : { overwrite } ) ,
1945+ } ) ;
1946+ assert . deepEqual ( result , {
1947+ protocolVersion : 1 ,
1948+ operation : 'write_file' ,
1949+ workspaceId : 'primary' ,
1950+ path,
1951+ created : true ,
1952+ bytesWritten : 2 ,
1953+ } ) ;
1954+ assert . equal ( await readFile ( join ( root , ...path . split ( '/' ) ) , 'utf8' ) , '*\n' ) ;
1955+ }
1956+ assert . equal ( ( await stat ( join ( root , '.librechat' , 'attachments' ) ) ) . isDirectory ( ) , true ) ;
1957+
1958+ await assert . rejects (
1959+ tools . execute ( {
1960+ protocolVersion : 1 ,
1961+ operation : 'write_file' ,
1962+ workspaceId : 'primary' ,
1963+ path : '.librechat/attachments/.gitignore' ,
1964+ content : '!keep\n' ,
1965+ overwrite : false ,
1966+ } ) ,
1967+ ( error : unknown ) =>
1968+ error instanceof WorkspaceToolError &&
1969+ error . code === 'EDIT_CONFLICT' &&
1970+ ! error . mutationMayHaveCommitted ,
1971+ 'a create never replaces a file in a folder it just created' ,
1972+ ) ;
1973+ assert . equal ( await readFile ( join ( root , '.librechat' , 'attachments' , '.gitignore' ) , 'utf8' ) , '*\n' ) ;
1974+ } ) ;
1975+
1976+ test ( 'a pathname-only root that has been replaced by a symlink creates nothing' , async ( t ) => {
1977+ const parent = await realpath (
1978+ await mkdtemp ( join ( tmpdir ( ) , 'librechat-code-workspace-parent-' ) ) ,
1979+ ) ;
1980+ t . after ( ( ) => rm ( parent , { recursive : true , force : true } ) ) ;
1981+ const root = join ( parent , 'root' ) ;
1982+ const elsewhere = join ( parent , 'elsewhere' ) ;
1983+ await mkdir ( root ) ;
1984+ await mkdir ( elsewhere ) ;
1985+ const tools = await pathnameRootTools ( root ) ;
1986+ await rm ( root , { recursive : true } ) ;
1987+ await symlink ( elsewhere , root ) ;
19231988
19241989 await assert . rejects (
19251990 tools . execute ( {
@@ -1931,10 +1996,10 @@ test('a pathname-only workspace root reports a missing parent instead of creatin
19311996 } ) ,
19321997 ( error : unknown ) =>
19331998 error instanceof WorkspaceToolError &&
1934- error . code === 'NOT_FOUND ' &&
1935- error . message === 'Workspace path does not exist' ,
1999+ error . code === 'INVALID_PATH ' &&
2000+ ! error . mutationMayHaveCommitted ,
19362001 ) ;
1937- assert . deepEqual ( await readdir ( root ) , [ ] ) ;
2002+ assert . deepEqual ( await readdir ( elsewhere ) , [ ] ) ;
19382003} ) ;
19392004
19402005test ( 'a missing workspace path is NOT_FOUND only when reached through the workspace' , async ( t ) => {
0 commit comments