create([ 'tenant_id' => $tenant->id, 'user_id' => $user->id, 'type' => 'policy_version.prune', 'status' => 'queued', 'summary_counts' => [], 'context' => [ 'target_scope' => [ 'entra_tenant_id' => (string) ($tenant->tenant_id ?? $tenant->external_id), ], ], ]); Bus::fake(); $job = new BulkPolicyVersionPruneJob( tenantId: (int) $tenant->getKey(), userId: (int) $user->getKey(), policyVersionIds: [3, 2, 1], retentionDays: 90, operationRun: $run, context: [], ); $job->handle(app(OperationRunService::class)); $run = $run->fresh(); expect($run)->not->toBeNull(); expect($run?->status)->toBe('running'); expect($run?->summary_counts['total'] ?? null)->toBe(3); Bus::assertDispatched(PolicyVersionPruneWorkerJob::class, 3); })->group('ops-ux'); it('prunes only eligible versions and updates summary counts', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $policy = Policy::factory()->create([ 'tenant_id' => $tenant->id, ]); $eligible = PolicyVersion::factory()->create([ 'tenant_id' => $tenant->id, 'policy_id' => $policy->id, 'version_number' => 1, 'captured_at' => now()->subDays(120), 'deleted_at' => null, ]); $notEligibleCurrent = PolicyVersion::factory()->create([ 'tenant_id' => $tenant->id, 'policy_id' => $policy->id, 'version_number' => 2, 'captured_at' => now()->subDays(120), 'deleted_at' => null, ]); $run = OperationRun::factory()->create([ 'tenant_id' => $tenant->id, 'user_id' => $user->id, 'type' => 'policy_version.prune', 'status' => 'running', 'summary_counts' => [ 'total' => 2, 'processed' => 0, 'failed' => 0, ], 'context' => [ 'target_scope' => [ 'entra_tenant_id' => (string) ($tenant->tenant_id ?? $tenant->external_id), ], ], ]); $lock = new class implements Lock { public function get($callback = null): bool { return true; } public function block($seconds, $callback = null): bool { return true; } public function release(): bool { return true; } public function owner(): string { return 'test'; } public function forceRelease(): void { // no-op } }; Cache::partialMock() ->shouldReceive('lock') ->andReturn($lock); (new PolicyVersionPruneWorkerJob( tenantId: (int) $tenant->getKey(), userId: (int) $user->getKey(), policyVersionId: (int) $eligible->getKey(), retentionDays: 90, operationRun: $run, ))->handle(app(OperationRunService::class), app(TargetScopeConcurrencyLimiter::class)); (new PolicyVersionPruneWorkerJob( tenantId: (int) $tenant->getKey(), userId: (int) $user->getKey(), policyVersionId: (int) $notEligibleCurrent->getKey(), retentionDays: 90, operationRun: $run, ))->handle(app(OperationRunService::class), app(TargetScopeConcurrencyLimiter::class)); $run = $run->fresh(); expect($eligible->fresh()?->trashed())->toBeTrue(); expect($notEligibleCurrent->fresh()?->trashed())->toBeFalse(); expect($run)->not->toBeNull(); expect($run?->status)->toBe('completed'); expect($run?->outcome)->toBe('succeeded'); expect($run?->summary_counts['processed'] ?? null)->toBe(2); expect($run?->summary_counts['succeeded'] ?? null)->toBe(1); expect($run?->summary_counts['deleted'] ?? null)->toBe(1); expect($run?->summary_counts['skipped'] ?? null)->toBe(1); })->group('ops-ux');