TenantAtlas/apps/platform/tests/Unit/Support/OpsUx/GovernanceRunDiagnosticSummaryBuilderTest.php
Ahmed Darrazi c6cc58e1f3
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 43s
feat: add governance run summaries
2026-04-20 22:43:30 +02:00

232 lines
8.5 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\OperationRun;
use App\Models\Tenant;
use App\Support\OpsUx\GovernanceRunDiagnosticSummaryBuilder;
use App\Support\OpsUx\SummaryCountsNormalizer;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\Feature\Concerns\BuildsGovernanceArtifactTruthFixtures;
uses(RefreshDatabase::class, BuildsGovernanceArtifactTruthFixtures::class);
it('derives a blocked baseline capture summary with prerequisite-focused next steps', function (): void {
$tenant = Tenant::factory()->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');
});