TenantAtlas/apps/platform/tests/Unit/ResolutionGuidance/Spec351ReviewOutputResolveActionMapperTest.php
Ahmed Darrazi a6ff903093
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 3m45s
feat: review output resolve actions v1 (spec 351)
Implemented the first version of review output resolve actions. Included a ReviewOutputResolveActionMapper, commands to seed browser fixtures, updated CustomerReviewWorkspace, EnvironmentReviewResource, UI enforcement, and related views. Also added extensive unit, feature, and browser tests, and updated the design coverage matrix.
2026-06-04 02:48:18 +02:00

246 lines
8.6 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\EnvironmentReview;
use App\Support\EnvironmentReviewCompletenessState;
use App\Support\ResolutionGuidance\ResolutionAction;
use App\Support\ResolutionGuidance\ReviewOutputResolveActionMapper;
use App\Support\ReviewPacks\ReviewPackOutputReadiness;
use App\Support\ReviewPacks\ReviewPackOutputResolutionGuidance;
it('prefers a known successor review when one is concretely available', 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',
urls: [
'review' => '/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,
]);
}