create(); $run = OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'type' => 'baseline_capture', 'status' => 'completed', 'outcome' => 'blocked', 'context' => [ 'reason_code' => 'missing_capability', 'baseline_capture' => [ 'subjects_total' => 0, 'gaps' => [ 'count' => 0, ], ], ], 'failure_summary' => [[ 'reason_code' => 'missing_capability', 'message' => 'A required capability is missing for this run.', ]], 'completed_at' => now(), ]); $summary = app(GovernanceRunDiagnosticSummaryBuilder::class)->build($run); expect($summary)->not->toBeNull() ->and($summary?->headline)->toContain('No baseline was captured') ->and($summary?->dominantCause['label'])->toBe('No governed subjects captured') ->and($summary?->nextActionCategory)->toBe('refresh_prerequisite_data') ->and($summary?->affectedScaleCue['label'])->toBe('Capture scope') ->and($summary?->affectedScaleCue['value'])->toContain('0 governed subjects'); }); it('derives an ambiguous baseline compare summary with affected scale and scope review guidance', function (): void { $tenant = Tenant::factory()->create(); $run = OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'type' => 'baseline_compare', 'status' => 'completed', 'outcome' => 'partially_succeeded', 'context' => [ 'baseline_compare' => [ 'reason_code' => 'ambiguous_subjects', 'subjects_total' => 12, 'evidence_gaps' => [ 'count' => 4, ], 'coverage' => [ 'proof' => false, ], ], ], 'summary_counts' => [ 'total' => 0, 'processed' => 0, 'errors_recorded' => 2, ], 'completed_at' => now(), ]); $summary = app(GovernanceRunDiagnosticSummaryBuilder::class)->build($run); expect($summary)->not->toBeNull() ->and($summary?->headline)->toContain('ambiguous subject matching') ->and($summary?->dominantCause['label'])->toBe('Ambiguous matches') ->and($summary?->nextActionCategory)->toBe('review_scope_or_ambiguous_matches') ->and($summary?->affectedScaleCue['label'])->toBe('Affected subjects') ->and($summary?->affectedScaleCue['value'])->toContain('4 governed subjects'); }); it('keeps execution outcome separate from artifact impact for stale evidence snapshot runs', function (): void { $tenant = Tenant::factory()->create(); [, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner'); $run = $this->makeArtifactTruthRun($tenant, 'tenant.evidence.snapshot.generate'); $this->makeStaleArtifactTruthEvidenceSnapshot( tenant: $tenant, snapshotOverrides: [ 'operation_run_id' => (int) $run->getKey(), ], ); $summary = app(GovernanceRunDiagnosticSummaryBuilder::class)->build($run->fresh()); expect($summary)->not->toBeNull() ->and($summary?->executionOutcomeLabel)->toBe('Completed successfully') ->and($summary?->artifactImpactLabel)->not->toBe($summary?->executionOutcomeLabel) ->and($summary?->headline)->toContain('stale') ->and($summary?->nextActionCategory)->toBe('refresh_prerequisite_data'); }); it('derives resume capture or generation when a compare run records a resume token', function (): void { $tenant = Tenant::factory()->create(); $run = OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'type' => 'baseline_compare', 'status' => 'completed', 'outcome' => 'partially_succeeded', 'context' => [ 'baseline_compare' => [ 'resume_token' => 'resume-token-220', 'evidence_gaps' => [ 'count' => 2, ], ], ], 'completed_at' => now(), ]); $summary = app(GovernanceRunDiagnosticSummaryBuilder::class)->build($run); expect($summary)->not->toBeNull() ->and($summary?->nextActionCategory)->toBe('resume_capture_or_generation') ->and($summary?->headline)->toContain('evidence capture still needs to resume'); }); it('keeps deterministic multi-cause ordering for degraded review composition runs', function (): void { $tenant = Tenant::factory()->create(); [$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner'); $run = $this->makeArtifactTruthRun($tenant, 'tenant.review.compose'); $snapshot = $this->makeStaleArtifactTruthEvidenceSnapshot($tenant, [ 'operation_run_id' => null, ]); $this->makeArtifactTruthReview( tenant: $tenant, user: $user, snapshot: $snapshot, reviewOverrides: [ 'operation_run_id' => (int) $run->getKey(), 'completeness_state' => 'partial', ], summaryOverrides: [ 'section_state_counts' => [ 'complete' => 4, 'partial' => 1, 'missing' => 1, 'stale' => 0, ], ], ); $builder = app(GovernanceRunDiagnosticSummaryBuilder::class); $first = $builder->build($run->fresh()); $second = $builder->build($run->fresh()); expect($first)->not->toBeNull() ->and($second)->not->toBeNull() ->and($first?->dominantCause['label'])->toBe('Missing sections') ->and($first?->secondaryCauses[0]['label'] ?? null)->toBe('Stale evidence basis') ->and($first?->secondaryCauses)->toEqual($second?->secondaryCauses) ->and($first?->headline)->toContain('missing sections and stale evidence'); }); it('derives no further action for publishable review pack runs', function (): void { $tenant = Tenant::factory()->create(); [$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner'); $run = $this->makeArtifactTruthRun($tenant, 'tenant.review_pack.generate'); $snapshot = $this->makeArtifactTruthEvidenceSnapshot($tenant, [ 'operation_run_id' => null, ]); $review = $this->makeArtifactTruthReview($tenant, $user, $snapshot, [ 'operation_run_id' => null, ]); $this->makeArtifactTruthReviewPack( tenant: $tenant, user: $user, snapshot: $snapshot, review: $review, packOverrides: [ 'operation_run_id' => (int) $run->getKey(), ], ); $summary = app(GovernanceRunDiagnosticSummaryBuilder::class)->build($run->fresh()); expect($summary)->not->toBeNull() ->and($summary?->nextActionCategory)->toBe('no_further_action') ->and($summary?->nextActionText)->toBe('No action needed.'); }); it('does not invent new summary count keys while deriving scale cues', function (): void { $tenant = Tenant::factory()->create(); $run = OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'type' => 'baseline_compare', 'status' => 'completed', 'outcome' => 'partially_succeeded', 'summary_counts' => [ 'total' => 7, 'custom_noise' => 99, ], 'context' => [ 'baseline_compare' => [], ], 'completed_at' => now(), ]); $summary = app(GovernanceRunDiagnosticSummaryBuilder::class)->build($run); expect($summary)->not->toBeNull() ->and(array_keys(SummaryCountsNormalizer::normalize(is_array($run->summary_counts) ? $run->summary_counts : []))) ->toBe(['total']) ->and($summary?->affectedScaleCue['source'])->toBe('summary_counts') ->and($summary?->affectedScaleCue['label'])->toBe('Total'); });