@@ -40,9 +40,11 @@ function is_wp_error( mixed $thing ): bool {
4040
4141require_once dirname (__DIR__ ) . '/inc/Support/CommandSpec.php ' ;
4242require_once dirname (__DIR__ ) . '/inc/Support/RuntimeCapabilities.php ' ;
43+ require_once dirname (__DIR__ ) . '/inc/Support/PathSecurity.php ' ;
4344require_once dirname (__DIR__ ) . '/inc/Support/ProcessRunner.php ' ;
4445require_once dirname (__DIR__ ) . '/inc/Support/GitRunner.php ' ;
4546require_once dirname (__DIR__ ) . '/inc/Workspace/WorkspaceHandle.php ' ;
47+ require_once dirname (__DIR__ ) . '/inc/Workspace/WorktreeContextInjector.php ' ;
4648require_once dirname (__DIR__ ) . '/inc/Workspace/WorkspaceCoreUtilities.php ' ;
4749require_once dirname (__DIR__ ) . '/inc/Workspace/WorkspaceActiveNoSignalCleanup.php ' ;
4850require_once dirname (__DIR__ ) . '/inc/Workspace/WorkspaceWorktreeCleanupEngine.php ' ;
@@ -137,4 +139,101 @@ protected function resolve_remote_default_ref( string $primary_path, int $timeou
137139worktree_cleanup_patch_equivalence_assert_same (0 , $ evidence ['git_cherry ' ]['unmatched ' ], 'no unmatched commits are promoted ' );
138140worktree_cleanup_patch_equivalence_assert_same ('preserve_local_branch ' , $ evidence ['local_branch_handling ' ], 'cleanup preserves non-contained local branch ' );
139141
142+ $ suggest_action = new ReflectionMethod ($ cleanup , 'suggest_active_no_signal_action ' );
143+ $ merged_action = $ suggest_action ->invoke (
144+ $ cleanup ,
145+ array (
146+ 'dirty ' => 0 ,
147+ 'unpushed ' => 0 ,
148+ 'commits_outside_default ' => 0 ,
149+ 'remote_tracking ' => true ,
150+ 'upstream_equivalence ' => array (
151+ 'effective_status ' => 'equivalent_clean ' ,
152+ ),
153+ )
154+ );
155+ worktree_cleanup_patch_equivalence_assert_same ('merged_to_default ' , $ merged_action , 'exact clean containment in the remote default branch should win over patch-equivalence and remote-tracking labels ' );
156+
157+ $ merged = $ root . '/merged-worktree ' ;
158+ worktree_cleanup_patch_equivalence_run ($ primary , 'git worktree add -b merged-feature ../merged-worktree origin/main ' );
159+ worktree_cleanup_patch_equivalence_run ($ merged , 'git config user.email test@example.com ' );
160+ worktree_cleanup_patch_equivalence_run ($ merged , 'git config user.name Test ' );
161+ file_put_contents ($ merged . '/merged.txt ' , "merged \n" );
162+ worktree_cleanup_patch_equivalence_run ($ merged , 'git add merged.txt ' );
163+ worktree_cleanup_patch_equivalence_run ($ merged , 'git commit -m merged-feature ' );
164+ worktree_cleanup_patch_equivalence_run ($ primary , 'git merge --ff-only merged-feature ' );
165+ worktree_cleanup_patch_equivalence_run ($ primary , 'git push origin main ' );
166+
167+ $ merged_cleanup = new class ($ root ) {
168+ use WorkspaceCoreUtilities;
169+ use WorkspaceActiveNoSignalCleanup;
170+ use WorkspaceWorktreeCleanupEngine;
171+
172+ protected const CLEANUP_GIT_PROBE_TIMEOUT = 5 ;
173+
174+ protected string $ workspace_path ;
175+
176+ public function __construct ( string $ workspace_path ) {
177+ $ this ->workspace_path = $ workspace_path ;
178+ }
179+
180+ public function get_primary_path ( string $ repo ): string {
181+ return $ this ->workspace_path . '/primary ' ;
182+ }
183+
184+ protected function resolve_remote_default_ref ( string $ primary_path , int $ timeout_seconds = 0 ): string |WP_Error |null {
185+ return 'refs/remotes/origin/main ' ;
186+ }
187+
188+ /** @return array<int,string> */
189+ protected function protected_base_branch_names (): array {
190+ return array ( 'main ' , 'master ' , 'trunk ' , 'develop ' );
191+ }
192+
193+ protected function resolve_worktree_branch_from_head_file ( string $ wt_path ): ?string {
194+ $ result = $ this ->run_git ($ wt_path , 'branch --show-current ' , self ::CLEANUP_GIT_PROBE_TIMEOUT );
195+ if ( is_wp_error ($ result ) ) {
196+ return null ;
197+ }
198+
199+ $ branch = trim ((string ) ( $ result ['output ' ] ?? '' ));
200+ return '' === $ branch ? null : $ branch ;
201+ }
202+
203+ protected function probe_worktree_dirty_count ( string $ path , int $ timeout_seconds = 0 ): int |WP_Error {
204+ $ result = $ this ->run_git ($ path , 'status --porcelain ' , $ timeout_seconds );
205+ if ( is_wp_error ($ result ) ) {
206+ return $ result ;
207+ }
208+
209+ $ lines = array_filter (array_map ('trim ' , explode ("\n" , (string ) ( $ result ['output ' ] ?? '' ))));
210+ return count ($ lines );
211+ }
212+
213+ /** @return array<string,mixed> */
214+ public function worktree_active_no_signal_report ( array $ opts = array () ): array {
215+ return array (
216+ 'success ' => true ,
217+ 'rows ' => array (
218+ array (
219+ 'handle ' => 'repo@merged-feature ' ,
220+ 'repo ' => 'repo ' ,
221+ 'branch ' => 'merged-feature ' ,
222+ 'path ' => $ this ->workspace_path . '/merged-worktree ' ,
223+ 'dirty ' => 0 ,
224+ 'unpushed ' => 0 ,
225+ 'commits_outside_default ' => 0 ,
226+ 'suggested_action ' => 'remote_tracking_clean ' ,
227+ ),
228+ ),
229+ 'summary ' => array ( 'inspected ' => 1 ),
230+ );
231+ }
232+ };
233+
234+ $ merged_apply = $ merged_cleanup ->worktree_active_no_signal_merged_apply (array ( 'dry_run ' => true ));
235+ worktree_cleanup_patch_equivalence_assert_same (true , is_array ($ merged_apply ), 'merged apply should return a dry-run result ' );
236+ worktree_cleanup_patch_equivalence_assert_same (1 , $ merged_apply ['summary ' ]['planned ' ], 'merged apply should plan clean rows with zero commits outside default even when an older report label was remote_tracking_clean ' );
237+ worktree_cleanup_patch_equivalence_assert_same ('repo@merged-feature ' , $ merged_apply ['planned ' ][0 ]['handle ' ], 'merged apply should preserve the planned handle ' );
238+
140239echo "worktree-cleanup-patch-equivalence: ok \n" ;
0 commit comments