forTenant($tenant)->create([ 'user_id' => (int) $user->getKey(), 'type' => 'environment.review.compose', 'status' => OperationRunStatus::Completed->value, 'outcome' => OperationRunOutcome::Succeeded->value, ]); EnvironmentReview::factory()->ready()->create([ 'workspace_id' => (int) $tenant->workspace_id, 'managed_environment_id' => (int) $tenant->getKey(), 'evidence_snapshot_id' => (int) $snapshot->getKey(), 'initiated_by_user_id' => (int) $user->getKey(), 'operation_run_id' => (int) $run->getKey(), ]); $decision = app(OperationRunActionEligibility::class)->forRun($run->fresh(), $user); expect(data_get($decision, 'primary_action.key'))->toBe('view_review') ->and(data_get($decision, 'primary_action.label'))->toBe(__('localization.operations.actions.view_review')) ->and(data_get($decision, 'primary_action.requires_confirmation'))->toBeFalse(); }); it('prefers the related evidence snapshot as the primary safe action in Spec365', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $run = OperationRun::factory()->forTenant($tenant)->create([ 'user_id' => (int) $user->getKey(), 'type' => 'tenant.evidence.snapshot.generate', 'status' => OperationRunStatus::Completed->value, 'outcome' => OperationRunOutcome::Succeeded->value, ]); EvidenceSnapshot::query()->create([ 'workspace_id' => (int) $tenant->workspace_id, 'managed_environment_id' => (int) $tenant->getKey(), 'operation_run_id' => (int) $run->getKey(), 'initiated_by_user_id' => (int) $user->getKey(), 'fingerprint' => hash('sha256', 'spec365-evidence-'.$run->getKey()), 'status' => EvidenceSnapshotStatus::Active->value, 'completeness_state' => EvidenceCompletenessState::Complete->value, 'summary' => ['source' => 'spec365'], 'generated_at' => now(), ]); $decision = app(OperationRunActionEligibility::class)->forRun($run->fresh(), $user); expect(data_get($decision, 'primary_action.key'))->toBe('view_evidence') ->and(data_get($decision, 'primary_action.label'))->toBe(__('localization.operations.actions.view_evidence')); }); it('prefers the related review pack as the primary safe action in Spec365', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $run = OperationRun::factory()->forTenant($tenant)->create([ 'user_id' => (int) $user->getKey(), 'type' => 'environment.review_pack.generate', 'status' => OperationRunStatus::Completed->value, 'outcome' => OperationRunOutcome::Succeeded->value, ]); ReviewPack::factory()->ready()->create([ 'workspace_id' => (int) $tenant->workspace_id, 'managed_environment_id' => (int) $tenant->getKey(), 'operation_run_id' => (int) $run->getKey(), 'initiated_by_user_id' => (int) $user->getKey(), ]); $decision = app(OperationRunActionEligibility::class)->forRun($run->fresh(), $user); expect(data_get($decision, 'primary_action.key'))->toBe('view_report') ->and(data_get($decision, 'primary_action.label'))->toBe(__('localization.operations.actions.view_report')); }); it('maps partial inventory sync runs to affected-family drilldown in Spec365', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $run = OperationRun::factory()->forTenant($tenant)->create([ 'user_id' => (int) $user->getKey(), 'type' => 'inventory.sync', 'status' => OperationRunStatus::Completed->value, 'outcome' => OperationRunOutcome::PartiallySucceeded->value, ]); $decision = app(OperationRunActionEligibility::class)->forRun($run->fresh(), $user); expect(data_get($decision, 'primary_action.key'))->toBe('view_affected_families') ->and(data_get($decision, 'primary_action.label'))->toBe(__('localization.operations.actions.view_affected_families')); }); it('maps blocked backup executions with backup truth to backup details in Spec365', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $backupSet = BackupSet::factory()->for($tenant)->create(['status' => 'completed']); $run = OperationRun::factory()->forTenant($tenant)->create([ 'user_id' => (int) $user->getKey(), 'type' => 'backup.schedule.execute', 'status' => OperationRunStatus::Completed->value, 'outcome' => OperationRunOutcome::Blocked->value, 'context' => [ 'backup_set_id' => (int) $backupSet->getKey(), ], ]); $decision = app(OperationRunActionEligibility::class)->forRun($run->fresh(), $user); expect(data_get($decision, 'primary_action.key'))->toBe('view_backup_details') ->and(data_get($decision, 'primary_action.label'))->toBe(__('localization.operations.actions.view_backup_details')); }); it('prefers restore details over mutation actions for high-risk restore runs in Spec365', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $backupSet = BackupSet::factory()->for($tenant)->create(['status' => 'completed']); $run = OperationRun::factory()->forTenant($tenant)->create([ 'user_id' => (int) $user->getKey(), 'type' => 'restore.execute', 'status' => OperationRunStatus::Queued->value, 'outcome' => OperationRunOutcome::Pending->value, 'created_at' => now()->subMinutes(10), ]); $restoreRun = RestoreRun::withoutEvents(fn (): RestoreRun => RestoreRun::factory() ->for($tenant, 'tenant') ->for($backupSet) ->create([ 'workspace_id' => (int) $tenant->workspace_id, 'operation_run_id' => (int) $run->getKey(), 'status' => RestoreRunStatus::Completed->value, 'is_dry_run' => false, ])); $run->forceFill([ 'context' => [ 'restore_run_id' => (int) $restoreRun->getKey(), 'backup_set_id' => (int) $backupSet->getKey(), ], ])->save(); $decision = app(OperationRunActionEligibility::class)->forRun($run->fresh(), $user); expect($decision['high_risk'])->toBeTrue() ->and(data_get($decision, 'primary_action.key'))->toBe('view_restore_details') ->and(data_get($decision, 'primary_action.color'))->toBe('warning') ->and(data_get($decision, 'primary_action.requires_confirmation'))->toBeFalse() ->and(spec365OperationRunPrimaryActionKeys($decision['secondary_actions']))->toContain('reconcile'); }); it('keeps failed restore runs on restore details without unsafe high-risk actions in Spec365', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $backupSet = BackupSet::factory()->for($tenant)->create(['status' => 'completed']); $run = OperationRun::factory()->forTenant($tenant)->create([ 'user_id' => (int) $user->getKey(), 'type' => 'restore.execute', 'status' => OperationRunStatus::Completed->value, 'outcome' => OperationRunOutcome::Failed->value, ]); $restoreRun = RestoreRun::withoutEvents(fn (): RestoreRun => RestoreRun::factory() ->failedOutcome() ->for($tenant, 'tenant') ->for($backupSet) ->create([ 'workspace_id' => (int) $tenant->workspace_id, 'operation_run_id' => (int) $run->getKey(), 'status' => RestoreRunStatus::Failed->value, ])); $run->forceFill([ 'context' => [ 'restore_run_id' => (int) $restoreRun->getKey(), 'backup_set_id' => (int) $backupSet->getKey(), ], ])->save(); $decision = app(OperationRunActionEligibility::class)->forRun($run->fresh(), $user); expect($decision['high_risk'])->toBeTrue() ->and(data_get($decision, 'primary_action.key'))->toBe('view_restore_details') ->and($decision['disabled_reasons']['retry'])->toBe(__('localization.operations.actions.disabled.high_risk_retry')) ->and(spec365OperationRunPrimaryActionKeys($decision['secondary_actions']))->not->toContain('reconcile'); }); /** * @param list> $actions * @return list */ function spec365OperationRunPrimaryActionKeys(array $actions): array { return array_values(array_filter(array_map( static fn (array $action): ?string => is_string($action['key'] ?? null) ? (string) $action['key'] : null, $actions, ))); }