create(); $user = User::factory()->create(); $service = app(BulkOperationService::class); $run = $service->createRun($tenant, $user, 'policy', 'delete', [1, 2, 3], 3); $service->start($run); $service->recordSuccess($run); $service->recordSkipped($run); $service->recordFailure($run, '3', 'Test failure'); $run->refresh(); expect($run->status)->toBe('running') ->and($run->processed_items)->toBe(3) ->and($run->succeeded)->toBe(1) ->and($run->skipped)->toBe(1) ->and($run->failed)->toBe(1) ->and($run->failures)->toBeArray() ->and($run->failures)->toHaveCount(1); }); test('bulk operation run total_items is at least the item_ids count', function () { $tenant = Tenant::factory()->create(); $user = User::factory()->create(); $service = app(BulkOperationService::class); $run = $service->createRun($tenant, $user, 'inventory', 'sync', ['a', 'b', 'c', 'd'], 1); expect($run->total_items)->toBe(4); }); test('bulk operation completion clamps total_items up to processed_items', function () { $tenant = Tenant::factory()->create(); $user = User::factory()->create(); $service = app(BulkOperationService::class); $run = $service->createRun($tenant, $user, 'inventory', 'sync', ['only-one'], 1); $service->start($run); $service->recordSuccess($run); $service->recordSuccess($run); $run->refresh(); expect($run->processed_items)->toBe(2)->and($run->total_items)->toBe(1); $service->complete($run); $run->refresh(); expect($run->total_items)->toBe(2); }); test('bulk operation completion treats non-skipped failure entries as errors even when failed is zero', function () { $tenant = Tenant::factory()->create(); $user = User::factory()->create(); $service = app(BulkOperationService::class); $run = $service->createRun($tenant, $user, 'backup_set', 'add_policies', ['1'], 1); $service->start($run); $service->recordSuccess($run); $run->update([ 'failures' => [ [ 'type' => 'foundation', 'item_id' => 'foundation', 'reason' => 'Forbidden', 'reason_code' => 'graph_forbidden', 'timestamp' => now()->toIso8601String(), ], ], ]); $service->complete($run); $run->refresh(); expect($run->failed)->toBe(0) ->and($run->status)->toBe('completed_with_errors'); });