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. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #422
246 lines
8.6 KiB
PHP
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,
|
|
]);
|
|
}
|