TenantAtlas/apps/platform/tests/Feature/Operations/Spec367OperationRunActionabilityResolverTest.php
ahmido 564da05096 feat: implement operation run actionability system (#439)
This PR introduces the Operation Run Actionability System.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #439
2026-06-08 13:34:25 +00:00

239 lines
9.6 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\BackupSet;
use App\Models\ManagedEnvironment;
use App\Models\OperationRun;
use App\Models\ProviderConnection;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\Operations\Actionability\OperationRunActionabilityResolver;
use App\Support\Operations\Actionability\OperationRunActionabilityStatus;
it('resolves old provider connection blockers when the same connection is currently healthy in Spec367', function (): void {
$tenant = ManagedEnvironment::factory()->create();
$connection = ProviderConnection::factory()->verifiedHealthy()->create([
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_id' => (int) $tenant->getKey(),
'provider' => 'microsoft',
]);
$run = spec367TerminalRun($tenant, [
'type' => 'provider.connection.check',
'outcome' => OperationRunOutcome::Blocked->value,
'context' => [
'provider' => 'microsoft',
'provider_connection_id' => (int) $connection->getKey(),
],
]);
$result = app(OperationRunActionabilityResolver::class)->evaluate($run->fresh());
expect($result->status)->toBe(OperationRunActionabilityStatus::ResolvedByCurrentState)
->and($result->requiresCurrentFollowUp())->toBeFalse()
->and($result->resolvingModelType)->toBe('provider_connection')
->and(OperationRun::query()->whereKey($run->getKey())->terminalFollowUp()->exists())->toBeTrue()
->and(OperationRun::query()->whereKey($run->getKey())->currentTerminalFollowUp()->exists())->toBeFalse();
});
it('supersedes old provider blockers with later same-scope successful checks in Spec367', function (): void {
$tenant = ManagedEnvironment::factory()->create();
$connection = ProviderConnection::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_id' => (int) $tenant->getKey(),
'provider' => 'microsoft',
]);
$oldRun = spec367TerminalRun($tenant, [
'type' => 'provider.connection.check',
'outcome' => OperationRunOutcome::Failed->value,
'completed_at' => now()->subHours(2),
'created_at' => now()->subHours(2),
'context' => [
'provider' => 'microsoft',
'provider_connection_id' => (int) $connection->getKey(),
],
]);
$laterRun = spec367TerminalRun($tenant, [
'type' => 'provider.connection.check',
'outcome' => OperationRunOutcome::Succeeded->value,
'completed_at' => now()->subHour(),
'created_at' => now()->subHour(),
'context' => [
'provider' => 'microsoft',
'provider_connection_id' => (int) $connection->getKey(),
],
]);
$result = app(OperationRunActionabilityResolver::class)->evaluate($oldRun->fresh());
expect($result->status)->toBe(OperationRunActionabilityStatus::SupersededByLaterSuccess)
->and($result->supersedingRunId)->toBe((int) $laterRun->getKey())
->and($result->requiresCurrentFollowUp())->toBeFalse();
});
it('does not supersede repeatable runs from a different scope or correlation in Spec367', function (): void {
$tenant = ManagedEnvironment::factory()->create();
$otherTenant = ManagedEnvironment::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
]);
$oldRun = spec367TerminalRun($tenant, [
'type' => 'inventory.sync',
'outcome' => OperationRunOutcome::Failed->value,
'completed_at' => now()->subHours(2),
'created_at' => now()->subHours(2),
'context' => ['selection_hash' => 'selected-family-a'],
]);
spec367TerminalRun($otherTenant, [
'type' => 'inventory.sync',
'outcome' => OperationRunOutcome::Succeeded->value,
'completed_at' => now()->subHour(),
'created_at' => now()->subHour(),
'context' => ['selection_hash' => 'selected-family-a'],
]);
spec367TerminalRun($tenant, [
'type' => 'inventory.sync',
'outcome' => OperationRunOutcome::Succeeded->value,
'completed_at' => now()->subMinutes(30),
'created_at' => now()->subMinutes(30),
'context' => ['selection_hash' => 'selected-family-b'],
]);
$result = app(OperationRunActionabilityResolver::class)->evaluate($oldRun->fresh());
expect($result->status)->toBe(OperationRunActionabilityStatus::Actionable)
->and($result->requiresCurrentFollowUp())->toBeTrue();
});
it('supersedes repeatable runs only with later same-scope success in Spec367', function (): void {
$tenant = ManagedEnvironment::factory()->create();
$oldRun = spec367TerminalRun($tenant, [
'type' => 'inventory.sync',
'outcome' => OperationRunOutcome::PartiallySucceeded->value,
'completed_at' => now()->subHours(2),
'created_at' => now()->subHours(2),
'context' => ['selection_hash' => 'selected-family-a'],
]);
$laterRun = spec367TerminalRun($tenant, [
'type' => 'inventory.sync',
'outcome' => OperationRunOutcome::Succeeded->value,
'completed_at' => now()->subHour(),
'created_at' => now()->subHour(),
'context' => ['selection_hash' => 'selected-family-a'],
]);
$result = app(OperationRunActionabilityResolver::class)->evaluate($oldRun->fresh());
expect($result->status)->toBe(OperationRunActionabilityStatus::SupersededByLaterSuccess)
->and($result->supersedingRunId)->toBe((int) $laterRun->getKey())
->and(OperationRun::query()->whereKey($oldRun->getKey())->currentTerminalFollowUp()->exists())->toBeFalse();
});
it('uses repository artifact proof to resolve terminal backup update history in Spec367', function (): void {
$tenant = ManagedEnvironment::factory()->create();
$backupSet = BackupSet::factory()->for($tenant)->recentCompleted()->create([
'workspace_id' => (int) $tenant->workspace_id,
]);
$run = spec367TerminalRun($tenant, [
'type' => 'backup_set.update',
'outcome' => OperationRunOutcome::Blocked->value,
'context' => [
'backup_set_id' => (int) $backupSet->getKey(),
],
]);
$result = app(OperationRunActionabilityResolver::class)->evaluate($run->fresh());
expect($result->status)->toBe(OperationRunActionabilityStatus::ResolvedByCurrentState)
->and($result->resolvingModelType)->toBe('backup_set')
->and($result->resolvingModelId)->toBe((int) $backupSet->getKey())
->and($result->requiresCurrentFollowUp())->toBeFalse();
});
it('keeps high-risk terminal operations in manual review even when later runs succeed in Spec367', function (): void {
$tenant = ManagedEnvironment::factory()->create();
$oldRun = spec367TerminalRun($tenant, [
'type' => 'restore.execute',
'outcome' => OperationRunOutcome::Failed->value,
'completed_at' => now()->subHours(2),
'created_at' => now()->subHours(2),
]);
spec367TerminalRun($tenant, [
'type' => 'restore.execute',
'outcome' => OperationRunOutcome::Succeeded->value,
'completed_at' => now()->subHour(),
'created_at' => now()->subHour(),
]);
$result = app(OperationRunActionabilityResolver::class)->evaluate($oldRun->fresh());
expect($result->status)->toBe(OperationRunActionabilityStatus::RequiresManualReview)
->and($result->requiresCurrentFollowUp())->toBeTrue()
->and(OperationRun::query()->whereKey($oldRun->getKey())->currentTerminalFollowUp()->exists())->toBeTrue();
});
it('keeps reconciled successful terminal history out of current follow-up in Spec367', function (): void {
$tenant = ManagedEnvironment::factory()->create();
$run = spec367TerminalRun($tenant, [
'type' => 'restore.execute',
'outcome' => OperationRunOutcome::Succeeded->value,
'context' => [
'reconciliation' => [
'reconciled_at' => now()->toIso8601String(),
'decision' => 'completed',
'source' => 'restore_run_observer',
],
],
]);
$result = app(OperationRunActionabilityResolver::class)->evaluate($run->fresh());
expect($result->status)->toBe(OperationRunActionabilityStatus::NotTerminal)
->and($result->requiresCurrentFollowUp())->toBeFalse()
->and(OperationRun::query()->whereKey($run->getKey())->terminalFollowUp()->exists())->toBeTrue()
->and(OperationRun::query()->whereKey($run->getKey())->currentTerminalFollowUp()->exists())->toBeFalse();
});
it('fails closed for unknown terminal operation types in Spec367', function (): void {
$tenant = ManagedEnvironment::factory()->create();
$run = spec367TerminalRun($tenant, [
'type' => 'unknown.operation',
'outcome' => OperationRunOutcome::Failed->value,
]);
$result = app(OperationRunActionabilityResolver::class)->evaluate($run->fresh());
expect($result->status)->toBe(OperationRunActionabilityStatus::RequiresManualReview)
->and($result->reasonCode)->toBe('unknown_operation_type')
->and($result->requiresCurrentFollowUp())->toBeTrue();
});
/**
* @param array<string, mixed> $attributes
*/
function spec367TerminalRun(ManagedEnvironment $tenant, array $attributes = []): OperationRun
{
$completedAt = $attributes['completed_at'] ?? now()->subMinutes(5);
return OperationRun::factory()->forTenant($tenant)->create(array_replace([
'type' => 'policy.sync',
'status' => OperationRunStatus::Completed->value,
'outcome' => OperationRunOutcome::Failed->value,
'created_at' => $completedAt,
'completed_at' => $completedAt,
'context' => [],
], $attributes));
}