create(); createUserWithTenant(tenant: $tenant, user: $approver, role: 'owner', workspaceRole: 'manager'); $finding = Finding::factory() ->for($tenant) ->riskAccepted() ->create(array_merge([ 'workspace_id' => (int) $tenant->workspace_id, ], $findingAttributes)); $exception = FindingException::query()->create(array_merge([ 'workspace_id' => (int) $tenant->workspace_id, 'managed_environment_id' => (int) $tenant->getKey(), 'finding_id' => (int) $finding->getKey(), 'requested_by_user_id' => (int) $owner->getKey(), 'owner_user_id' => (int) $owner->getKey(), 'approved_by_user_id' => (int) $approver->getKey(), 'status' => FindingException::STATUS_ACTIVE, 'current_validity_state' => FindingException::VALIDITY_VALID, 'request_reason' => 'Temporary compensating control is still in place.', 'approval_reason' => 'Accepted until scheduled remediation is complete.', 'requested_at' => now()->subDays(10), 'approved_at' => now()->subDays(9), 'effective_from' => now()->subDays(9), 'review_due_at' => now()->addDays(14), 'expires_at' => now()->addDays(30), 'evidence_summary' => ['reference_count' => 0], ], $exceptionAttributes)); if ($decisionType !== null) { $decision = $exception->decisions()->create([ 'workspace_id' => (int) $tenant->workspace_id, 'managed_environment_id' => (int) $tenant->getKey(), 'actor_user_id' => (int) $approver->getKey(), 'decision_type' => $decisionType, 'reason' => 'Spec354 adapter test decision.', 'metadata' => $decisionMetadata, 'decided_at' => now()->subDays(9), ]); $exception->forceFill(['current_decision_id' => (int) $decision->getKey()])->save(); } return [$tenant, $finding->fresh(), $exception->fresh(['finding', 'tenant', 'owner', 'currentDecision', 'evidenceReferences'])]; } it('classifies ready and expiring accepted-risk states', function (): void { [, , $ready] = spec354AdapterFixture(); [, , $expiring] = spec354AdapterFixture( exceptionAttributes: [ 'review_due_at' => now()->addDay(), 'expires_at' => now()->addDays(2), ], ); $adapter = app(AcceptedRiskResolutionAdapter::class); expect($adapter->forQueue($ready)['key'])->toBe('accepted_risk.ready') ->and($adapter->forQueue($ready)['title'])->toBe(__('localization.accepted_risk_guidance.title_ready')) ->and($adapter->forQueue($expiring)['key'])->toBe('accepted_risk.expiring') ->and($adapter->forQueue($expiring)['title'])->toBe(__('localization.accepted_risk_guidance.title_expiring')); }); it('classifies expired accepted-risk state', function (): void { [, , $exception] = spec354AdapterFixture( exceptionAttributes: [ 'status' => FindingException::STATUS_ACTIVE, 'current_validity_state' => FindingException::VALIDITY_VALID, 'review_due_at' => now()->subDays(3), 'expires_at' => now()->subDay(), ], decisionType: FindingExceptionDecision::TYPE_APPROVED, ); $guidance = app(AcceptedRiskResolutionAdapter::class)->forQueue($exception); expect($guidance['title'])->toBe(__('localization.accepted_risk_guidance.title_expired')); }); it('classifies revoked accepted-risk state', function (): void { [, , $exception] = spec354AdapterFixture( exceptionAttributes: [ 'status' => FindingException::STATUS_REVOKED, 'current_validity_state' => FindingException::VALIDITY_REVOKED, 'revoked_at' => now()->subDay(), ], decisionType: FindingExceptionDecision::TYPE_REVOKED, ); $guidance = app(AcceptedRiskResolutionAdapter::class)->forQueue($exception); expect($guidance['title'])->toBe(__('localization.accepted_risk_guidance.title_revoked')); }); it('classifies rejected accepted-risk state', function (): void { [, , $exception] = spec354AdapterFixture( exceptionAttributes: [ 'status' => FindingException::STATUS_REJECTED, 'current_validity_state' => FindingException::VALIDITY_REJECTED, 'rejected_at' => now()->subDay(), ], decisionType: FindingExceptionDecision::TYPE_REJECTED, ); $guidance = app(AcceptedRiskResolutionAdapter::class)->forQueue($exception); expect($guidance['title'])->toBe(__('localization.accepted_risk_guidance.title_rejected')); }); it('distinguishes pending initial review from pending renewal', function (): void { [, , $pendingInitial] = spec354AdapterFixture( exceptionAttributes: [ 'status' => FindingException::STATUS_PENDING, 'current_validity_state' => FindingException::VALIDITY_MISSING_SUPPORT, 'approved_by_user_id' => null, 'approved_at' => null, 'effective_from' => null, 'approval_reason' => null, ], decisionType: FindingExceptionDecision::TYPE_REQUESTED, ); [, , $pendingRenewal] = spec354AdapterFixture( exceptionAttributes: [ 'status' => FindingException::STATUS_PENDING, 'current_validity_state' => FindingException::VALIDITY_VALID, ], decisionType: FindingExceptionDecision::TYPE_RENEWAL_REQUESTED, ); $adapter = app(AcceptedRiskResolutionAdapter::class); expect($adapter->forQueue($pendingInitial)['title'])->toBe(__('localization.accepted_risk_guidance.title_pending')) ->and($adapter->forQueue($pendingInitial)['primary_action']['label'])->toBe(__('localization.accepted_risk_guidance.next_step_pending')) ->and($adapter->forQueue($pendingRenewal)['title'])->toBe(__('localization.accepted_risk_guidance.title_pending_renewal')) ->and($adapter->forQueue($pendingRenewal)['primary_action']['label'])->toBe(__('localization.accepted_risk_guidance.next_step_pending_renewal')); }); it('keeps lapsed carried-over governance dominant over pending renewal guidance', function (): void { [, , $expiredRenewal] = spec354AdapterFixture( exceptionAttributes: [ 'status' => FindingException::STATUS_PENDING, 'current_validity_state' => FindingException::VALIDITY_VALID, ], decisionType: FindingExceptionDecision::TYPE_RENEWAL_REQUESTED, decisionMetadata: [ 'previous_review_due_at' => now()->subDays(2)->toIso8601String(), 'previous_expires_at' => now()->subDay()->toIso8601String(), ], ); [, , $expiringRenewal] = spec354AdapterFixture( exceptionAttributes: [ 'status' => FindingException::STATUS_PENDING, 'current_validity_state' => FindingException::VALIDITY_VALID, ], decisionType: FindingExceptionDecision::TYPE_RENEWAL_REQUESTED, decisionMetadata: [ 'previous_review_due_at' => now()->addDay()->toIso8601String(), 'previous_expires_at' => now()->addDays(2)->toIso8601String(), ], ); $adapter = app(AcceptedRiskResolutionAdapter::class); expect($adapter->forQueue($expiredRenewal)['key'])->toBe('accepted_risk.expired') ->and($adapter->forQueue($expiringRenewal)['key'])->toBe('accepted_risk.expiring'); }); it('classifies missing support, fresh decision required, and incomplete governance states', function (): void { [, , $missingSupport] = spec354AdapterFixture( exceptionAttributes: [ 'current_validity_state' => FindingException::VALIDITY_MISSING_SUPPORT, 'status' => FindingException::STATUS_ACTIVE, ], ); [, , $freshDecision] = spec354AdapterFixture( findingAttributes: [ 'status' => Finding::STATUS_NEW, ], ); [, , $incompleteGovernance] = spec354AdapterFixture( exceptionAttributes: [ 'owner_user_id' => null, 'request_reason' => '', 'review_due_at' => null, ], ); $adapter = app(AcceptedRiskResolutionAdapter::class); expect($adapter->forQueue($missingSupport)['key'])->toBe('accepted_risk.missing_support') ->and($adapter->forQueue($freshDecision)['key'])->toBe('accepted_risk.fresh_decision_required') ->and($adapter->forQueue($freshDecision)['reason'])->toContain('fresh decision') ->and($adapter->forDetail($incompleteGovernance)['key'])->toBe('accepted_risk.incomplete_governance') ->and($adapter->forDetail($incompleteGovernance)['technical_details'])->toHaveKey(__('localization.accepted_risk_guidance.detail_missing_fields_label')); }); it('keeps missing support as review focus instead of a fake primary action', function (): void { [, , $exception] = spec354AdapterFixture( exceptionAttributes: [ 'current_validity_state' => FindingException::VALIDITY_MISSING_SUPPORT, 'status' => FindingException::STATUS_ACTIVE, ], ); $guidance = app(AcceptedRiskResolutionAdapter::class)->forDetail($exception); expect($guidance['primary_action']['type'])->toBe(ResolutionAction::TYPE_NONE) ->and($guidance['primary_action']['label'])->toBe(__('localization.accepted_risk_guidance.next_step_missing_support')) ->and($guidance['primary_action']['label'])->toContain('Review whether'); }); it('localizes dominant accepted-risk guidance copy for german locale', function (): void { $originalLocale = app()->getLocale(); [, , $exception] = spec354AdapterFixture( exceptionAttributes: [ 'review_due_at' => now()->addDay(), 'expires_at' => now()->addDays(2), ], ); app()->setLocale('de'); $guidance = app(AcceptedRiskResolutionAdapter::class)->forQueue($exception); expect($guidance['reason'])->toBe(__('localization.accepted_risk_guidance.reason_expiring')) ->and($guidance['impact'])->toBe(__('localization.accepted_risk_guidance.impact_expiring')) ->and($guidance['reason'])->not->toContain('The current accepted-risk governance window'); app()->setLocale($originalLocale); }); it('stays database-local and preserves conservative wording without mutating downstream review output state', function (): void { bindFailHardGraphClient(); [, , $exception] = spec354AdapterFixture(); assertNoOutboundHttp(function () use ($exception): void { $guidance = app(AcceptedRiskResolutionAdapter::class)->forDetail($exception); expect($guidance['primary_action']['label'])->toBe(__('localization.accepted_risk_guidance.next_step_ready')); }); });