Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 4m44s
Added BaselineReadinessGate, resolution propagation, and disclosure semantics logic per Spec 385. Integrated baseline unreadiness into Customer Review Workspace and Review Packs.
253 lines
9.9 KiB
PHP
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,
|
|
],
|
|
],
|
|
];
|
|
}
|