@@ -22,9 +22,113 @@ public static function get(): ?self
2222 if (! defined ('ABSPATH ' ) ) {
2323 define ('ABSPATH ' , $ tmp . '/wp/ ' );
2424 }
25- if (! defined ('DATAMACHINE_WORKSPACE_PATH ' ) ) {
26- define ('DATAMACHINE_WORKSPACE_PATH ' , $ tmp . '/workspace ' );
27- }
25+ if (! defined ('DATAMACHINE_WORKSPACE_PATH ' ) ) {
26+ define ('DATAMACHINE_WORKSPACE_PATH ' , $ tmp . '/workspace ' );
27+ }
28+ if (! defined ('ARRAY_A ' ) ) {
29+ define ('ARRAY_A ' , 'ARRAY_A ' );
30+ }
31+
32+ if (! function_exists ('current_time ' ) ) {
33+ function current_time ( string $ type , bool $ gmt = false ): string // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
34+ {
35+ return '2026-06-14 00:00:00 ' ;
36+ }
37+ }
38+ if (! function_exists ('wp_json_encode ' ) ) {
39+ function wp_json_encode ( mixed $ value , int $ flags = 0 , int $ depth = 512 ): string |false
40+ {
41+ return json_encode ($ value , $ flags , $ depth );
42+ }
43+ }
44+
45+ class DatamachineCodePruneFakeWpdb
46+ {
47+ public string $ prefix = 'wp_ ' ;
48+ public int $ insert_id = 0 ;
49+ public int $ rows_affected = 0 ;
50+ public string $ last_error = '' ;
51+
52+ /**
53+ * @var array<string,array<string,mixed>>
54+ */
55+ public array $ rows = array ();
56+ public array $ lock_rows = array ();
57+
58+ public function get_var ( string $ query ): string |int |null
59+ {
60+ if ( str_contains ($ query , 'SHOW TABLES LIKE ' ) ) {
61+ return $ this ->prefix . 'datamachine_code_locks ' ;
62+ }
63+
64+ if ( str_contains ($ query , 'COUNT(*) ' ) ) {
65+ return 0 ;
66+ }
67+
68+ return null ;
69+ }
70+
71+ public function query ( string $ query ): int
72+ {
73+ $ this ->rows_affected = 0 ;
74+ return 0 ;
75+ }
76+
77+ public function replace ( string $ table , array $ data ): int // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
78+ {
79+ $ this ->rows [ (string ) $ data ['handle ' ] ] = $ data ;
80+ return 1 ;
81+ }
82+
83+ public function insert ( string $ table , array $ data , ?array $ format = null ): int // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
84+ {
85+ ++$ this ->insert_id ;
86+ $ data ['id ' ] = $ this ->insert_id ;
87+ $ this ->lock_rows [ $ this ->insert_id ] = $ data ;
88+ return 1 ;
89+ }
90+
91+ public function delete ( string $ table , array $ where ): int // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
92+ {
93+ unset($ this ->rows [ (string ) $ where ['handle ' ] ]);
94+ return 1 ;
95+ }
96+
97+ public function update ( string $ table , array $ data , array $ where , ?array $ format = null , ?array $ where_format = null ): int // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
98+ {
99+ if ( str_contains ($ table , 'datamachine_code_locks ' ) ) {
100+ $ id = (int ) ( $ where ['id ' ] ?? 0 );
101+ if ( isset ($ this ->lock_rows [ $ id ]) ) {
102+ $ this ->lock_rows [ $ id ] = array_merge ($ this ->lock_rows [ $ id ], $ data );
103+ return 1 ;
104+ }
105+ return 0 ;
106+ }
107+
108+ $ handle = (string ) ( $ where ['handle ' ] ?? '' );
109+ if ( ! isset ($ this ->rows [ $ handle ]) ) {
110+ return 0 ;
111+ }
112+
113+ $ this ->rows [ $ handle ] = array_merge ($ this ->rows [ $ handle ], $ data );
114+ return 1 ;
115+ }
116+
117+ public function get_results ( string $ sql , string $ output ): array // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
118+ {
119+ $ rows = array_values ($ this ->rows );
120+ usort ($ rows , fn ( array $ a , array $ b ): int => strcmp ((string ) $ a ['handle ' ], (string ) $ b ['handle ' ]));
121+ return $ rows ;
122+ }
123+
124+ public function prepare ( string $ query , mixed ...$ args ): string
125+ {
126+ foreach ( $ args as $ arg ) {
127+ $ query = preg_replace ('/%s/ ' , "' " . addslashes ((string ) $ arg ) . "' " , $ query , 1 );
128+ }
129+ return $ query ;
130+ }
131+ }
28132
29133 if (! class_exists ('WP_Error ' ) ) {
30134 class WP_Error
@@ -70,15 +174,43 @@ function is_wp_error( $value ): bool
70174 echo " fail {$ label }\n" ;
71175 };
72176
73- $ old_path = getenv ('PATH ' );
74- putenv ('PATH=/nonexistent-dmc-no-git ' );
75-
76- mkdir (DATAMACHINE_WORKSPACE_PATH . '/demo/.git ' , 0777 , true );
77-
78- require __DIR__ . '/../inc/Support/RuntimeCapabilities.php ' ;
79- require __DIR__ . '/../inc/Support/ProcessRunner.php ' ;
80- require __DIR__ . '/../inc/Support/GitRunner.php ' ;
81- require __DIR__ . '/../inc/Support/PathSecurity.php ' ;
177+ $ old_path = getenv ('PATH ' );
178+ putenv ('PATH=/nonexistent-dmc-no-git ' );
179+
180+ mkdir (DATAMACHINE_WORKSPACE_PATH . '/demo/.git ' , 0777 , true );
181+ mkdir (DATAMACHINE_WORKSPACE_PATH . '/demo@stale-marker ' , 0777 , true );
182+ file_put_contents (DATAMACHINE_WORKSPACE_PATH . '/demo@stale-marker/.git ' , 'gitdir: ' . DATAMACHINE_WORKSPACE_PATH . '/demo/.git/worktrees/demo@stale-marker ' . "\n" );
183+
184+ $ GLOBALS ['wpdb ' ] = new DatamachineCodePruneFakeWpdb ();
185+ $ GLOBALS ['wpdb ' ]->rows ['demo@missing-path ' ] = array (
186+ 'handle ' => 'demo@missing-path ' ,
187+ 'repo ' => 'demo ' ,
188+ 'branch ' => 'missing-path ' ,
189+ 'path ' => DATAMACHINE_WORKSPACE_PATH . '/demo@missing-path ' ,
190+ 'primary_path ' => DATAMACHINE_WORKSPACE_PATH . '/demo ' ,
191+ 'is_primary ' => 0 ,
192+ 'is_worktree ' => 1 ,
193+ 'missing_path ' => 1 ,
194+ 'metadata ' => array (),
195+ 'updated_at ' => '2026-06-13 00:00:00 ' ,
196+ );
197+ $ GLOBALS ['wpdb ' ]->rows ['demo@stale-marker ' ] = array (
198+ 'handle ' => 'demo@stale-marker ' ,
199+ 'repo ' => 'demo ' ,
200+ 'branch ' => 'stale-marker ' ,
201+ 'path ' => DATAMACHINE_WORKSPACE_PATH . '/demo@stale-marker ' ,
202+ 'primary_path ' => DATAMACHINE_WORKSPACE_PATH . '/demo ' ,
203+ 'is_primary ' => 0 ,
204+ 'is_worktree ' => 1 ,
205+ 'missing_path ' => 0 ,
206+ 'metadata ' => array (),
207+ 'updated_at ' => '2026-06-13 00:00:00 ' ,
208+ );
209+
210+ require __DIR__ . '/../inc/Support/RuntimeCapabilities.php ' ;
211+ require __DIR__ . '/../inc/Support/ProcessRunner.php ' ;
212+ require __DIR__ . '/../inc/Support/GitRunner.php ' ;
213+ require __DIR__ . '/../inc/Support/PathSecurity.php ' ;
82214 require __DIR__ . '/../inc/Workspace/WorktreeContextInjector.php ' ;
83215 require __DIR__ . '/../inc/Workspace/WorkspaceMutationLock.php ' ;
84216 require __DIR__ . '/../inc/Workspace/Workspace.php ' ;
@@ -89,9 +221,12 @@ function is_wp_error( $value ): bool
89221 $ result = $ workspace ->worktree_prune ();
90222
91223 $ assert ('prune returns success instead of git-unavailable error ' , ! is_wp_error ($ result ) && true === ( $ result ['success ' ] ?? false ));
92- $ assert ('prune records skipped primary ' , ! is_wp_error ($ result ) && 'demo ' === ( $ result ['skipped ' ][0 ]['repo ' ] ?? '' ));
93- $ assert ('prune returns host git command ' , ! is_wp_error ($ result ) && str_contains ((string ) ( $ result ['next_commands ' ][0 ] ?? '' ), 'git -C ' ));
94- $ assert ('inventory refresh still runs ' , ! is_wp_error ($ result ) && isset ($ result ['inventory ' ]['summary ' ]));
224+ $ assert ('prune records skipped primary ' , ! is_wp_error ($ result ) && 'demo ' === ( $ result ['skipped ' ][0 ]['repo ' ] ?? '' ));
225+ $ assert ('prune returns host git command ' , ! is_wp_error ($ result ) && str_contains ((string ) ( $ result ['next_commands ' ][0 ] ?? '' ), 'git -C ' ));
226+ $ assert ('inventory refresh still runs ' , ! is_wp_error ($ result ) && isset ($ result ['inventory ' ]['summary ' ]));
227+ $ assert ('prune removes missing-path inventory artifact ' , ! is_wp_error ($ result ) && 'demo@missing-path ' === ( $ result ['stale_inventory ' ][0 ]['handle ' ] ?? '' ) && ! isset ($ GLOBALS ['wpdb ' ]->rows ['demo@missing-path ' ]));
228+ $ assert ('prune reports path-present stale marker blocker ' , ! is_wp_error ($ result ) && 'stale_worktree_marker ' === ( $ result ['stale_marker_blockers ' ][0 ]['reason_code ' ] ?? '' ));
229+ $ assert ('prune leaves path-present stale marker row for review ' , isset ($ GLOBALS ['wpdb ' ]->rows ['demo@stale-marker ' ]));
95230
96231 putenv (false === $ old_path ? 'PATH ' : 'PATH= ' . $ old_path );
97232
0 commit comments