OperationRun::query()->count(), 'review_publication_resolution_cases' => ReviewPublicationResolutionCase::query()->count(), 'review_publication_resolution_steps' => ReviewPublicationResolutionStep::query()->count(), ]; } /** * @param array{operation_runs:int, review_publication_resolution_cases:int, review_publication_resolution_steps:int} $expected */ function spec390ExpectGuidanceSideEffectCounts(array $expected): void { expect(spec390GuidanceSideEffectCounts())->toBe($expected); } function spec390BackupSetWithItem(ManagedEnvironment $tenant): BackupSet { $policy = Policy::create([ 'managed_environment_id' => (int) $tenant->getKey(), 'external_id' => 'spec390-policy', 'policy_type' => 'deviceConfiguration', 'display_name' => 'Spec390 Policy', 'platform' => 'windows', 'metadata' => [], ]); $backupSet = BackupSet::factory()->create([ 'managed_environment_id' => (int) $tenant->getKey(), 'name' => 'Spec390 Backup', 'status' => 'completed', 'item_count' => 1, ]); 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, 'payload' => [ 'id' => $policy->external_id, 'displayName' => 'Spec390 Policy', 'settings' => ['enabled' => true], ], 'assignments' => [], 'metadata' => [], ]); return $backupSet; } /** * @return array */ function spec390ReadyWizardData(BackupSet $backupSet): array { /** @var RestoreSafetyResolver $safety */ $safety = app(RestoreSafetyResolver::class); $data = [ 'backup_set_id' => (int) $backupSet->getKey(), 'scope_mode' => 'all', 'backup_item_ids' => [], '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], 'preview_diffs' => [['policy_identifier' => 'spec390-policy']], 'preview_ran_at' => now('UTC')->toIso8601String(), 'is_dry_run' => true, ]; $data['check_basis'] = $safety->checksBasisFromData($data); $data['preview_basis'] = $safety->previewBasisFromData($data); return RestoreRunResource::synchronizeRestoreSafetyDraft($data); } it('adds blocked readiness guidance to the restore create presenter', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $this->actingAs($user); ensureDefaultProviderConnection($tenant, 'microsoft'); $backupSet = spec390BackupSetWithItem($tenant); $sideEffectCounts = spec390GuidanceSideEffectCounts(); $contract = RestoreRunCreatePresenter::contract( data: [ 'backup_set_id' => (int) $backupSet->getKey(), 'scope_mode' => 'all', 'backup_item_ids' => [], 'group_mapping' => [], ], currentStep: 3, compactFlow: true, tenant: $tenant, user: $user, ); expect(data_get($contract, 'readinessGuidance.state'))->toBe(RestoreReadinessState::Blocked->value) ->and(data_get($contract, 'readinessGuidance.reason'))->toBe(RestoreReadinessReason::ChecksNotRun->value) ->and(data_get($contract, 'readinessGuidance.nextAction'))->toBe(RestoreReadinessAction::RunReadinessChecks->value) ->and(data_get($contract, 'readinessGuidance.actionSafetyCopy'))->toBe('This will not execute the restore.'); spec390ExpectGuidanceSideEffectCounts($sideEffectCounts); }); it('adds ready-for-confirmation guidance to the restore create presenter', function (): void { $tenant = ManagedEnvironment::factory()->create([ 'rbac_status' => 'ok', 'rbac_last_checked_at' => now(), ]); [$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner'); $this->actingAs($user); ensureDefaultProviderConnection($tenant, 'microsoft'); $backupSet = spec390BackupSetWithItem($tenant); $contract = RestoreRunCreatePresenter::contract( data: spec390ReadyWizardData($backupSet), currentStep: 5, compactFlow: true, tenant: $tenant, user: $user, ); expect(data_get($contract, 'readinessGuidance.state'))->toBe(RestoreReadinessState::ReadyForConfirmation->value) ->and(data_get($contract, 'readinessGuidance.nextAction'))->toBe(RestoreReadinessAction::ContinueToConfirmation->value) ->and(data_get($contract, 'readinessGuidance.actionSafetyCopy'))->toBe('The restore still requires final confirmation before execution.'); }); it('blocks create presenter readiness when execution prerequisites are unavailable', function (): void { $tenant = ManagedEnvironment::factory()->create([ 'rbac_status' => 'ok', 'rbac_last_checked_at' => now(), ]); [$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner'); $this->actingAs($user); ensureDefaultProviderConnection($tenant, 'microsoft', ensureCredential: false); $backupSet = spec390BackupSetWithItem($tenant); $contract = RestoreRunCreatePresenter::contract( data: spec390ReadyWizardData($backupSet), currentStep: 5, compactFlow: true, tenant: $tenant, user: $user, ); expect(data_get($contract, 'readinessGuidance.state'))->toBe(RestoreReadinessState::Blocked->value) ->and(data_get($contract, 'readinessGuidance.reason'))->toBe(RestoreReadinessReason::ExecutionPrerequisiteBlocked->value) ->and(data_get($contract, 'readinessGuidance.nextAction'))->toBe(RestoreReadinessAction::ReviewValidationBlockers->value) ->and(data_get($contract, 'wizardGate.execution_state'))->toBe('unavailable_until_prerequisites'); }); it('renders persisted restore-run readiness guidance on the view page', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $this->actingAs($user); Filament::setTenant($tenant, true); $backupSet = spec390BackupSetWithItem($tenant); $operationRun = OperationRun::factory()->forTenant($tenant)->create([ 'type' => OperationRunType::RestoreExecute->value, 'status' => OperationRunStatus::Running->value, ]); $restoreRun = RestoreRun::factory()->for($tenant, 'tenant')->for($backupSet)->create([ 'status' => RestoreRunStatus::Running->value, 'operation_run_id' => (int) $operationRun->getKey(), ]); $sideEffectCounts = spec390GuidanceSideEffectCounts(); Livewire::test(ViewRestoreRun::class, ['record' => $restoreRun->getKey()]) ->assertSee('Restore readiness') ->assertSee('Restore execution is in progress.') ->assertSee('Next safe action: Open operation') ->assertSee('This guidance only opens existing execution evidence.'); spec390ExpectGuidanceSideEffectCounts($sideEffectCounts); }); it('lets readonly users inspect persisted readiness guidance but keeps create mutations forbidden', function (): void { $tenant = ManagedEnvironment::factory()->create(); [$user] = createUserWithTenant($tenant, role: 'readonly'); $this->actingAs($user); Filament::setTenant($tenant, true); $backupSet = spec390BackupSetWithItem($tenant); $restoreRun = RestoreRun::factory()->for($tenant, 'tenant')->for($backupSet)->previewOnly()->create(); Livewire::actingAs($user) ->test(ViewRestoreRun::class, ['record' => $restoreRun->getKey()]) ->assertSee('Restore readiness') ->assertSee("Restore can't continue yet."); $this->actingAs($user) ->get(RestoreRunResource::getUrl('create', panel: 'admin', tenant: $tenant)) ->assertForbidden(); }); it('preserves final execution confirmation and safety gates', function (): void { $tenant = ManagedEnvironment::factory()->create([ 'rbac_status' => 'ok', 'rbac_last_checked_at' => now(), ]); [$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner'); $this->actingAs($user); Filament::setTenant($tenant, true); ensureDefaultProviderConnection($tenant, 'microsoft'); $backupSet = spec390BackupSetWithItem($tenant); $sideEffectCounts = spec390GuidanceSideEffectCounts(); $this->mock(RestoreService::class, function (MockInterface $mock): void { $mock->shouldNotReceive('preview'); $mock->shouldNotReceive('execute'); }); Livewire::test(CreateRestoreRun::class) ->fillForm([ ...spec390ReadyWizardData($backupSet), 'is_dry_run' => false, 'acknowledged_impact' => false, 'tenant_confirm' => null, ]) ->call('create') ->assertHasFormErrors(['acknowledged_impact']); spec390ExpectGuidanceSideEffectCounts($sideEffectCounts); });