1- import {
2- existsSync ,
3- mkdtempSync ,
4- readFileSync ,
5- symlinkSync ,
6- writeFileSync ,
7- } from 'node:fs' ;
1+ import { existsSync , mkdtempSync , readFileSync , symlinkSync , writeFileSync } from 'node:fs' ;
82import { tmpdir } from 'node:os' ;
93import path from 'node:path' ;
104import { describe , expect , it , vi } from 'vitest' ;
@@ -25,10 +19,10 @@ import { AGENTS_MD_CODEX_BUDGET_BYTES, createAgentCommand, runInstall, runList }
2519
2620/** Windows requires Developer Mode or elevation to create symlinks. */
2721function canCreateSymlinks ( ) : boolean {
22+ const probeRoot = mkdtempSync ( path . join ( tmpdir ( ) , 'agent-symlink-probe-' ) ) ;
23+ const target = path . join ( probeRoot , 'target.txt' ) ;
24+ writeFileSync ( target , 'probe' ) ;
2825 try {
29- const probeRoot = mkdtempSync ( path . join ( tmpdir ( ) , 'agent-symlink-probe-' ) ) ;
30- const target = path . join ( probeRoot , 'target.txt' ) ;
31- writeFileSync ( target , 'probe' ) ;
3226 symlinkSync ( target , path . join ( probeRoot , 'link.txt' ) , 'file' ) ;
3327 return true ;
3428 } catch {
@@ -1109,76 +1103,76 @@ describe('runInstall — default AgentFs (real disk)', () => {
11091103 it . skipIf ( ! symlinkCapable ) (
11101104 'refuses to write through a symlinked parent dir (real disk) — exit 5' ,
11111105 async ( ) => {
1112- const tmpRoot = mkdtempSync ( path . join ( tmpdir ( ) , 'agent-test-symlink-parent-' ) ) ;
1113- const outside = mkdtempSync ( path . join ( tmpdir ( ) , 'agent-test-outside-' ) ) ;
1114- // `.claude` is a real symlink to a directory outside the project root.
1115- symlinkSync ( outside , path . join ( tmpRoot , '.claude' ) , 'dir' ) ;
1116- const { deps } = makeCapture ( ) ;
1117-
1118- let thrown : unknown ;
1119- try {
1120- await runInstall (
1121- {
1122- profile : 'default' ,
1123- output : 'text' ,
1124- debug : false ,
1125- dryRun : false ,
1126- target : [ 'claude' ] ,
1127- skills : [ 'testsprite-verify' ] ,
1128- force : false ,
1129- dir : tmpRoot ,
1130- } ,
1131- { ...deps } ,
1132- ) ;
1133- } catch ( err ) {
1134- thrown = err ;
1135- }
1106+ const tmpRoot = mkdtempSync ( path . join ( tmpdir ( ) , 'agent-test-symlink-parent-' ) ) ;
1107+ const outside = mkdtempSync ( path . join ( tmpdir ( ) , 'agent-test-outside-' ) ) ;
1108+ // `.claude` is a real symlink to a directory outside the project root.
1109+ symlinkSync ( outside , path . join ( tmpRoot , '.claude' ) , 'dir' ) ;
1110+ const { deps } = makeCapture ( ) ;
1111+
1112+ let thrown : unknown ;
1113+ try {
1114+ await runInstall (
1115+ {
1116+ profile : 'default' ,
1117+ output : 'text' ,
1118+ debug : false ,
1119+ dryRun : false ,
1120+ target : [ 'claude' ] ,
1121+ skills : [ 'testsprite-verify' ] ,
1122+ force : false ,
1123+ dir : tmpRoot ,
1124+ } ,
1125+ { ...deps } ,
1126+ ) ;
1127+ } catch ( err ) {
1128+ thrown = err ;
1129+ }
11361130
1137- expect ( thrown ) . toBeInstanceOf ( CLIError ) ;
1138- expect ( ( thrown as CLIError ) . exitCode ) . toBe ( 5 ) ;
1139- // Nothing was created through the symlink, outside --dir.
1140- expect ( existsSync ( path . join ( outside , 'skills' ) ) ) . toBe ( false ) ;
1131+ expect ( thrown ) . toBeInstanceOf ( CLIError ) ;
1132+ expect ( ( thrown as CLIError ) . exitCode ) . toBe ( 5 ) ;
1133+ // Nothing was created through the symlink, outside --dir.
1134+ expect ( existsSync ( path . join ( outside , 'skills' ) ) ) . toBe ( false ) ;
11411135 } ,
11421136 ) ;
11431137
11441138 it . skipIf ( ! symlinkCapable ) (
11451139 'refuses to overwrite a symlinked target file (real disk) with --force — exit 5' ,
11461140 async ( ) => {
1147- const tmpRoot = mkdtempSync ( path . join ( tmpdir ( ) , 'agent-test-symlink-target-' ) ) ;
1148- const outsideDir = mkdtempSync ( path . join ( tmpdir ( ) , 'agent-test-outside-target-' ) ) ;
1149- const { path : relPath } = renderForTarget ( 'claude' , 'testsprite-verify' ) ;
1150- const abs = path . resolve ( tmpRoot , relPath ) ;
1151- const nodeFs = await import ( 'node:fs/promises' ) ;
1152- await nodeFs . mkdir ( path . dirname ( abs ) , { recursive : true } ) ;
1153- // SKILL.md is a real symlink to a file outside the project root.
1154- const outsideFile = path . join ( outsideDir , 'secret.txt' ) ;
1155- await nodeFs . writeFile ( outsideFile , 'SECRET' , 'utf8' ) ;
1156- symlinkSync ( outsideFile , abs , 'file' ) ;
1157- const { deps } = makeCapture ( ) ;
1158-
1159- let thrown : unknown ;
1160- try {
1161- await runInstall (
1162- {
1163- profile : 'default' ,
1164- output : 'text' ,
1165- debug : false ,
1166- dryRun : false ,
1167- target : [ 'claude' ] ,
1168- skills : [ 'testsprite-verify' ] ,
1169- force : true ,
1170- dir : tmpRoot ,
1171- } ,
1172- { ...deps } ,
1173- ) ;
1174- } catch ( err ) {
1175- thrown = err ;
1176- }
1141+ const tmpRoot = mkdtempSync ( path . join ( tmpdir ( ) , 'agent-test-symlink-target-' ) ) ;
1142+ const outsideDir = mkdtempSync ( path . join ( tmpdir ( ) , 'agent-test-outside-target-' ) ) ;
1143+ const { path : relPath } = renderForTarget ( 'claude' , 'testsprite-verify' ) ;
1144+ const abs = path . resolve ( tmpRoot , relPath ) ;
1145+ const nodeFs = await import ( 'node:fs/promises' ) ;
1146+ await nodeFs . mkdir ( path . dirname ( abs ) , { recursive : true } ) ;
1147+ // SKILL.md is a real symlink to a file outside the project root.
1148+ const outsideFile = path . join ( outsideDir , 'secret.txt' ) ;
1149+ await nodeFs . writeFile ( outsideFile , 'SECRET' , 'utf8' ) ;
1150+ symlinkSync ( outsideFile , abs , 'file' ) ;
1151+ const { deps } = makeCapture ( ) ;
1152+
1153+ let thrown : unknown ;
1154+ try {
1155+ await runInstall (
1156+ {
1157+ profile : 'default' ,
1158+ output : 'text' ,
1159+ debug : false ,
1160+ dryRun : false ,
1161+ target : [ 'claude' ] ,
1162+ skills : [ 'testsprite-verify' ] ,
1163+ force : true ,
1164+ dir : tmpRoot ,
1165+ } ,
1166+ { ...deps } ,
1167+ ) ;
1168+ } catch ( err ) {
1169+ thrown = err ;
1170+ }
11771171
1178- expect ( thrown ) . toBeInstanceOf ( CLIError ) ;
1179- expect ( ( thrown as CLIError ) . exitCode ) . toBe ( 5 ) ;
1180- // The outside file was NOT overwritten (nor clobbered via the .bak path).
1181- expect ( readFileSync ( outsideFile , 'utf8' ) ) . toBe ( 'SECRET' ) ;
1172+ expect ( thrown ) . toBeInstanceOf ( CLIError ) ;
1173+ expect ( ( thrown as CLIError ) . exitCode ) . toBe ( 5 ) ;
1174+ // The outside file was NOT overwritten (nor clobbered via the .bak path).
1175+ expect ( readFileSync ( outsideFile , 'utf8' ) ) . toBe ( 'SECRET' ) ;
11821176 } ,
11831177 ) ;
11841178} ) ;
0 commit comments