diff --git a/inc/Workspace/Workspace.php b/inc/Workspace/Workspace.php index 42ea274..67846bc 100644 --- a/inc/Workspace/Workspace.php +++ b/inc/Workspace/Workspace.php @@ -1807,7 +1807,7 @@ public function worktree_active_no_signal_finalized_apply( array $opts = array() 'written' => $written, 'skipped' => $skipped, 'summary' => $summary, - 'pagination' => $this->build_active_no_signal_apply_pagination( (array) ( $report['pagination'] ?? array() ), 'active-no-signal-finalized-apply', $dry_run, $opts), + 'pagination' => $this->build_active_no_signal_apply_pagination( (array) ( $report['pagination'] ?? array() ), 'active-no-signal-finalized-apply', $dry_run, $opts, count( $written ) ), 'evidence' => array( 'scope' => 'promote finalized active_no_signal PR evidence into cleanup_eligible metadata', 'safety' => 'Revalidates dirty, unpushed, identity, and closed+merged PR evidence before writing metadata. Does not delete worktrees.', @@ -1900,7 +1900,7 @@ public function worktree_active_no_signal_equivalent_clean_apply( array $opts = 'written' => $written, 'skipped' => $skipped, 'summary' => $summary, - 'pagination' => $this->build_active_no_signal_apply_pagination( (array) ( $report['pagination'] ?? array() ), 'active-no-signal-equivalent-clean-apply', $dry_run, $opts), + 'pagination' => $this->build_active_no_signal_apply_pagination( (array) ( $report['pagination'] ?? array() ), 'active-no-signal-equivalent-clean-apply', $dry_run, $opts, count( $written ) ), 'evidence' => array( 'scope' => 'promote effectively clean upstream-equivalent active_no_signal rows into cleanup_eligible metadata', 'safety' => 'Revalidates upstream-equivalence evidence before writing metadata. Does not delete worktrees.', @@ -1993,7 +1993,7 @@ public function worktree_active_no_signal_merged_apply( array $opts = array() ): 'written' => $written, 'skipped' => $skipped, 'summary' => $summary, - 'pagination' => $this->build_active_no_signal_apply_pagination( (array) ( $report['pagination'] ?? array() ), 'active-no-signal-merged-apply', $dry_run, $opts), + 'pagination' => $this->build_active_no_signal_apply_pagination( (array) ( $report['pagination'] ?? array() ), 'active-no-signal-merged-apply', $dry_run, $opts, count( $written ) ), 'evidence' => array( 'scope' => 'promote clean active_no_signal rows contained in remote default into cleanup_eligible metadata', 'safety' => 'Revalidates clean worktree, no unpushed commits, containment, primary protection, branch identity, and merged-to-default evidence before writing metadata. Does not delete worktrees.', @@ -2010,7 +2010,7 @@ public function worktree_active_no_signal_merged_apply( array $opts = array() ): * @param array $opts Original operation options. * @return array */ - private function build_active_no_signal_apply_pagination( array $pagination, string $operation, bool $dry_run, array $opts ): array { + private function build_active_no_signal_apply_pagination( array $pagination, string $operation, bool $dry_run, array $opts, int $written_count = 0 ): array { if ( null === ( $pagination['next_offset'] ?? null ) ) { $pagination['next_command'] = null; return $pagination; @@ -2027,6 +2027,11 @@ private function build_active_no_signal_apply_pagination( array $pagination, str $dry_run_arg = $dry_run ? ' --dry-run' : ''; $limit = (int) ( $pagination['limit'] ?? 25 ); $next_offset = (int) $pagination['next_offset']; + if ( ! $dry_run && $written_count > 0 ) { + $current = (int) ( $pagination['offset'] ?? 0 ); + $next_offset = max( $current, $next_offset - $written_count ); + $pagination['next_offset'] = $next_offset; + } $pagination['next_command'] = sprintf( 'studio wp datamachine-code workspace worktree %s%s --limit=%d --offset=%d%s --format=json', diff --git a/tests/smoke-worktree-metadata-reconcile.php b/tests/smoke-worktree-metadata-reconcile.php index f558642..e78e930 100644 --- a/tests/smoke-worktree-metadata-reconcile.php +++ b/tests/smoke-worktree-metadata-reconcile.php @@ -905,8 +905,12 @@ public function worktree_list( ?string $repo = null, ?string $state = null, arra $merged_report = $ws->worktree_active_no_signal_report(array( 'limit' => 100, 'offset' => 0 )); $assert(true, ! is_wp_error($merged_report) && ( $merged_report['success'] ?? false ), 'merged-to-default active/no-signal report succeeds'); $merged_rows = array(); + $first_merged_offset = null; foreach ((array) ( $merged_report['rows'] ?? array() ) as $row ) { $merged_rows[ $row['handle'] ?? '' ] = $row; + if ( null === $first_merged_offset && 'merged_to_default' === (string) ( $row['suggested_action'] ?? '' ) ) { + $first_merged_offset = count($merged_rows) - 1; + } } $assert('merged_to_default', $merged_rows['demo@default-merged']['suggested_action'] ?? '', 'clean contained branch is classified merged_to_default'); $assert('fix/foo', $merged_rows['demo@fix-foo']['branch'] ?? '', 'active/no-signal report uses actual checked-out branch with slashes'); @@ -929,10 +933,14 @@ public function worktree_list( ?string $repo = null, ?string $state = null, arra $assert('', \DataMachineCode\Workspace\WorktreeContextInjector::get_metadata('demo@default-merged')['cleanup_eligible_at'] ?? '', 'merged-to-default dry-run leaves metadata unchanged'); $merged_page_dry_run = $ws->worktree_active_no_signal_merged_apply(array( 'dry_run' => true, 'limit' => 1, 'offset' => 0, 'internal_budget_label' => '1s', 'internal_budget_seconds' => 60, 'internal_budget_started' => microtime(true) )); $assert(true, str_contains($merged_page_dry_run['pagination']['next_command'] ?? '', 'active-no-signal-merged-apply --dry-run --limit=1 --offset=1 --until-budget=1s --format=json'), 'merged-to-default dry-run continuation stays on merged apply command'); + $assert(true, null !== $first_merged_offset, 'merged-to-default report exposes at least one merged candidate offset'); + $merged_page_apply = $ws->worktree_active_no_signal_merged_apply(array( 'limit' => 1, 'offset' => (int) $first_merged_offset, 'internal_budget_label' => '1s', 'internal_budget_seconds' => 60, 'internal_budget_started' => microtime(true) )); + $assert(1, (int) ( $merged_page_apply['summary']['written'] ?? 0 ), 'merged-to-default page apply writes one row'); + $assert(true, str_contains($merged_page_apply['pagination']['next_command'] ?? '', 'active-no-signal-merged-apply --limit=1 --offset=' . (int) $first_merged_offset . ' --until-budget=1s --format=json'), 'merged-to-default apply continuation accounts for rows removed from active/no-signal page'); $merged_apply = $ws->worktree_active_no_signal_merged_apply(array( 'limit' => 100, 'offset' => 0 )); $assert(true, ! is_wp_error($merged_apply) && ( $merged_apply['success'] ?? false ), 'merged-to-default active/no-signal apply succeeds'); - $assert(2, (int) ( $merged_apply['summary']['written'] ?? 0 ), 'merged-to-default apply writes only safe merged row metadata'); + $assert(1, (int) ( $merged_apply['summary']['written'] ?? 0 ), 'merged-to-default apply writes remaining safe merged row metadata'); $stored_default_merged = \DataMachineCode\Workspace\WorktreeContextInjector::get_metadata('demo@default-merged'); $assert('cleanup_eligible', $stored_default_merged['lifecycle_state'] ?? '', 'merged-to-default apply stores cleanup_eligible state'); $assert('merged', $stored_default_merged['finalized_state'] ?? '', 'merged-to-default apply records merged finalizer state');