diff --git a/apps/platform/app/Filament/Resources/RestoreRunResource.php b/apps/platform/app/Filament/Resources/RestoreRunResource.php index 290bcea3..cb7d2b1c 100644 --- a/apps/platform/app/Filament/Resources/RestoreRunResource.php +++ b/apps/platform/app/Filament/Resources/RestoreRunResource.php @@ -72,6 +72,7 @@ use Filament\Schemas\Components\Utilities\Set; use Filament\Schemas\Components\Wizard\Step; use Filament\Schemas\Schema; +use Filament\Support\Exceptions\Halt; use Filament\Tables; use Filament\Tables\Contracts\HasTable; use Filament\Tables\Filters\TrashedFilter; @@ -707,6 +708,49 @@ public static function getWizardSteps(): array ]), Step::make('Preview') ->description('Dry-run preview') + ->afterValidation(function (Get $get): void { + $state = static::wizardSafetyState(static::draftDataSnapshot($get)); + $previewIntegrity = $state['previewIntegrity'] ?? []; + $checksIntegrity = $state['checksIntegrity'] ?? []; + $executionReadiness = $state['executionReadiness'] ?? []; + + $previewIsCurrent = is_array($previewIntegrity) + && ($previewIntegrity['state'] ?? null) === PreviewIntegrityState::STATE_CURRENT; + $checksAreCurrent = is_array($checksIntegrity) + && ($checksIntegrity['state'] ?? null) === ChecksIntegrityState::STATE_CURRENT; + $executionAllowed = is_array($executionReadiness) + && (bool) ($executionReadiness['allowed'] ?? false); + + if (! $checksAreCurrent) { + Notification::make() + ->title('Safety checks required') + ->body('Run the safety checks for the current scope before proceeding to confirmation.') + ->warning() + ->send(); + + throw new Halt(); + } + + if (! $previewIsCurrent) { + Notification::make() + ->title('Preview required') + ->body('Generate a preview for the current scope before proceeding to confirmation.') + ->warning() + ->send(); + + throw new Halt(); + } + + if (! $executionAllowed) { + Notification::make() + ->title('Technical blocker present') + ->body('Resolve the technical blockers before proceeding to confirmation.') + ->danger() + ->send(); + + throw new Halt(); + } + }) ->schema([ Forms\Components\Hidden::make('preview_summary') ->default(null), @@ -793,11 +837,22 @@ public static function getWizardSteps(): array $policiesChanged = (int) ($summary['policies_changed'] ?? 0); $policiesTotal = (int) ($summary['policies_total'] ?? 0); + $previewBody = match (true) { + $policiesTotal <= 0 => 'No policies in scope.', + $policiesChanged <= 0 => 'No policy changes detected.', + $policiesChanged === 1 => '1 policy will be updated during execution.', + default => "{$policiesChanged} policies will be updated during execution.", + }; + $previewStatus = match (true) { + $policiesTotal <= 0 => 'info', + $policiesChanged > 0 => 'warning', + default => 'success', + }; Notification::make() ->title('Preview generated') - ->body("Policies: {$policiesChanged}/{$policiesTotal} changed") - ->status($policiesChanged > 0 ? 'warning' : 'success') + ->body($previewBody) + ->status($previewStatus) ->send(); }), Actions\Action::make('clear_restore_preview') diff --git a/apps/platform/app/Support/RestoreSafety/RestoreSafetyCopy.php b/apps/platform/app/Support/RestoreSafety/RestoreSafetyCopy.php index c811e99a..8fe5990e 100644 --- a/apps/platform/app/Support/RestoreSafety/RestoreSafetyCopy.php +++ b/apps/platform/app/Support/RestoreSafety/RestoreSafetyCopy.php @@ -26,6 +26,7 @@ public static function primaryNextAction(?string $action): string 'generate_preview' => 'Generate a preview for the current scope.', 'regenerate_preview' => 'Regenerate the preview for the current scope.', 'rerun_checks' => 'Run the safety checks again for the current scope.', + 'review_and_confirm' => 'Review the preview and complete confirmation before execution can be queued.', 'review_warnings' => 'Review the warnings before real execution.', 'execute' => 'Queue the real restore execution.', 'review_preview' => 'Review the preview evidence before claiming recovery or queueing execution.', diff --git a/apps/platform/resources/views/filament/forms/components/restore-run-preview.blade.php b/apps/platform/resources/views/filament/forms/components/restore-run-preview.blade.php index c3fcfc6d..587d9eb5 100644 --- a/apps/platform/resources/views/filament/forms/components/restore-run-preview.blade.php +++ b/apps/platform/resources/views/filament/forms/components/restore-run-preview.blade.php @@ -13,9 +13,15 @@ $checksIntegrity = $checksIntegrity ?? []; $checksIntegrity = is_array($checksIntegrity) ? $checksIntegrity : []; + $executionReadiness = $executionReadiness ?? []; + $executionReadiness = is_array($executionReadiness) ? $executionReadiness : []; + $safetyAssessment = $safetyAssessment ?? []; $safetyAssessment = is_array($safetyAssessment) ? $safetyAssessment : []; + $currentScope = $currentScope ?? []; + $currentScope = is_array($currentScope) ? $currentScope : []; + $ranAt = $ranAt ?? ($previewIntegrity['generated_at'] ?? null); $ranAtLabel = null; @@ -37,9 +43,24 @@ $assignmentsChanged = (int) ($summary['assignments_changed'] ?? 0); $scopeTagsChanged = (int) ($summary['scope_tags_changed'] ?? 0); $diffsOmitted = (int) ($summary['diffs_omitted'] ?? 0); + $integritySummary = $previewIntegrity['display_summary'] ?? 'Generate a preview before real execution.'; + $nextAction = $safetyAssessment['primary_next_action'] ?? 'generate_preview'; - $nextActionLabel = \App\Support\RestoreSafety\RestoreSafetyCopy::primaryNextAction(is_string($nextAction) ? $nextAction : 'generate_preview'); + $nextAction = is_string($nextAction) ? $nextAction : 'generate_preview'; + $previewIsCurrent = ($previewIntegrity['state'] ?? null) === 'current'; + $checksAreCurrent = ($checksIntegrity['state'] ?? null) === 'current'; + $executionAllowed = (bool) ($executionReadiness['allowed'] ?? false); + + if ($previewIsCurrent && $checksAreCurrent && $executionAllowed && $nextAction === 'execute') { + $nextAction = 'review_and_confirm'; + } + + $nextActionLabel = \App\Support\RestoreSafety\RestoreSafetyCopy::primaryNextAction($nextAction); + + $scopeMode = ($currentScope['scope_mode'] ?? null) === 'selected' ? 'selected' : 'all'; + $scopeLabel = $scopeMode === 'selected' ? 'All selected restore items' : 'All restore items'; + $limitedKeys = static function (array $items, int $limit = 8): array { $keys = array_keys($items); @@ -49,180 +70,400 @@ return array_slice($keys, 0, $limit); }; + + $sourceSelected = (int) ($currentScope['backup_set_id'] ?? 0) > 0; + $targetSelected = $executionReadiness !== []; + $validationComplete = $checksAreCurrent; + $previewComplete = $previewIsCurrent; + $technicalBlocked = ! $executionAllowed; + + $gatesTotal = 7; + $gatesComplete = count(array_filter([ + $sourceSelected, + $targetSelected, + $validationComplete, + $previewComplete, + ])); + + $nextGateLabel = match (true) { + ! $sourceSelected => 'Source required', + ! $targetSelected => 'Target required', + ! $validationComplete => 'Validation required', + ! $previewComplete => 'Preview required', + $technicalBlocked => 'Technical blocker present', + default => 'Confirmation required', + }; + + $executionLabel = $technicalBlocked ? 'Blocked' : 'Unavailable'; + $executionSummary = $technicalBlocked + ? 'Execution is blocked until the technical prerequisites are healthy again.' + : 'Execution is unavailable until required safety gates are complete.'; + + $gateTone = static function (string $status): string { + return match ($status) { + 'complete' => 'success', + 'required' => 'warning', + 'blocked' => 'danger', + default => 'gray', + }; + }; + + $gateBadge = static function (string $status): string { + return match ($status) { + 'complete' => 'Complete', + 'required' => 'Required', + 'blocked' => 'Blocked', + default => 'Unavailable', + }; + }; + + $gateStatus = static function (bool $complete, bool $required = false, bool $blocked = false): string { + if ($blocked) { + return 'blocked'; + } + + if ($complete) { + return 'complete'; + } + + return $required ? 'required' : 'unavailable'; + }; + + $safetyGates = [ + [ + 'step' => 1, + 'label' => 'Source selected', + 'summary' => 'A usable source backup is selected for this restore draft.', + 'status' => $gateStatus($sourceSelected, required: true), + ], + [ + 'step' => 2, + 'label' => 'Target selected', + 'summary' => 'Target environment is the route-bound managed environment.', + 'status' => $gateStatus($targetSelected, required: true), + ], + [ + 'step' => 3, + 'label' => 'Validation', + 'summary' => $validationComplete + ? 'Checks evidence is current for the selected restore scope.' + : 'Run checks for the current scope before confirmation.', + 'status' => $gateStatus($validationComplete, required: true), + ], + [ + 'step' => 4, + 'label' => 'Preview', + 'summary' => $previewComplete + ? 'Preview evidence is current for the selected restore scope.' + : 'Generate a preview for the current scope before confirmation.', + 'status' => $gateStatus($previewComplete, required: true), + ], + [ + 'step' => 5, + 'label' => 'Confirmation', + 'summary' => 'Explicit confirmation required before real execution.', + 'status' => $gateStatus(false, required: true, blocked: $technicalBlocked), + ], + [ + 'step' => 6, + 'label' => 'Execution', + 'summary' => $executionSummary, + 'status' => $gateStatus(false, blocked: $technicalBlocked), + ], + [ + 'step' => 7, + 'label' => 'Post-run evidence', + 'summary' => 'Post-run evidence is unavailable before execution.', + 'status' => 'unavailable', + ], + ]; @endphp -
- -
-
- - {{ $integritySpec->label }} - - @if (($checksIntegrity['state'] ?? null) === 'current') - - Checks current +
+
+ +
+
+ + {{ $integritySpec->label }} - @endif - @if (($safetyAssessment['state'] ?? null) === 'ready_with_caution') - - Calm readiness suppressed + @if (($checksIntegrity['state'] ?? null) === 'current') + + Checks current + + @endif + @if (($safetyAssessment['state'] ?? null) === 'ready_with_caution') + + Calm readiness suppressed + + @endif +
+ +
+
What the preview proves
+
{{ $integritySummary }}
+
+ Primary next step +
+
+ {{ $nextActionLabel }} +
+
+ +
+ + @if ($policiesTotal <= 0) + No policies in scope + @elseif ($policiesChanged <= 0) + No policy changes + @elseif ($policiesChanged === 1) + 1 policy will be updated + @else + {{ $policiesChanged }} policies will be updated + @endif - @endif -
- -
-
What the preview proves
-
{{ $integritySummary }}
-
- Primary next step -
-
- {{ $nextActionLabel }} -
-
- -
- - {{ $policiesChanged }}/{{ $policiesTotal }} policies changed - - - {{ $assignmentsChanged }} assignments changed - - - {{ $scopeTagsChanged }} scope tags changed - - @if ($diffsOmitted > 0) - {{ $diffsOmitted }} diffs omitted (limit) + {{ $policiesTotal }} {{ \Illuminate\Support\Str::plural('policy', $policiesTotal) }} reviewed - @endif -
- - @if (($previewIntegrity['invalidation_reasons'] ?? []) !== []) -
- Invalidated by: {{ implode(', ', array_map(static fn (string $reason): string => \Illuminate\Support\Str::replace('_', ' ', $reason), $previewIntegrity['invalidation_reasons'])) }} + + @if ($assignmentsChanged <= 0) + No assignment changes + @elseif ($assignmentsChanged === 1) + 1 assignment will be updated + @else + {{ $assignmentsChanged }} assignments will be updated + @endif + + + @if ($scopeTagsChanged <= 0) + No scope tag changes + @elseif ($scopeTagsChanged === 1) + 1 scope tag will be updated + @else + {{ $scopeTagsChanged }} scope tags will be updated + @endif + + @if ($diffsOmitted > 0) + + {{ $diffsOmitted }} diffs omitted (limit) + + @endif
- @endif -
-
- @if ($diffs === []) - -
- No preview diff is recorded for this scope yet. + @if (($previewIntegrity['invalidation_reasons'] ?? []) !== []) +
+ Invalidated by: {{ implode(', ', array_map(static fn (string $reason): string => \Illuminate\Support\Str::replace('_', ' ', $reason), $previewIntegrity['invalidation_reasons'])) }} +
+ @endif
- @else -
- @foreach ($diffs as $entry) - @php - $entry = is_array($entry) ? $entry : []; - $name = $entry['display_name'] ?? $entry['policy_identifier'] ?? 'Item'; - $type = $entry['policy_type'] ?? 'type'; - $platform = $entry['platform'] ?? 'platform'; - $action = $entry['action'] ?? 'update'; - $diff = is_array($entry['diff'] ?? null) ? $entry['diff'] : []; - $diffSummary = is_array($diff['summary'] ?? null) ? $diff['summary'] : []; - $added = (int) ($diffSummary['added'] ?? 0); - $removed = (int) ($diffSummary['removed'] ?? 0); - $changed = (int) ($diffSummary['changed'] ?? 0); - $assignmentsDelta = (bool) ($entry['assignments_changed'] ?? false); - $scopeTagsDelta = (bool) ($entry['scope_tags_changed'] ?? false); - $diffOmitted = (bool) ($entry['diff_omitted'] ?? false); - $diffTruncated = (bool) ($entry['diff_truncated'] ?? false); - $changedKeys = $limitedKeys(is_array($diff['changed'] ?? null) ? $diff['changed'] : []); - $addedKeys = $limitedKeys(is_array($diff['added'] ?? null) ? $diff['added'] : []); - $removedKeys = $limitedKeys(is_array($diff['removed'] ?? null) ? $diff['removed'] : []); - @endphp - -
- - {{ $action }} - - - {{ $added }} added - - - {{ $removed }} removed - - - {{ $changed }} changed - - @if ($assignmentsDelta) + @if ($diffs === []) + +
+ No preview diff is recorded for this scope yet. +
+
+ @else +
+ @foreach ($diffs as $entry) + @php + $entry = is_array($entry) ? $entry : []; + $nameRaw = $entry['display_name'] ?? $entry['policy_identifier'] ?? 'Item'; + $nameRaw = is_string($nameRaw) ? $nameRaw : 'Item'; + $name = (string) \Illuminate\Support\Str::of($nameRaw) + ->headline() + ->replaceMatches('/\\bbitlocker\\b/i', 'BitLocker'); + $action = $entry['action'] ?? 'update'; + $action = in_array($action, ['create', 'update'], true) ? $action : 'update'; + $diff = is_array($entry['diff'] ?? null) ? $entry['diff'] : []; + $diffSummary = is_array($diff['summary'] ?? null) ? $diff['summary'] : []; + $added = (int) ($diffSummary['added'] ?? 0); + $removed = (int) ($diffSummary['removed'] ?? 0); + $changed = (int) ($diffSummary['changed'] ?? 0); + $assignmentsDelta = (bool) ($entry['assignments_changed'] ?? false); + $scopeTagsDelta = (bool) ($entry['scope_tags_changed'] ?? false); + $diffOmitted = (bool) ($entry['diff_omitted'] ?? false); + $diffTruncated = (bool) ($entry['diff_truncated'] ?? false); + + $changedKeys = $limitedKeys(is_array($diff['changed'] ?? null) ? $diff['changed'] : []); + $addedKeys = $limitedKeys(is_array($diff['added'] ?? null) ? $diff['added'] : []); + $removedKeys = $limitedKeys(is_array($diff['removed'] ?? null) ? $diff['removed'] : []); + @endphp + + +
+ + {{ \Illuminate\Support\Str::headline((string) $action) }} + + + {{ $added }} added + + + {{ $removed }} removed + - assignments - - @endif - @if ($scopeTagsDelta) - - scope tags - - @endif - @if ($diffTruncated) - - truncated + {{ $changed }} changed + @if ($assignmentsDelta) + + assignments + + @endif + @if ($scopeTagsDelta) + + scope tags + + @endif + @if ($diffTruncated) + + truncated + + @endif +
+ +
+
+ Scope: + {{ $scopeLabel }} +
+
+ Change type: + {{ \Illuminate\Support\Str::headline((string) $action) }} +
+
+ Impact: + + @if ($added === 0 && $removed === 0 && $changed === 0 && ! $assignmentsDelta && ! $scopeTagsDelta) + No policy changes detected in preview. + @else + Changes detected in preview. They will apply during restore execution. + @endif + +
+
+ + @if ($diffOmitted) +
+ Diff details omitted due to preview limits. Narrow scope to see more items in detail. +
+ @elseif ($changedKeys !== [] || $addedKeys !== [] || $removedKeys !== []) +
+ @if ($changedKeys !== []) +
+
+ Changed keys (sample) +
+
    + @foreach ($changedKeys as $key) +
  • + {{ $key }} +
  • + @endforeach +
+
+ @endif + @if ($addedKeys !== []) +
+
+ Added keys (sample) +
+
    + @foreach ($addedKeys as $key) +
  • + {{ $key }} +
  • + @endforeach +
+
+ @endif + @if ($removedKeys !== []) +
+
+ Removed keys (sample) +
+
    + @foreach ($removedKeys as $key) +
  • + {{ $key }} +
  • + @endforeach +
+
+ @endif +
@endif +
+ @endforeach +
+ @endif +
+ +
+ +
+
+
+ {{ $gatesComplete }} of {{ $gatesTotal }} gates complete
+
+ Next gate: + {{ $nextGateLabel }} +
+
+ Execution: + {{ $executionLabel }} +
+
+ {{ $executionSummary }} +
+
- @if ($diffOmitted) -
- Diff details omitted due to preview limits. Narrow scope to see more items in detail. + + +
+ @foreach ($safetyGates as $gate) + @php + $status = (string) ($gate['status'] ?? 'unavailable'); + $tone = $gateTone($status); + $badge = $gateBadge($status); + @endphp +
+
+
+
+
+ {{ $gate['step'] ?? '•' }} +
+
+ {{ $gate['label'] ?? 'Gate' }} +
+
+
+ {{ $gate['summary'] ?? '' }} +
+
+ + + {{ $badge }} + +
- @elseif ($changedKeys !== [] || $addedKeys !== [] || $removedKeys !== []) -
- @if ($changedKeys !== []) -
-
- Changed keys (sample) -
-
    - @foreach ($changedKeys as $key) -
  • - {{ $key }} -
  • - @endforeach -
-
- @endif - @if ($addedKeys !== []) -
-
- Added keys (sample) -
-
    - @foreach ($addedKeys as $key) -
  • - {{ $key }} -
  • - @endforeach -
-
- @endif - @if ($removedKeys !== []) -
-
- Removed keys (sample) -
-
    - @foreach ($removedKeys as $key) -
  • - {{ $key }} -
  • - @endforeach -
-
- @endif -
- @endif - - @endforeach -
- @endif + @endforeach +
+
+
+
diff --git a/apps/platform/tests/Browser/Spec332RestoreRunWizardPreviewSmokeTest.php b/apps/platform/tests/Browser/Spec332RestoreRunWizardPreviewSmokeTest.php new file mode 100644 index 00000000..48b5c17f --- /dev/null +++ b/apps/platform/tests/Browser/Spec332RestoreRunWizardPreviewSmokeTest.php @@ -0,0 +1,109 @@ +browser()->timeout(30_000); + +uses(RefreshDatabase::class); + +function spec332RestoreWizardSmokeLoginUrl(User $user, ManagedEnvironment $tenant, string $redirect = ''): string +{ + return route('admin.local.smoke-login', array_filter([ + 'email' => $user->email, + 'tenant' => $tenant->external_id, + 'workspace' => $tenant->workspace->slug, + 'redirect' => $redirect, + ], static fn (?string $value): bool => filled($value))); +} + +it('keeps safety gates collapsed by default on the restore preview step', function (): void { + $tenant = ManagedEnvironment::factory()->create([ + 'rbac_status' => 'ok', + 'rbac_last_checked_at' => now(), + ]); + [$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner'); + ensureDefaultProviderConnection($tenant, 'microsoft'); + + $policy = Policy::create([ + 'managed_environment_id' => (int) $tenant->getKey(), + 'external_id' => 'policy-preview-1', + 'policy_type' => 'deviceConfiguration', + 'display_name' => 'Device Config Policy', + 'platform' => 'windows', + ]); + + PolicyVersion::create([ + 'managed_environment_id' => (int) $tenant->getKey(), + 'policy_id' => (int) $policy->getKey(), + 'version_number' => 1, + 'policy_type' => $policy->policy_type, + 'platform' => $policy->platform, + 'captured_at' => now(), + 'snapshot' => [ + 'foo' => 'current', + ], + 'metadata' => [], + 'assignments' => [], + 'scope_tags' => [], + ]); + + $backupSet = BackupSet::factory()->create([ + 'managed_environment_id' => (int) $tenant->getKey(), + 'name' => 'Spec332 Preview Backup', + 'status' => 'completed', + 'item_count' => 1, + ]); + + $backupItem = BackupItem::factory()->create([ + 'managed_environment_id' => (int) $tenant->getKey(), + 'backup_set_id' => (int) $backupSet->getKey(), + 'policy_id' => (int) $policy->getKey(), + 'policy_identifier' => $policy->external_id, + 'policy_type' => $policy->policy_type, + 'platform' => $policy->platform, + 'captured_at' => now(), + 'payload' => [ + 'foo' => 'backup', + ], + 'assignments' => [], + 'metadata' => [], + ]); + + bindFailHardGraphClient(); + + $redirectBase = RestoreRunResource::getUrl('create', panel: 'admin', tenant: $tenant); + $redirectPath = parse_url($redirectBase, PHP_URL_PATH) ?: '/admin'; + $redirect = $redirectPath + .'?backup_set_id='.(int) $backupSet->getKey() + .'&scope_mode=selected' + .'&backup_item_ids='.(int) $backupItem->getKey(); + + visit(spec332RestoreWizardSmokeLoginUrl($user, $tenant, $redirect)) + ->resize(1920, 1200) + ->waitForText('Select Backup Set') + ->assertNoJavaScriptErrors() + ->assertNoConsoleLogs() + ->click('Next') + ->waitForText('Define Restore Scope') + ->click('Next') + ->waitForText('Run checks') + ->click('Run checks') + ->waitForText('No group-based assignments detected.') + ->click('Next') + ->waitForText('Generate preview') + ->click('Generate preview') + ->waitForText('Policy change preview') + ->assertSee('Review the preview and complete confirmation before execution can be queued.') + ->assertSee('View safety gates') + ->assertDontSee('Hide safety gates') + ->assertSee('Execution: Unavailable'); +}); diff --git a/apps/platform/tests/Feature/Filament/RestoreRunPreviewProductizationTest.php b/apps/platform/tests/Feature/Filament/RestoreRunPreviewProductizationTest.php new file mode 100644 index 00000000..9a8a6f29 --- /dev/null +++ b/apps/platform/tests/Feature/Filament/RestoreRunPreviewProductizationTest.php @@ -0,0 +1,75 @@ +create([ + 'rbac_status' => 'ok', + 'rbac_last_checked_at' => now(), + ]); + [$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner'); + ensureDefaultProviderConnection($tenant, 'microsoft'); + + /** @var RestoreSafetyResolver $resolver */ + $resolver = app(RestoreSafetyResolver::class); + + $data = [ + 'backup_set_id' => 10, + 'scope_mode' => 'selected', + 'backup_item_ids' => [1], + 'group_mapping' => [], + 'check_summary' => ['blocking' => 0, 'warning' => 0, 'safe' => 1], + 'check_results' => [['code' => 'safe', 'severity' => 'safe']], + 'checks_ran_at' => now('UTC')->toIso8601String(), + 'preview_summary' => [ + 'generated_at' => now('UTC')->toIso8601String(), + 'policies_total' => 1, + 'policies_changed' => 0, + 'assignments_changed' => 0, + 'scope_tags_changed' => 0, + ], + 'preview_diffs' => [[ + 'policy_identifier' => 'policy-1', + 'display_name' => 'Bitlocker Require', + 'policy_type' => 'deviceCompliancePolicy', + 'platform' => 'all', + 'action' => 'update', + 'assignments_changed' => false, + 'scope_tags_changed' => false, + 'diff' => [ + 'summary' => ['added' => 0, 'removed' => 0, 'changed' => 0], + 'changed' => [], + 'added' => [], + 'removed' => [], + ], + ]], + 'preview_ran_at' => now('UTC')->toIso8601String(), + ]; + + $data['check_basis'] = $resolver->checksBasisFromData($data); + $data['preview_basis'] = $resolver->previewBasisFromData($data); + $data = App\Filament\Resources\RestoreRunResource::synchronizeRestoreSafetyDraft($data); + + setAdminPanelContext($tenant); + + Livewire::actingAs($user) + ->test(CreateRestoreRun::class) + ->set('data', $data) + ->goToWizardStep(4) + ->assertSeeText('Review the preview and complete confirmation before execution can be queued.') + ->assertDontSeeText('Resolve the technical blockers before real execution.') + ->assertSeeText('Policy change preview') + ->assertSeeText('BitLocker Require') + ->assertSeeText('No policy changes') + ->assertSeeText('1 policy reviewed') + ->assertDontSeeText('deviceCompliancePolicy') + ->assertDontSeeText('deviceCompliancePolicy • all'); +});