'/admin/reviews/41', 'evidence' => '/admin/evidence/8', 'successor_review' => '/admin/reviews/42', ], execution: [ 'can_manage_review' => true, ], ); expect($mapped['primary_action']['key'])->toBe('open_successor_review') ->and($mapped['primary_action']['type'])->toBe(ResolutionAction::TYPE_NAVIGATION) ->and($mapped['primary_action']['action_name'])->toBe('open_successor_review') ->and($mapped['primary_action']['url'])->toBe('/admin/reviews/42'); }); it('labels a known mutable successor as open draft review on the workspace', function (): void { $review = spec351ReviewModel('published'); $guidance = spec351Guidance( reviewStatus: 'published', publishBlockers: ['Operator approval note is still missing.'], hasReadyExport: true, ); $mapped = ReviewOutputResolveActionMapper::map( review: $review, guidance: $guidance, sourceSurface: 'customer_review_workspace', urls: [ 'review' => '/admin/reviews/41', 'evidence' => '/admin/evidence/8', 'successor_review' => '/admin/reviews/42', ], execution: [ 'can_manage_review' => true, 'successor_review_status' => 'draft', ], ); expect($mapped['primary_action']['key'])->toBe('open_successor_review') ->and($mapped['primary_action']['label'])->toBe('Open draft review') ->and($mapped['primary_action']['action_name'])->toBeNull() ->and($mapped['primary_action']['url'])->toBe('/admin/reviews/42'); }); it('prefers create next review for published blocked workspace output when execution is safe', function (): void { $review = spec351ReviewModel('published'); $guidance = spec351Guidance( reviewStatus: 'published', publishBlockers: ['Operator approval note is still missing.'], hasReadyExport: true, ); $mapped = ReviewOutputResolveActionMapper::map( review: $review, guidance: $guidance, sourceSurface: 'customer_review_workspace', urls: [ 'review' => '/admin/reviews/41', 'evidence' => '/admin/evidence/8', ], execution: [ 'can_manage_review' => true, ], ); expect($mapped['primary_action']['key'])->toBe('create_next_review') ->and($mapped['primary_action']['type'])->toBe(ResolutionAction::TYPE_OPERATION_ACTION) ->and($mapped['primary_action']['action_name'])->toBe('createNextReview') ->and($mapped['secondary_actions'][0]['key'])->toBe('resolve_review_blockers'); }); it('falls back honestly when published blocked output cannot execute on the current surface', function (): void { $review = spec351ReviewModel('published'); $guidance = spec351Guidance( reviewStatus: 'published', publishBlockers: ['Operator approval note is still missing.'], hasReadyExport: true, ); $mapped = ReviewOutputResolveActionMapper::map( review: $review, guidance: $guidance, sourceSurface: 'environment_review_detail.customer_workspace', urls: [ 'review' => '/admin/reviews/41', 'evidence' => '/admin/evidence/8', ], execution: [ 'can_manage_review' => true, ], ); expect($mapped['primary_action']['key'])->toBe('resolve_review_blockers') ->and($mapped['primary_action']['type'])->toBe(ResolutionAction::TYPE_NAVIGATION) ->and($mapped['primary_action']['action_name'])->toBeNull(); }); it('prefers refresh review for mutable blocked detail when execution is allowed', function (): void { $review = spec351ReviewModel('draft'); $guidance = spec351Guidance( reviewStatus: 'draft', evidenceState: EnvironmentReviewCompletenessState::Partial->value, hasReadyExport: false, ); $mapped = ReviewOutputResolveActionMapper::map( review: $review, guidance: $guidance, sourceSurface: 'environment_review_detail', urls: [ 'review' => '/admin/reviews/41', 'evidence' => '/admin/evidence/8', ], execution: [ 'can_manage_review' => true, ], ); expect($mapped['primary_action']['key'])->toBe('refresh_review') ->and($mapped['primary_action']['type'])->toBe(ResolutionAction::TYPE_OPERATION_ACTION) ->and($mapped['primary_action']['action_name'])->toBe('refresh_review'); }); it('prefers publish review for ready mutable detail when execution is allowed', function (): void { $review = spec351ReviewModel('ready'); $guidance = spec351Guidance( reviewStatus: 'ready', hasReadyExport: false, ); $mapped = ReviewOutputResolveActionMapper::map( review: $review, guidance: $guidance, sourceSurface: 'environment_review_detail', urls: [ 'review' => '/admin/reviews/41', 'evidence' => '/admin/evidence/8', ], execution: [ 'can_manage_review' => true, ], ); expect($mapped['primary_action']['key'])->toBe('publish_review') ->and($mapped['primary_action']['type'])->toBe(ResolutionAction::TYPE_DOMAIN_ACTION) ->and($mapped['primary_action']['action_name'])->toBe('publish_review'); }); it('keeps a none fallback for unsupported no-link states', function (): void { $review = spec351ReviewModel('archived'); $guidance = ReviewPackOutputResolutionGuidance::fromReadiness([ 'review_status' => 'archived', 'readiness_state' => 'unknown', 'primary_reason' => 'review_output_limitations', 'has_ready_export' => false, 'contains_pii' => false, 'limitations' => [], 'section_summary' => [], 'section_state_counts' => [], 'evidence_completeness_state' => EnvironmentReviewCompletenessState::Missing->value, 'required_section_state_counts' => [], ], []); $mapped = ReviewOutputResolveActionMapper::map( review: $review, guidance: $guidance, sourceSurface: 'environment_review_detail', urls: [], execution: [ 'can_manage_review' => false, ], ); expect($mapped['primary_action']['type'])->toBe(ResolutionAction::TYPE_NONE) ->and($mapped['secondary_actions'])->toBeEmpty(); }); function spec351ReviewModel(string $status): EnvironmentReview { $review = new EnvironmentReview; $review->forceFill([ 'id' => 41, 'workspace_id' => 1, 'managed_environment_id' => 7, 'status' => $status, ]); return $review; } function spec351Guidance( string $reviewStatus, array $publishBlockers = [], bool $hasReadyExport = false, string $evidenceState = EnvironmentReviewCompletenessState::Complete->value, ): array { $readiness = ReviewPackOutputReadiness::derive( reviewStatus: $reviewStatus, reviewCompletenessState: EnvironmentReviewCompletenessState::Complete->value, evidenceCompletenessState: $evidenceState, sectionStateCounts: [ EnvironmentReviewCompletenessState::Complete->value => 5, ], requiredSectionCount: 5, requiredSectionStateCounts: [ EnvironmentReviewCompletenessState::Complete->value => 5, ], publishBlockers: $publishBlockers, hasReadyExport: $hasReadyExport, includePii: false, protectedValuesHidden: true, disclosurePresent: true, ); return ReviewPackOutputResolutionGuidance::fromReadiness($readiness, [ 'review' => '/admin/reviews/41', 'evidence' => '/admin/evidence/8', 'operation' => '/admin/operations/9', 'download' => $hasReadyExport ? '/admin/review-packs/8/download' : null, ]); }