TenantAtlas/apps/platform/tests/Feature/Evidence/BaselineDriftPostureSourceTest.php
ahmido 3a9402998a feat(evidence): implement baseline review readiness integration (#456)
Added `BaselineReadinessGate`, resolution propagation, and disclosure semantics logic per Spec 385. Integrates baseline unreadiness into Customer Review Workspace and Review Packs to prevent report generation when identity bindings are unresolved.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #456
2026-06-17 22:54:11 +00:00

253 lines
9.9 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\ProviderResourceBinding;
use App\Services\Evidence\Sources\BaselineDriftPostureSource;
use App\Support\Baselines\CompareSemantics\CompareResultReason;
use App\Support\Evidence\EvidenceCompletenessState;
use App\Support\OperationRunOutcome;
use App\Support\Resources\ProviderResourceResolutionMode;
it('keeps baseline drift posture missing when no drift findings or compare proof exist', function (): void {
[, $tenant] = createUserWithTenant(role: 'owner');
ProviderResourceBinding::factory()->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<CompareResultReason> $reasons
* @return array<string, mixed>
*/
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,
],
],
];
}