219 lines
9.2 KiB
PHP
219 lines
9.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\BackupSet;
|
|
use App\Models\EnvironmentReview;
|
|
use App\Models\EvidenceSnapshot;
|
|
use App\Models\OperationRun;
|
|
use App\Models\RestoreRun;
|
|
use App\Models\ReviewPack;
|
|
use App\Support\Evidence\EvidenceCompletenessState;
|
|
use App\Support\Evidence\EvidenceSnapshotStatus;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\Operations\OperationRunActionEligibility;
|
|
use App\Support\RestoreRunStatus;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
it('prefers the related environment review as the primary safe action in Spec365', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
$snapshot = seedEnvironmentReviewEvidence($tenant, operationRunCount: 0);
|
|
|
|
$run = OperationRun::factory()->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<array<string, mixed>> $actions
|
|
* @return list<string>
|
|
*/
|
|
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,
|
|
)));
|
|
}
|