create([ 'status' => 'running', 'outcome' => 'pending', 'summary_counts' => [ 'total' => 10, 'processed' => 4, ], 'started_at' => now()->subMinute(), ]); $progress = OperationRunProgressContract::forRun($run); expect($progress['capability'])->toBe('counted') ->and($progress['display'])->toBe('counted') ->and($progress['processed'])->toBe(4) ->and($progress['total'])->toBe(10) ->and($progress['percent'])->toBe(40) ->and($progress['label'])->toBe('4 / 10 processed (40%)'); }); it('clamps counted progress into a truthful visible range', function (): void { $run = OperationRun::factory()->create([ 'status' => 'running', 'outcome' => 'pending', 'summary_counts' => [ 'total' => 10, 'processed' => 15, ], 'started_at' => now()->subMinute(), ]); $progress = OperationRunProgressContract::forRun($run); expect($progress['capability'])->toBe('counted') ->and($progress['processed'])->toBe(10) ->and($progress['total'])->toBe(10) ->and($progress['percent'])->toBe(100) ->and($progress['label'])->toBe('10 / 10 processed (100%)'); }); it('keeps queued runs activity only even when planned totals exist', function (): void { $run = OperationRun::factory()->create([ 'status' => 'queued', 'outcome' => 'pending', 'summary_counts' => [ 'total' => 10, 'processed' => 0, ], ]); $progress = OperationRunProgressContract::forRun($run); expect($progress['capability'])->toBe('activity') ->and($progress['display'])->toBe('indeterminate') ->and($progress['label'])->toBe('Waiting for worker.') ->and($progress['percent'])->toBeNull(); }); it('returns no progress for terminal runs even when retained counts exist', function (): void { $run = OperationRun::factory()->create([ 'status' => 'completed', 'outcome' => 'succeeded', 'summary_counts' => [ 'total' => 10, 'processed' => 10, ], 'started_at' => now()->subMinutes(2), 'completed_at' => now()->subSecond(), ]); $progress = OperationRunProgressContract::forRun($run); expect($progress['capability'])->toBe('none') ->and($progress['display'])->toBe('none') ->and($progress['label'])->toBeNull() ->and($progress['percent'])->toBeNull(); }); it('does not let outcome counters masquerade as counted progress', function (): void { $run = OperationRun::factory()->create([ 'status' => 'running', 'outcome' => 'pending', 'summary_counts' => [ 'succeeded' => 4, 'failed' => 1, 'skipped' => 2, ], 'started_at' => now()->subMinute(), ]); $progress = OperationRunProgressContract::forRun($run); expect($progress['capability'])->toBe('activity') ->and($progress['display'])->toBe('indeterminate') ->and($progress['label'])->toBe('Progress details pending.') ->and($progress['percent'])->toBeNull(); }); it('classifies repo-real baseline evidence capture runs as phased fallback', function (): void { $run = OperationRun::factory()->create([ 'type' => 'baseline_capture', 'status' => 'running', 'outcome' => 'pending', 'context' => [ 'baseline_capture' => [ 'evidence_capture' => [ 'requested' => 10, 'succeeded' => 3, 'skipped' => 1, ], 'resume_token' => 'resume-123', ], ], 'started_at' => now()->subMinute(), ]); $progress = OperationRunProgressContract::forRun($run); expect($progress['capability'])->toBe('phased') ->and($progress['display'])->toBe('indeterminate') ->and($progress['label'])->toBe('Capturing evidence.') ->and($progress['percent'])->toBeNull(); }); it('uses canonical phase metadata when present for phased runs', function (): void { $run = OperationRun::factory()->create([ 'type' => 'baseline_compare', 'status' => 'running', 'outcome' => 'pending', 'context' => [ 'progress' => [ 'phase' => [ 'key' => 'persisting', 'label' => 'Saving comparison results.', ], ], ], 'started_at' => now()->subMinute(), ]); $progress = OperationRunProgressContract::forRun($run); expect($progress['capability'])->toBe('phased') ->and($progress['display'])->toBe('indeterminate') ->and($progress['label'])->toBe('Saving comparison results.') ->and($progress['percent'])->toBeNull(); }); it('classifies aggregate multi-run work as composite fallback', function (): void { $run = OperationRun::factory()->create([ 'status' => 'running', 'outcome' => 'pending', 'summary_counts' => [ 'operation_count' => 3, ], 'started_at' => now()->subMinute(), ]); $progress = OperationRunProgressContract::forRun($run); expect($progress['capability'])->toBe('composite') ->and($progress['display'])->toBe('indeterminate') ->and($progress['label'])->toBe('Composite progress pending.') ->and($progress['percent'])->toBeNull(); }); it('derives a tenant review composite label from aggregate operation truth', function (): void { $run = OperationRun::factory()->create([ 'type' => 'tenant.review.compose', 'status' => 'running', 'outcome' => 'pending', 'summary_counts' => [ 'operation_count' => 3, ], 'started_at' => now()->subMinute(), ]); $progress = OperationRunProgressContract::forRun($run); expect($progress['capability'])->toBe('composite') ->and($progress['display'])->toBe('indeterminate') ->and($progress['label'])->toBe('Review composition is aggregating 3 operations.') ->and($progress['percent'])->toBeNull(); }); it('uses explicit composite attention hints when present', function (): void { $run = OperationRun::factory()->create([ 'type' => 'tenant.review.compose', 'status' => 'running', 'outcome' => 'pending', 'summary_counts' => [ 'operation_count' => 4, ], 'context' => [ 'progress' => [ 'composite' => [ 'label' => 'Review composition is aggregating 4 operations. 1 failed operation currently needs review.', ], ], ], 'started_at' => now()->subMinute(), ]); $progress = OperationRunProgressContract::forRun($run); expect($progress['capability'])->toBe('composite') ->and($progress['display'])->toBe('indeterminate') ->and($progress['label'])->toBe('Review composition is aggregating 4 operations. 1 failed operation currently needs review.') ->and($progress['percent'])->toBeNull(); });