Implemented the consolidated operator guidance panel for the environment dashboard. Updated EnvironmentDashboardSummaryBuilder to prioritize and select guidance based on the operator guidance contract. Added comprehensive unit, feature, and browser tests to verify the guidance selection logic and UI rendering. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #423
194 lines
8.4 KiB
PHP
194 lines
8.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Filament\Resources\EnvironmentReviewResource;
|
|
use App\Models\ManagedEnvironment;
|
|
use App\Models\OperationRun;
|
|
use App\Models\ReviewPack;
|
|
use App\Models\User;
|
|
use App\Services\EnvironmentReviews\EnvironmentReviewLifecycleService;
|
|
use App\Services\Intune\ManagedEnvironmentRequiredPermissionsViewModelBuilder;
|
|
use App\Support\EnvironmentDashboard\EnvironmentDashboardSummaryBuilder;
|
|
use App\Support\EnvironmentReviewStatus;
|
|
use App\Support\Links\RequiredPermissionsLinks;
|
|
use App\Support\OperationRunLinks;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
|
|
use function Pest\Laravel\mock;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
function mockSpec352DashboardPermissions(array $overview = []): void
|
|
{
|
|
mock(ManagedEnvironmentRequiredPermissionsViewModelBuilder::class, function ($mock) use ($overview): void {
|
|
$mock->shouldReceive('build')->andReturn([
|
|
'overview' => array_replace_recursive([
|
|
'overall' => 'ready',
|
|
'counts' => [
|
|
'missing_application' => 0,
|
|
'missing_delegated' => 0,
|
|
],
|
|
'freshness' => [
|
|
'is_stale' => false,
|
|
'last_refreshed_at' => now()->toIso8601String(),
|
|
],
|
|
], $overview),
|
|
]);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @return array{
|
|
* review:\App\Models\EnvironmentReview,
|
|
* successor:\App\Models\EnvironmentReview|null,
|
|
* reviewPack:ReviewPack,
|
|
* }
|
|
*/
|
|
function spec352SeedBlockedReviewOutput(ManagedEnvironment $environment, User $user, bool $withSuccessorDraft = false): array
|
|
{
|
|
$snapshot = seedPartialEnvironmentReviewEvidence($environment, findingCount: 0, driftCount: 0, operationRunCount: 0);
|
|
$review = composeEnvironmentReviewForTest($environment, $user, $snapshot);
|
|
$review->forceFill([
|
|
'status' => EnvironmentReviewStatus::Published->value,
|
|
'published_at' => now()->subHour(),
|
|
'published_by_user_id' => (int) $user->getKey(),
|
|
'summary' => array_replace_recursive(is_array($review->summary) ? $review->summary : [], [
|
|
'publish_blockers' => ['Operator approval note is still missing.'],
|
|
]),
|
|
])->save();
|
|
|
|
$reviewPack = ReviewPack::factory()->ready()->create([
|
|
'managed_environment_id' => (int) $environment->getKey(),
|
|
'workspace_id' => (int) $environment->workspace_id,
|
|
'environment_review_id' => (int) $review->getKey(),
|
|
'evidence_snapshot_id' => (int) $snapshot->getKey(),
|
|
'initiated_by_user_id' => (int) $user->getKey(),
|
|
'file_path' => 'review-packs/spec352-review-output.zip',
|
|
'file_disk' => 'exports',
|
|
'generated_at' => now()->subMinutes(10),
|
|
'options' => [
|
|
'include_pii' => false,
|
|
'include_operations' => true,
|
|
],
|
|
]);
|
|
|
|
$review->forceFill([
|
|
'current_export_review_pack_id' => (int) $reviewPack->getKey(),
|
|
])->save();
|
|
|
|
$successor = null;
|
|
|
|
if ($withSuccessorDraft) {
|
|
$successor = app(EnvironmentReviewLifecycleService::class)->createNextReview($review->fresh(), $user, $snapshot);
|
|
}
|
|
|
|
return [
|
|
'review' => $review->fresh(),
|
|
'successor' => $successor?->fresh(),
|
|
'reviewPack' => $reviewPack->fresh(),
|
|
];
|
|
}
|
|
|
|
it('prioritizes provider blockers over review output guidance when both are present', function (): void {
|
|
[$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
|
|
|
|
mockSpec352DashboardPermissions([
|
|
'overall' => 'blocked',
|
|
'counts' => [
|
|
'missing_application' => 1,
|
|
'missing_delegated' => 0,
|
|
],
|
|
]);
|
|
|
|
spec352SeedBlockedReviewOutput($environment, $user, withSuccessorDraft: true);
|
|
|
|
$summary = app(EnvironmentDashboardSummaryBuilder::class)
|
|
->build($environment, $user)
|
|
->toArray();
|
|
|
|
expect(data_get($summary, 'operatorGuidance.key'))->toBe('provider_readiness.required_permissions')
|
|
->and(data_get($summary, 'operatorGuidance.title'))->toBe(__('localization.dashboard.overview.operator_guidance_provider_blocked_title'))
|
|
->and(data_get($summary, 'operatorGuidance.actionLabel'))->toBe('Review permissions')
|
|
->and(data_get($summary, 'operatorGuidance.actionUrl'))->toBe(RequiredPermissionsLinks::requiredPermissions($environment))
|
|
->and(data_get($summary, 'readinessDecision.actionLabel'))->toBe('Review permissions')
|
|
->and(data_get($summary, 'readinessDecision.helperText'))->toBeNull();
|
|
});
|
|
|
|
it('surfaces review output guidance with open draft review when provider blockers are absent', function (): void {
|
|
[$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
|
|
|
|
mockSpec352DashboardPermissions();
|
|
|
|
$reviewState = spec352SeedBlockedReviewOutput($environment, $user, withSuccessorDraft: true);
|
|
$successor = $reviewState['successor'];
|
|
|
|
expect($successor)->not->toBeNull();
|
|
|
|
$summary = app(EnvironmentDashboardSummaryBuilder::class)
|
|
->build($environment, $user)
|
|
->toArray();
|
|
|
|
expect(data_get($summary, 'operatorGuidance.key'))->toBe('review_output.publication_blocked')
|
|
->and(data_get($summary, 'operatorGuidance.title'))->toBe(__('localization.review.draft_review_exists'))
|
|
->and(data_get($summary, 'operatorGuidance.actionLabel'))->toBe('Open draft review')
|
|
->and(data_get($summary, 'operatorGuidance.actionUrl'))->toContain('/environment-reviews/'.($successor?->getKey()))
|
|
->and(data_get($summary, 'readinessDecision.helperText'))->toBeNull()
|
|
->and(collect(data_get($summary, 'operatorGuidance.secondaryActions', []))->pluck('actionLabel')->intersect([
|
|
'Inspect review blockers',
|
|
'Open evidence basis',
|
|
'Open customer workspace',
|
|
])->isNotEmpty())->toBeTrue();
|
|
});
|
|
|
|
it('falls back to repo-backed operations attention when higher-priority guidance is absent', function (): void {
|
|
[$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
|
|
|
|
mockSpec352DashboardPermissions();
|
|
|
|
OperationRun::factory()->create([
|
|
'managed_environment_id' => (int) $environment->getKey(),
|
|
'workspace_id' => (int) $environment->workspace_id,
|
|
'type' => 'inventory_sync',
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Failed->value,
|
|
'completed_at' => now()->subMinutes(5),
|
|
]);
|
|
|
|
$summary = app(EnvironmentDashboardSummaryBuilder::class)
|
|
->build($environment, $user)
|
|
->toArray();
|
|
|
|
expect(data_get($summary, 'operatorGuidance.key'))->toBe('recommended_action.operations_requiring_attention')
|
|
->and(data_get($summary, 'operatorGuidance.title'))->toBe(__('localization.dashboard.overview.operator_guidance_operations_title'))
|
|
->and(data_get($summary, 'operatorGuidance.actionLabel'))->toBe('Review operations')
|
|
->and(data_get($summary, 'operatorGuidance.actionUrl'))->toBe(OperationRunLinks::index(
|
|
$environment,
|
|
activeTab: OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP,
|
|
problemClass: OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP,
|
|
));
|
|
});
|
|
|
|
it('renders a calm no-urgent-action fallback when no dominant case exists', function (): void {
|
|
[$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
|
|
|
|
mockSpec352DashboardPermissions();
|
|
|
|
workspaceOverviewSeedQuietTenantTruth($environment);
|
|
$backupSet = workspaceOverviewSeedHealthyBackup($environment);
|
|
workspaceOverviewSeedRestoreHistory($environment, $backupSet, 'completed');
|
|
|
|
$summary = app(EnvironmentDashboardSummaryBuilder::class)
|
|
->build($environment, $user)
|
|
->toArray();
|
|
|
|
expect(data_get($summary, 'recommendedActions'))->toBe([])
|
|
->and(data_get($summary, 'operatorGuidance.key'))->toBe('environment.no_urgent_action')
|
|
->and(data_get($summary, 'operatorGuidance.title'))->toBe(__('localization.dashboard.overview.operator_guidance_no_urgent_title'))
|
|
->and(data_get($summary, 'operatorGuidance.actionLabel'))->toBe(__('localization.dashboard.overview.action_review_environment'))
|
|
->and(data_get($summary, 'operatorGuidance.actionUrl'))->toBe(EnvironmentReviewResource::environmentScopedUrl('index', tenant: $environment))
|
|
->and(data_get($summary, 'readinessDecision.helperText'))->toBeNull();
|
|
});
|