@@ -769,4 +769,149 @@ public async Task FullyIsolated_WorktreeIdSetOnAgentSessionInfo()
769769 }
770770
771771 #endregion
772+
773+ #region Worktree Failure Fallback (Windows long-path / git failure scenarios)
774+
775+ [ Fact ]
776+ public async Task GroupShared_WorktreeFailure_FallsBackToExistingWorktree ( )
777+ {
778+ // Simulate scenario: worktree creation fails (e.g., Windows long-path issue)
779+ // but an existing worktree for the repo exists — should use it instead of temp dir.
780+ var existingWt = new WorktreeInfo
781+ {
782+ Id = "existing-wt" ,
783+ RepoId = "repo-1" ,
784+ Branch = "main" ,
785+ Path = "/existing/worktree/path"
786+ } ;
787+ var rm = new FailingRepoManagerWithExistingWorktree (
788+ new ( ) { new ( ) { Id = "repo-1" , Name = "Repo" } } ,
789+ new ( ) { existingWt } ) ;
790+ var svc = CreateDemoService ( rm ) ;
791+ var preset = MakePreset ( 2 , WorktreeStrategy . GroupShared ) ;
792+
793+ var group = await svc . CreateGroupFromPresetAsync ( preset ,
794+ workingDirectory : null ,
795+ repoId : "repo-1" ) ;
796+
797+ Assert . NotNull ( group ) ;
798+ // Should have fallen back to the existing worktree
799+ Assert . Equal ( "existing-wt" , group ! . WorktreeId ) ;
800+
801+ // Sessions should use the existing worktree path, not a temp dir
802+ var organized = svc . GetOrganizedSessions ( ) ;
803+ var groupSessions = organized . FirstOrDefault ( g => g . Group . Id == group ! . Id ) . Sessions ;
804+ Assert . NotNull ( groupSessions ) ;
805+ Assert . All ( groupSessions , s =>
806+ {
807+ Assert . NotNull ( s . WorkingDirectory ) ;
808+ Assert . Equal ( "/existing/worktree/path" , s . WorkingDirectory ) ;
809+ } ) ;
810+ }
811+
812+ [ Fact ]
813+ public async Task GroupShared_WorktreeFailure_WithWorkingDirectory_UsesWorkingDirectory ( )
814+ {
815+ // When worktree creation fails but a workingDirectory was provided,
816+ // sessions should use the workingDirectory (not temp)
817+ var rm = new FailingRepoManager ( new ( ) { new ( ) { Id = "repo-1" , Name = "Repo" } } ) ;
818+ var svc = CreateDemoService ( rm ) ;
819+ var preset = MakePreset ( 2 , WorktreeStrategy . GroupShared ) ;
820+
821+ var group = await svc . CreateGroupFromPresetAsync ( preset ,
822+ workingDirectory : "/provided/fallback" ,
823+ repoId : "repo-1" ) ;
824+
825+ Assert . NotNull ( group ) ;
826+
827+ // orchWorkDir should still be /provided/fallback since worktree failed
828+ var organized = svc . GetOrganizedSessions ( ) ;
829+ var groupSessions = organized . FirstOrDefault ( g => g . Group . Id == group ! . Id ) . Sessions ;
830+ Assert . NotNull ( groupSessions ) ;
831+ Assert . All ( groupSessions , s =>
832+ {
833+ Assert . NotNull ( s . WorkingDirectory ) ;
834+ Assert . Equal ( "/provided/fallback" , s . WorkingDirectory ) ;
835+ } ) ;
836+ }
837+
838+ [ Fact ]
839+ public async Task FullyIsolated_WorktreeFailure_FallsBackToExistingWorktree ( )
840+ {
841+ var existingWt = new WorktreeInfo
842+ {
843+ Id = "existing-wt" ,
844+ RepoId = "repo-1" ,
845+ Branch = "main" ,
846+ Path = "/existing/worktree/path"
847+ } ;
848+ var rm = new FailingRepoManagerWithExistingWorktree (
849+ new ( ) { new ( ) { Id = "repo-1" , Name = "Repo" } } ,
850+ new ( ) { existingWt } ) ;
851+ var svc = CreateDemoService ( rm ) ;
852+ var preset = MakePreset ( 2 , WorktreeStrategy . FullyIsolated ) ;
853+
854+ var group = await svc . CreateGroupFromPresetAsync ( preset ,
855+ workingDirectory : null ,
856+ repoId : "repo-1" ) ;
857+
858+ Assert . NotNull ( group ) ;
859+ // Orchestrator should have fallen back to existing worktree
860+ Assert . Equal ( "existing-wt" , group ! . WorktreeId ) ;
861+
862+ // Sessions should still be created
863+ var members = svc . Organization . Sessions
864+ . Where ( s => s . GroupId == group ! . Id )
865+ . ToList ( ) ;
866+ Assert . Equal ( 3 , members . Count ) ; // 1 orch + 2 workers
867+ }
868+
869+ [ Fact ]
870+ public async Task GroupShared_BranchName_UsesSharedPrefix ( )
871+ {
872+ // GroupShared should create a worktree with "-shared-" in the branch name,
873+ // not "-orchestrator-" — this is the explicit handling fix.
874+ var rm = new FakeRepoManager ( new ( ) { new ( ) { Id = "repo-1" , Name = "Repo" } } ) ;
875+ var svc = CreateDemoService ( rm ) ;
876+ var preset = MakePreset ( 2 , WorktreeStrategy . GroupShared ) ;
877+
878+ await svc . CreateGroupFromPresetAsync ( preset ,
879+ workingDirectory : null ,
880+ repoId : "repo-1" ,
881+ nameOverride : "MyTeam" ) ;
882+
883+ Assert . Single ( rm . CreateCalls ) ;
884+ Assert . Contains ( "shared" , rm . CreateCalls [ 0 ] . BranchName ) ;
885+ Assert . DoesNotContain ( "orchestrator" , rm . CreateCalls [ 0 ] . BranchName ) ;
886+ }
887+
888+ /// <summary>
889+ /// A FailingRepoManager that also has existing worktrees in its state,
890+ /// so the fallback logic can find them.
891+ /// </summary>
892+ private class FailingRepoManagerWithExistingWorktree : RepoManager
893+ {
894+ public FailingRepoManagerWithExistingWorktree ( List < RepositoryInfo > repos , List < WorktreeInfo > worktrees )
895+ {
896+ var stateField = typeof ( RepoManager ) . GetField ( "_state" ,
897+ System . Reflection . BindingFlags . NonPublic | System . Reflection . BindingFlags . Instance ) ! ;
898+ var loadedField = typeof ( RepoManager ) . GetField ( "_loaded" ,
899+ System . Reflection . BindingFlags . NonPublic | System . Reflection . BindingFlags . Instance ) ! ;
900+ stateField . SetValue ( this , new RepositoryState { Repositories = repos , Worktrees = worktrees } ) ;
901+ loadedField . SetValue ( this , true ) ;
902+ }
903+
904+ public override Task < WorktreeInfo > CreateWorktreeAsync ( string repoId , string branchName ,
905+ string ? baseBranch = null , bool skipFetch = false , string ? localPath = null , CancellationToken ct = default )
906+ {
907+ throw new InvalidOperationException ( "Simulated Windows long-path failure" ) ;
908+ }
909+
910+ public override Task FetchAsync ( string repoId , CancellationToken ct = default )
911+ {
912+ return Task . CompletedTask ; // Fetch succeeds, only worktree creation fails
913+ }
914+ }
915+
916+ #endregion
772917}
0 commit comments