TenantAtlas/apps/platform/tests/Unit/Support/Operations/Spec365OperationRunPrimaryActionTest.php
ahmido 6ac0913ff8 feat: implement operations UI operator actions regression gate (#436)
Implemented operations UI operator actions regression gate.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #436
2026-06-08 01:21:14 +00:00

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,
)));
}