@@ -90,7 +90,7 @@ describe("WorktreeRuntime.resolvePath", () => {
9090} ) ;
9191
9292describe ( "WorktreeRuntime.deleteWorkspace" , ( ) => {
93- it ( "deletes agent branches when removing worktrees" , async ( ) => {
93+ it ( "deletes non- agent branches when removing worktrees (force) " , async ( ) => {
9494 const rootDir = await fsPromises . realpath (
9595 await fsPromises . mkdtemp ( path . join ( os . tmpdir ( ) , "worktree-runtime-delete-" ) )
9696 ) ;
@@ -106,7 +106,7 @@ describe("WorktreeRuntime.deleteWorkspace", () => {
106106 const runtime = new WorktreeRuntime ( srcBaseDir ) ;
107107 const initLogger = createNullInitLogger ( ) ;
108108
109- const branchName = "agent_explore_aaaaaaaaaa " ;
109+ const branchName = "feature_aaaaaaaaaa " ;
110110 const createResult = await runtime . createWorkspace ( {
111111 projectPath,
112112 branchName,
@@ -116,16 +116,80 @@ describe("WorktreeRuntime.deleteWorkspace", () => {
116116 } ) ;
117117 expect ( createResult . success ) . toBe ( true ) ;
118118 if ( ! createResult . success ) return ;
119+ if ( ! createResult . workspacePath ) {
120+ throw new Error ( "Expected workspacePath from createWorkspace" ) ;
121+ }
122+ const workspacePath = createResult . workspacePath ;
119123
120- const before = execSync ( `git branch --list "${ branchName } "` , {
124+ // Make the branch unmerged (so -d would fail); force delete should still delete it.
125+ execSync ( "bash -lc 'echo \"change\" >> README.md'" , {
126+ cwd : workspacePath ,
127+ stdio : "ignore" ,
128+ } ) ;
129+ execSync ( "git add README.md" , { cwd : workspacePath , stdio : "ignore" } ) ;
130+ execSync ( 'git commit -m "change"' , { cwd : workspacePath , stdio : "ignore" } ) ;
131+
132+ const deleteResult = await runtime . deleteWorkspace ( projectPath , branchName , true ) ;
133+ expect ( deleteResult . success ) . toBe ( true ) ;
134+
135+ const after = execSync ( `git branch --list "${ branchName } "` , {
121136 cwd : projectPath ,
122137 stdio : [ "ignore" , "pipe" , "ignore" ] ,
123138 } )
124139 . toString ( )
125140 . trim ( ) ;
126- expect ( before ) . toContain ( branchName ) ;
141+ expect ( after ) . toBe ( "" ) ;
142+ } finally {
143+ await fsPromises . rm ( rootDir , { recursive : true , force : true } ) ;
144+ }
145+ } , 20_000 ) ;
127146
128- const deleteResult = await runtime . deleteWorkspace ( projectPath , branchName , true ) ;
147+ it ( "deletes merged branches when removing worktrees (safe delete)" , async ( ) => {
148+ const rootDir = await fsPromises . realpath (
149+ await fsPromises . mkdtemp ( path . join ( os . tmpdir ( ) , "worktree-runtime-delete-" ) )
150+ ) ;
151+
152+ try {
153+ const projectPath = path . join ( rootDir , "repo" ) ;
154+ await fsPromises . mkdir ( projectPath , { recursive : true } ) ;
155+ initGitRepo ( projectPath ) ;
156+
157+ const srcBaseDir = path . join ( rootDir , "src" ) ;
158+ await fsPromises . mkdir ( srcBaseDir , { recursive : true } ) ;
159+
160+ const runtime = new WorktreeRuntime ( srcBaseDir ) ;
161+ const initLogger = createNullInitLogger ( ) ;
162+
163+ const branchName = "feature_merge_aaaaaaaaaa" ;
164+ const createResult = await runtime . createWorkspace ( {
165+ projectPath,
166+ branchName,
167+ trunkBranch : "main" ,
168+ directoryName : branchName ,
169+ initLogger,
170+ } ) ;
171+ expect ( createResult . success ) . toBe ( true ) ;
172+ if ( ! createResult . success ) return ;
173+ if ( ! createResult . workspacePath ) {
174+ throw new Error ( "Expected workspacePath from createWorkspace" ) ;
175+ }
176+ const workspacePath = createResult . workspacePath ;
177+
178+ // Commit on the workspace branch.
179+ execSync ( "bash -lc 'echo \"merged-change\" >> README.md'" , {
180+ cwd : workspacePath ,
181+ stdio : "ignore" ,
182+ } ) ;
183+ execSync ( "git add README.md" , { cwd : workspacePath , stdio : "ignore" } ) ;
184+ execSync ( 'git commit -m "merged-change"' , {
185+ cwd : workspacePath ,
186+ stdio : "ignore" ,
187+ } ) ;
188+
189+ // Merge into main so `git branch -d` succeeds.
190+ execSync ( `git merge "${ branchName } "` , { cwd : projectPath , stdio : "ignore" } ) ;
191+
192+ const deleteResult = await runtime . deleteWorkspace ( projectPath , branchName , false ) ;
129193 expect ( deleteResult . success ) . toBe ( true ) ;
130194
131195 const after = execSync ( `git branch --list "${ branchName } "` , {
@@ -139,4 +203,63 @@ describe("WorktreeRuntime.deleteWorkspace", () => {
139203 await fsPromises . rm ( rootDir , { recursive : true , force : true } ) ;
140204 }
141205 } , 20_000 ) ;
206+
207+ it ( "does not delete protected branches" , async ( ) => {
208+ const rootDir = await fsPromises . realpath (
209+ await fsPromises . mkdtemp ( path . join ( os . tmpdir ( ) , "worktree-runtime-delete-" ) )
210+ ) ;
211+
212+ try {
213+ const projectPath = path . join ( rootDir , "repo" ) ;
214+ await fsPromises . mkdir ( projectPath , { recursive : true } ) ;
215+ initGitRepo ( projectPath ) ;
216+
217+ // Move the main worktree off main so we can add a separate worktree on main.
218+ execSync ( "git checkout -b other" , { cwd : projectPath , stdio : "ignore" } ) ;
219+
220+ const srcBaseDir = path . join ( rootDir , "src" ) ;
221+ await fsPromises . mkdir ( srcBaseDir , { recursive : true } ) ;
222+
223+ const runtime = new WorktreeRuntime ( srcBaseDir ) ;
224+ const initLogger = createNullInitLogger ( ) ;
225+
226+ const branchName = "main" ;
227+ const createResult = await runtime . createWorkspace ( {
228+ projectPath,
229+ branchName,
230+ trunkBranch : "main" ,
231+ directoryName : branchName ,
232+ initLogger,
233+ } ) ;
234+ expect ( createResult . success ) . toBe ( true ) ;
235+ if ( ! createResult . success ) return ;
236+ if ( ! createResult . workspacePath ) {
237+ throw new Error ( "Expected workspacePath from createWorkspace" ) ;
238+ }
239+ const workspacePath = createResult . workspacePath ;
240+
241+ const deleteResult = await runtime . deleteWorkspace ( projectPath , branchName , true ) ;
242+ expect ( deleteResult . success ) . toBe ( true ) ;
243+
244+ // The worktree directory should be removed.
245+ let worktreeExists = true ;
246+ try {
247+ await fsPromises . access ( workspacePath ) ;
248+ } catch {
249+ worktreeExists = false ;
250+ }
251+ expect ( worktreeExists ) . toBe ( false ) ;
252+
253+ // But protected branches (like main) should never be deleted.
254+ const after = execSync ( `git branch --list "${ branchName } "` , {
255+ cwd : projectPath ,
256+ stdio : [ "ignore" , "pipe" , "ignore" ] ,
257+ } )
258+ . toString ( )
259+ . trim ( ) ;
260+ expect ( after ) . toBe ( "main" ) ;
261+ } finally {
262+ await fsPromises . rm ( rootDir , { recursive : true , force : true } ) ;
263+ }
264+ } , 20_000 ) ;
142265} ) ;
0 commit comments