@@ -45,9 +45,11 @@ describe('pathUtils', () => {
4545 expect ( getTrailingPathLabel ( '/tmp/repo/work2' , 1 ) ) . toBe ( 'work2' ) ;
4646 } ) ;
4747
48- test ( 'bootstrapProjectsRoot falls back to legacy GitHub when the new projects root is empty ' , ( ) => {
48+ test ( 'bootstrapProjectsRoot falls back to legacy GitHub when repos use worktree layout ' , ( ) => {
4949 const tmpHome = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'path-utils-home-' ) ) ;
50- fs . mkdirSync ( path . join ( tmpHome , 'GitHub' , 'games' ) , { recursive : true } ) ;
50+ // Create repos with worktree layout (master/ with .git)
51+ fs . mkdirSync ( path . join ( tmpHome , 'GitHub' , 'repo-a' , 'master' , '.git' ) , { recursive : true } ) ;
52+ fs . mkdirSync ( path . join ( tmpHome , 'GitHub' , 'repo-b' , 'master' , '.git' ) , { recursive : true } ) ;
5153
5254 const {
5355 bootstrapProjectsRoot,
@@ -63,6 +65,26 @@ describe('pathUtils', () => {
6365 expect ( process . env . AGENT_WORKSPACE_PROJECTS_DIR ) . toBe ( getLegacyProjectsRoot ( ) ) ;
6466 } ) ;
6567
68+ test ( 'bootstrapProjectsRoot skips legacy GitHub when repos are all flat clones' , ( ) => {
69+ const tmpHome = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'path-utils-home-' ) ) ;
70+ // Flat clones — no master/ subdirectory, just .git at root
71+ fs . mkdirSync ( path . join ( tmpHome , 'GitHub' , 'repo-a' , '.git' ) , { recursive : true } ) ;
72+ fs . mkdirSync ( path . join ( tmpHome , 'GitHub' , 'repo-b' , '.git' ) , { recursive : true } ) ;
73+ fs . mkdirSync ( path . join ( tmpHome , 'GitHub' , 'repo-c' , '.git' ) , { recursive : true } ) ;
74+
75+ const {
76+ bootstrapProjectsRoot,
77+ getProjectsRoot,
78+ getLegacyProjectsRoot
79+ } = loadPathUtils ( tmpHome ) ;
80+
81+ const result = bootstrapProjectsRoot ( ) ;
82+
83+ expect ( result . usingLegacyProjectsRoot ) . toBe ( false ) ;
84+ expect ( result . legacySkipReason ) . toBe ( 'no-worktree-layout' ) ;
85+ expect ( getProjectsRoot ( ) ) . not . toBe ( getLegacyProjectsRoot ( ) ) ;
86+ } ) ;
87+
6688 test ( 'bootstrapProjectsRoot keeps the new projects root when it is already populated' , ( ) => {
6789 const tmpHome = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'path-utils-home-' ) ) ;
6890 fs . mkdirSync ( path . join ( tmpHome , 'GitHub' , 'games' ) , { recursive : true } ) ;
@@ -82,6 +104,48 @@ describe('pathUtils', () => {
82104 expect ( process . env . AGENT_WORKSPACE_PROJECTS_DIR ) . toBeUndefined ( ) ;
83105 } ) ;
84106
107+ test ( 'hasWorktreeLayout detects master/ with .git' , ( ) => {
108+ const tmpDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'wt-layout-' ) ) ;
109+ fs . mkdirSync ( path . join ( tmpDir , 'master' , '.git' ) , { recursive : true } ) ;
110+
111+ const { hasWorktreeLayout } = loadPathUtils ( ) ;
112+ expect ( hasWorktreeLayout ( tmpDir ) ) . toBe ( true ) ;
113+ } ) ;
114+
115+ test ( 'hasWorktreeLayout detects main/ with .git' , ( ) => {
116+ const tmpDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'wt-layout-' ) ) ;
117+ fs . mkdirSync ( path . join ( tmpDir , 'main' , '.git' ) , { recursive : true } ) ;
118+
119+ const { hasWorktreeLayout } = loadPathUtils ( ) ;
120+ expect ( hasWorktreeLayout ( tmpDir ) ) . toBe ( true ) ;
121+ } ) ;
122+
123+ test ( 'hasWorktreeLayout returns false for flat clones' , ( ) => {
124+ const tmpDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'wt-layout-' ) ) ;
125+ fs . mkdirSync ( path . join ( tmpDir , '.git' ) , { recursive : true } ) ;
126+ fs . mkdirSync ( path . join ( tmpDir , 'src' ) , { recursive : true } ) ;
127+
128+ const { hasWorktreeLayout } = loadPathUtils ( ) ;
129+ expect ( hasWorktreeLayout ( tmpDir ) ) . toBe ( false ) ;
130+ } ) ;
131+
132+ test ( 'countWorktreeLayoutRepos counts repos at multiple nesting depths' , ( ) => {
133+ const tmpDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'wt-count-' ) ) ;
134+ // Depth 1: worktree layout
135+ fs . mkdirSync ( path . join ( tmpDir , 'repo-a' , 'master' , '.git' ) , { recursive : true } ) ;
136+ // Depth 2 (nested category): worktree layout
137+ fs . mkdirSync ( path . join ( tmpDir , 'games' , 'zoo-game' , 'master' , '.git' ) , { recursive : true } ) ;
138+ // Depth 2: flat clone
139+ fs . mkdirSync ( path . join ( tmpDir , 'games' , 'flat-game' , '.git' ) , { recursive : true } ) ;
140+ // Depth 4 (deep nesting): worktree layout
141+ fs . mkdirSync ( path . join ( tmpDir , 'games' , 'hytopia' , 'games' , 'hyfire' , 'master' , '.git' ) , { recursive : true } ) ;
142+
143+ const { countWorktreeLayoutRepos } = loadPathUtils ( ) ;
144+ const result = countWorktreeLayoutRepos ( tmpDir ) ;
145+ expect ( result . total ) . toBe ( 4 ) ;
146+ expect ( result . worktree ) . toBe ( 3 ) ;
147+ } ) ;
148+
85149 test ( 'getAgentWorkspaceDir falls back to legacy data when legacy has more workspaces' , ( ) => {
86150 const tmpHome = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'path-utils-home-' ) ) ;
87151 fs . mkdirSync ( path . join ( tmpHome , '.agent-workspace' , 'workspaces' ) , { recursive : true } ) ;
0 commit comments