@@ -1286,3 +1286,171 @@ func TestSync_Prune_DeleteFailureContinues(t *testing.T) {
12861286 assert .Contains (t , output , "Pruned b2 (merged)" )
12871287 assert .Contains (t , output , "Pruned 1 merged branch" )
12881288}
1289+
1290+ // TestSync_InteractivePrune_PromptsAndPrunes verifies that when running in an
1291+ // interactive terminal without --prune, the user is prompted and merged branches
1292+ // are pruned when they confirm.
1293+ func TestSync_InteractivePrune_PromptsAndPrunes (t * testing.T ) {
1294+ s := stack.Stack {
1295+ Trunk : stack.BranchRef {Branch : "main" },
1296+ Branches : []stack.BranchRef {
1297+ {Branch : "b1" , PullRequest : & stack.PullRequestRef {Number : 1 , Merged : true }},
1298+ {Branch : "b2" },
1299+ },
1300+ }
1301+
1302+ tmpDir := t .TempDir ()
1303+ writeStackFile (t , tmpDir , s )
1304+
1305+ var deletedBranches []string
1306+ var promptShown string
1307+
1308+ mock := newSyncMock (tmpDir , "b2" )
1309+ mock .BranchExistsFn = func (name string ) bool { return true }
1310+ mock .DeleteBranchFn = func (name string , force bool ) error {
1311+ deletedBranches = append (deletedBranches , name )
1312+ return nil
1313+ }
1314+
1315+ restore := git .SetOps (mock )
1316+ defer restore ()
1317+
1318+ cfg , _ , errR := config .NewTestConfig ()
1319+ cfg .ForceInteractive = true
1320+ cfg .ConfirmFn = func (prompt string , defaultValue bool ) (bool , error ) {
1321+ promptShown = prompt
1322+ assert .True (t , defaultValue , "default should be yes" )
1323+ return true , nil // user confirms
1324+ }
1325+
1326+ cmd := SyncCmd (cfg )
1327+ // No --prune flag
1328+ cmd .SetOut (io .Discard )
1329+ cmd .SetErr (io .Discard )
1330+ err := cmd .Execute ()
1331+
1332+ cfg .Err .Close ()
1333+ errOut , _ := io .ReadAll (errR )
1334+ output := string (errOut )
1335+
1336+ assert .NoError (t , err )
1337+ assert .Contains (t , promptShown , "Prune 1 merged branch" )
1338+ assert .Equal (t , []string {"b1" }, deletedBranches )
1339+ assert .Contains (t , output , "Pruned b1 (merged)" )
1340+ }
1341+
1342+ // TestSync_InteractivePrune_UserDeclines verifies that when the user declines
1343+ // the prune prompt, no branches are deleted.
1344+ func TestSync_InteractivePrune_UserDeclines (t * testing.T ) {
1345+ s := stack.Stack {
1346+ Trunk : stack.BranchRef {Branch : "main" },
1347+ Branches : []stack.BranchRef {
1348+ {Branch : "b1" , PullRequest : & stack.PullRequestRef {Number : 1 , Merged : true }},
1349+ {Branch : "b2" },
1350+ },
1351+ }
1352+
1353+ tmpDir := t .TempDir ()
1354+ writeStackFile (t , tmpDir , s )
1355+
1356+ mock := newSyncMock (tmpDir , "b2" )
1357+ mock .BranchExistsFn = func (name string ) bool { return true }
1358+ mock .DeleteBranchFn = func (string , bool ) error {
1359+ t .Fatal ("DeleteBranch should not be called when user declines" )
1360+ return nil
1361+ }
1362+
1363+ restore := git .SetOps (mock )
1364+ defer restore ()
1365+
1366+ cfg , _ , _ := config .NewTestConfig ()
1367+ cfg .ForceInteractive = true
1368+ cfg .ConfirmFn = func (string , bool ) (bool , error ) {
1369+ return false , nil // user declines
1370+ }
1371+
1372+ cmd := SyncCmd (cfg )
1373+ cmd .SetOut (io .Discard )
1374+ cmd .SetErr (io .Discard )
1375+ err := cmd .Execute ()
1376+
1377+ assert .NoError (t , err )
1378+ }
1379+
1380+ // TestSync_NonInteractive_NoPrunePrompt verifies that when the terminal is not
1381+ // interactive and --prune is not set, no prompt is shown and no branches are deleted.
1382+ func TestSync_NonInteractive_NoPrunePrompt (t * testing.T ) {
1383+ s := stack.Stack {
1384+ Trunk : stack.BranchRef {Branch : "main" },
1385+ Branches : []stack.BranchRef {
1386+ {Branch : "b1" , PullRequest : & stack.PullRequestRef {Number : 1 , Merged : true }},
1387+ {Branch : "b2" },
1388+ },
1389+ }
1390+
1391+ tmpDir := t .TempDir ()
1392+ writeStackFile (t , tmpDir , s )
1393+
1394+ mock := newSyncMock (tmpDir , "b2" )
1395+ mock .BranchExistsFn = func (name string ) bool { return true }
1396+ mock .DeleteBranchFn = func (string , bool ) error {
1397+ t .Fatal ("DeleteBranch should not be called in non-interactive mode without --prune" )
1398+ return nil
1399+ }
1400+
1401+ restore := git .SetOps (mock )
1402+ defer restore ()
1403+
1404+ cfg , _ , _ := config .NewTestConfig ()
1405+ // ForceInteractive is false by default — simulates non-interactive/CI/agent
1406+
1407+ cmd := SyncCmd (cfg )
1408+ cmd .SetOut (io .Discard )
1409+ cmd .SetErr (io .Discard )
1410+ err := cmd .Execute ()
1411+
1412+ assert .NoError (t , err )
1413+ }
1414+
1415+ // TestSync_ExplicitPrune_SkipsPrompt verifies that --prune flag bypasses the
1416+ // interactive prompt and prunes directly.
1417+ func TestSync_ExplicitPrune_SkipsPrompt (t * testing.T ) {
1418+ s := stack.Stack {
1419+ Trunk : stack.BranchRef {Branch : "main" },
1420+ Branches : []stack.BranchRef {
1421+ {Branch : "b1" , PullRequest : & stack.PullRequestRef {Number : 1 , Merged : true }},
1422+ {Branch : "b2" },
1423+ },
1424+ }
1425+
1426+ tmpDir := t .TempDir ()
1427+ writeStackFile (t , tmpDir , s )
1428+
1429+ var deletedBranches []string
1430+
1431+ mock := newSyncMock (tmpDir , "b2" )
1432+ mock .BranchExistsFn = func (name string ) bool { return true }
1433+ mock .DeleteBranchFn = func (name string , force bool ) error {
1434+ deletedBranches = append (deletedBranches , name )
1435+ return nil
1436+ }
1437+
1438+ restore := git .SetOps (mock )
1439+ defer restore ()
1440+
1441+ cfg , _ , _ := config .NewTestConfig ()
1442+ cfg .ForceInteractive = true
1443+ cfg .ConfirmFn = func (string , bool ) (bool , error ) {
1444+ t .Fatal ("ConfirmFn should not be called when --prune is explicit" )
1445+ return false , nil
1446+ }
1447+
1448+ cmd := SyncCmd (cfg )
1449+ cmd .SetArgs ([]string {"--prune" })
1450+ cmd .SetOut (io .Discard )
1451+ cmd .SetErr (io .Discard )
1452+ err := cmd .Execute ()
1453+
1454+ assert .NoError (t , err )
1455+ assert .Equal (t , []string {"b1" }, deletedBranches )
1456+ }
0 commit comments