create([ 'managed_environment_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, ]); $payload = app(BaselineDriftPostureSource::class)->collect($tenant); expect($payload['state'])->toBe(EvidenceCompletenessState::Missing->value) ->and($payload['summary_payload']['drift_count'])->toBe(0) ->and($payload['summary_payload'])->not->toHaveKey('provider_resource_bindings') ->and($payload['summary_payload']['latest_compare_run_id'])->toBeNull(); }); it('does not mark old successful compare context as complete without structured readiness semantics', function (): void { [, $tenant] = createUserWithTenant(role: 'owner'); [$profile, $snapshot] = seedActiveBaselineForTenant($tenant); seedBaselineCompareRun( tenant: $tenant, profile: $profile, snapshot: $snapshot, compareContext: ['reason_code' => 'baseline.compare.no_drift_detected'], outcome: OperationRunOutcome::Succeeded->value, ); $payload = app(BaselineDriftPostureSource::class)->collect($tenant); expect($payload['state'])->toBe(EvidenceCompletenessState::Missing->value) ->and($payload['summary_payload']['drift_count'])->toBe(0) ->and($payload['summary_payload']['baseline_readiness']['readiness_state'])->toBe('baseline_compare_unproven') ->and($payload['summary_payload']['baseline_readiness']['publication_blockers'])->not->toBeEmpty(); }); it('marks no baseline drift complete when structured compare semantics verify no drift', function (): void { [, $tenant] = createUserWithTenant(role: 'owner'); [$profile, $snapshot] = seedActiveBaselineForTenant($tenant); $run = seedBaselineCompareRun( tenant: $tenant, profile: $profile, snapshot: $snapshot, compareContext: spec385EvidenceCompareContext([CompareResultReason::VerifiedNoDrift]), ); $payload = app(BaselineDriftPostureSource::class)->collect($tenant); expect($payload['state'])->toBe(EvidenceCompletenessState::Complete->value) ->and($payload['measured_at']?->equalTo($run->completed_at))->toBeTrue() ->and($payload['summary_payload']['drift_count'])->toBe(0) ->and($payload['summary_payload']['latest_compare_run_id'])->toBe((int) $run->getKey()) ->and($payload['summary_payload']['latest_compare_outcome'])->toBe(OperationRunOutcome::Succeeded->value) ->and($payload['summary_payload']['baseline_readiness']['customer_safe_claim'])->toBe('customer_ready'); }); it('honors active provider resource decisions from stored bindings', function (): void { [, $tenant] = createUserWithTenant(role: 'owner'); [$profile, $snapshot] = seedActiveBaselineForTenant($tenant); seedBaselineCompareRun( tenant: $tenant, profile: $profile, snapshot: $snapshot, compareContext: spec385EvidenceCompareContext([CompareResultReason::VerifiedNoDrift]), ); ProviderResourceBinding::factory()->create([ 'managed_environment_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'resolution_mode' => ProviderResourceResolutionMode::AcceptedLimitation->value, ]); $payload = app(BaselineDriftPostureSource::class)->collect($tenant); expect($payload['state'])->toBe(EvidenceCompletenessState::Partial->value) ->and($payload['summary_payload']['baseline_readiness']['readiness_state'])->toBe('baseline_compare_limited') ->and($payload['summary_payload']['baseline_readiness']['limitation_codes'])->toBe(['baseline_accepted_limitations']) ->and($payload['summary_payload']['baseline_readiness']['publication_blockers'])->toBe([]); }); it('blocks when stored binding decisions were revoked after the latest compare', function (): void { [, $tenant] = createUserWithTenant(role: 'owner'); [$profile, $snapshot] = seedActiveBaselineForTenant($tenant); seedBaselineCompareRun( tenant: $tenant, profile: $profile, snapshot: $snapshot, compareContext: spec385EvidenceCompareContext([CompareResultReason::VerifiedNoDrift]), completedAt: now()->subMinutes(5), ); ProviderResourceBinding::factory() ->revoked() ->create([ 'managed_environment_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'ended_at' => now(), ]); $payload = app(BaselineDriftPostureSource::class)->collect($tenant); expect($payload['state'])->toBe(EvidenceCompletenessState::Partial->value) ->and($payload['summary_payload']['baseline_readiness']['readiness_state'])->toBe('baseline_compare_blocked') ->and($payload['summary_payload']['baseline_readiness']['publication_blockers'])->toContain('Baseline subject decisions changed after the latest compare; refresh evidence before publication.'); }); it('surfaces structured baseline evidence gaps as readiness blockers', function (): void { [, $tenant] = createUserWithTenant(role: 'owner'); [$profile, $snapshot] = seedActiveBaselineForTenant($tenant); seedBaselineCompareRun( tenant: $tenant, profile: $profile, snapshot: $snapshot, compareContext: spec385EvidenceCompareContext([CompareResultReason::MissingLocalEvidence]), outcome: OperationRunOutcome::PartiallySucceeded->value, ); $payload = app(BaselineDriftPostureSource::class)->collect($tenant); expect($payload['state'])->toBe(EvidenceCompletenessState::Missing->value) ->and($payload['summary_payload']['baseline_readiness']['readiness_state'])->toBe('baseline_local_evidence_missing') ->and($payload['summary_payload']['baseline_readiness']['next_action'])->toBe('open_evidence_basis') ->and($payload['summary_payload']['baseline_readiness']['publication_blockers'])->not->toBeEmpty(); }); it('maps structured baseline readiness source edge cases through the evidence item', function ( CompareResultReason $reason, int $driftCount, string $outcome, ?int $completedDaysAgo, string $expectedState, string $expectedReadinessState, array $expectedLimitationCodes, ): void { [, $tenant] = createUserWithTenant(role: 'owner'); [$profile, $snapshot] = seedActiveBaselineForTenant($tenant); if ($driftCount > 0) { \App\Models\Finding::factory() ->count($driftCount) ->create([ 'managed_environment_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'finding_type' => \App\Models\Finding::FINDING_TYPE_DRIFT, ]); } seedBaselineCompareRun( tenant: $tenant, profile: $profile, snapshot: $snapshot, compareContext: spec385EvidenceCompareContext([$reason]), outcome: $outcome, completedAt: $completedDaysAgo === null ? now() : now()->subDays($completedDaysAgo), ); $payload = app(BaselineDriftPostureSource::class)->collect($tenant); expect($payload['state'])->toBe($expectedState) ->and($payload['summary_payload']['baseline_readiness']['readiness_state'])->toBe($expectedReadinessState) ->and($payload['summary_payload']['baseline_readiness']['limitation_codes'])->toBe($expectedLimitationCodes); })->with([ 'trusted drift' => [ CompareResultReason::VerifiedDriftDetected, 1, OperationRunOutcome::Succeeded->value, null, EvidenceCompletenessState::Complete->value, 'trusted_drift_detected', [], ], 'accepted limitation' => [ CompareResultReason::AcceptedLimitation, 0, OperationRunOutcome::PartiallySucceeded->value, null, EvidenceCompletenessState::Partial->value, 'baseline_compare_limited', ['baseline_accepted_limitations'], ], 'excluded subject' => [ CompareResultReason::ExcludedNonGoverned, 0, OperationRunOutcome::PartiallySucceeded->value, null, EvidenceCompletenessState::Partial->value, 'baseline_compare_limited', ['baseline_exclusions_present'], ], 'failed compare' => [ CompareResultReason::CompareFailed, 0, OperationRunOutcome::Failed->value, null, EvidenceCompletenessState::Missing->value, 'baseline_compare_failed', [], ], 'stale compare' => [ CompareResultReason::VerifiedNoDrift, 0, OperationRunOutcome::Succeeded->value, 45, EvidenceCompletenessState::Stale->value, 'baseline_compare_stale', [], ], ]); /** * @param list $reasons * @return array */ function spec385EvidenceCompareContext(array $reasons): array { $byReason = []; $byReadinessImpact = []; foreach ($reasons as $reason) { $byReason[$reason->value] = ($byReason[$reason->value] ?? 0) + 1; $impact = $reason->readinessImpact()->value; $byReadinessImpact[$impact] = ($byReadinessImpact[$impact] ?? 0) + 1; } return [ 'result_semantics' => [ 'version' => 'compare_semantics.v1', 'run_outcome' => 'completed', 'operation_outcome' => OperationRunOutcome::Succeeded->value, 'counts' => [ 'by_reason' => $byReason, 'by_readiness_impact' => $byReadinessImpact, ], ], ]; }