123]); $payload = ResolutionProofEvaluation::missing( ReviewPublicationResolutionStepKey::CompleteRequiredReports, $review, )->toStepPayload(); expect($payload['proof_type'])->toBeNull() ->and($payload['proof_id'])->toBeNull() ->and($payload['proof_status'])->toBe(ResolutionProofStatus::Missing->value) ->and($payload['proof_currentness'])->toBe(ResolutionProofCurrentness::Unknown->value) ->and($payload['proof_usability'])->toBe(ResolutionProofUsability::NotUsable->value) ->and($payload['proof_visibility'])->toBe(ResolutionProofVisibility::OperatorVisible->value); }); it('sanitizes unsafe proof summaries before persistence or audit', function (): void { $evaluation = new ResolutionProofEvaluation( actionKey: ReviewPublicationResolutionStepKey::CompleteRequiredReports, subjectType: EnvironmentReview::class, subjectId: 321, status: ResolutionProofStatus::Available, currentness: ResolutionProofCurrentness::Current, usability: ResolutionProofUsability::Usable, visibility: ResolutionProofVisibility::OperatorVisible, reasonCode: 'proof.required_reports_current', reference: new ResolutionProofReference('stored_report', 55, 'ready', now()), evaluatedAt: now(), safeSummary: [ 'label' => 'Current proof', 'message' => 'ClientException: Graph response body says Access token expired.', 'raw_graph_response' => ['access_token' => 'abc'], 'exception_message' => 'secret-token=abc123 rawGraphPayload {"access_token":"xyz"}', 'nested' => [ 'safe_reason' => 'Report was evaluated.', 'error' => 'Authorization: Bearer abc.def.ghi', 'payload' => ['full_report' => true], ], ], ); $payload = $evaluation->toStepPayload(); $encoded = json_encode($payload['proof_summary'], JSON_THROW_ON_ERROR); expect($payload['proof_summary'])->toHaveKey('label') ->and($payload['proof_summary'])->toHaveKey('nested') ->and($encoded)->not->toContain('raw_graph_response', 'access_token', 'Access token', 'ClientException', 'Authorization', 'Bearer', 'secret-token', 'rawGraphPayload', 'full_report'); }); it('only lets current usable operator-visible proof complete a step', function ( ResolutionProofCurrentness $currentness, ResolutionProofUsability $usability, ResolutionProofVisibility $visibility, bool $canComplete, ): void { $evaluation = new ResolutionProofEvaluation( actionKey: ReviewPublicationResolutionStepKey::GenerateReviewPack, subjectType: EnvironmentReview::class, subjectId: 123, status: ResolutionProofStatus::Available, currentness: $currentness, usability: $usability, visibility: $visibility, reasonCode: 'proof.test', ); expect($evaluation->canCompleteStep())->toBe($canComplete); })->with([ 'current usable visible' => [ ResolutionProofCurrentness::Current, ResolutionProofUsability::Usable, ResolutionProofVisibility::OperatorVisible, true, ], 'unknown usable visible' => [ ResolutionProofCurrentness::Unknown, ResolutionProofUsability::Usable, ResolutionProofVisibility::OperatorVisible, false, ], 'current inspection visible' => [ ResolutionProofCurrentness::Current, ResolutionProofUsability::InspectionOnly, ResolutionProofVisibility::OperatorVisible, false, ], 'current usable hidden' => [ ResolutionProofCurrentness::Current, ResolutionProofUsability::Usable, ResolutionProofVisibility::Hidden, false, ], 'current usable operator-limited' => [ ResolutionProofCurrentness::Current, ResolutionProofUsability::Usable, ResolutionProofVisibility::OperatorLimited, false, ], ]); it('requires available proof status before completing a step', function ( ResolutionProofStatus $status, bool $canComplete, ): void { $evaluation = new ResolutionProofEvaluation( actionKey: ReviewPublicationResolutionStepKey::GenerateReviewPack, subjectType: EnvironmentReview::class, subjectId: 123, status: $status, currentness: ResolutionProofCurrentness::Current, usability: ResolutionProofUsability::Usable, visibility: ResolutionProofVisibility::OperatorVisible, reasonCode: 'proof.test', ); expect($evaluation->canCompleteStep())->toBe($canComplete); })->with([ 'available proof completes' => [ ResolutionProofStatus::Available, true, ], 'running proof does not complete' => [ ResolutionProofStatus::Running, false, ], 'succeeded operation remains inspection-only' => [ ResolutionProofStatus::Succeeded, false, ], 'failed proof does not complete' => [ ResolutionProofStatus::Failed, false, ], ]);