mock(GraphClientInterface::class, function (MockInterface $mock) { $mock->shouldReceive('listPolicies')->never(); }); $sync = app(InventorySyncService::class); $selectionPayload = $sync->defaultSelectionPayload(); $computed = $sync->normalizeAndHashSelection($selectionPayload); $policyTypes = $computed['selection']['policy_types']; $mockSync = \Mockery::mock(InventorySyncService::class); $mockSync ->shouldReceive('executeSelection') ->once() ->andReturn([ 'status' => 'success', 'had_errors' => false, 'error_codes' => [], 'error_context' => [], 'errors_count' => 0, 'items_observed_count' => 0, 'items_upserted_count' => 0, 'skipped_policy_types' => [], 'processed_policy_types' => $computed['selection']['policy_types'], 'failed_policy_types' => [], 'selection_hash' => $computed['selection_hash'], ]); /** @var OperationRunService $opService */ $opService = app(OperationRunService::class); $opRun = $opService->ensureRun( tenant: $tenant, type: 'inventory_sync', inputs: $computed['selection'], initiator: $user, ); $job = new RunInventorySyncJob( tenantId: (int) $tenant->getKey(), userId: (int) $user->getKey(), operationRun: $opRun, ); $job->handle($mockSync, app(AuditLogger::class), $opService); $opRun->refresh(); expect($opRun->status)->toBe('completed'); expect($opRun->outcome)->toBe('succeeded'); $context = is_array($opRun->context) ? $opRun->context : []; expect($context)->toHaveKey('result'); expect($context['result']['had_errors'] ?? null)->toBeFalse(); $counts = is_array($opRun->summary_counts) ? $opRun->summary_counts : []; expect((int) ($counts['total'] ?? 0))->toBe(count($policyTypes)); expect((int) ($counts['processed'] ?? 0))->toBe(count($policyTypes)); expect((int) ($counts['succeeded'] ?? 0))->toBe(count($policyTypes)); expect((int) ($counts['failed'] ?? 0))->toBe(0); expect($user->notifications()->count())->toBe(1); $this->assertDatabaseHas('notifications', [ 'notifiable_id' => $user->getKey(), 'notifiable_type' => $user->getMorphClass(), 'type' => OperationRunCompleted::class, 'data->title' => 'Inventory sync completed', ]); }); it('maps skipped inventory sync runs to bulk progress as skipped with reason', function () { [$user, $tenant] = createUserWithTenant(role: 'owner'); $sync = app(InventorySyncService::class); $selectionPayload = $sync->defaultSelectionPayload(); $computed = $sync->normalizeAndHashSelection($selectionPayload); $policyTypes = $computed['selection']['policy_types']; /** @var OperationRunService $opService */ $opService = app(OperationRunService::class); $opRun = $opService->ensureRun( tenant: $tenant, type: 'inventory_sync', inputs: $computed['selection'], initiator: $user, ); $mockSync = \Mockery::mock(InventorySyncService::class); $mockSync ->shouldReceive('executeSelection') ->once() ->andReturn([ 'status' => 'skipped', 'had_errors' => true, 'error_codes' => ['locked'], 'error_context' => [], 'errors_count' => 0, 'items_observed_count' => 0, 'items_upserted_count' => 0, 'skipped_policy_types' => $computed['selection']['policy_types'], 'processed_policy_types' => [], 'failed_policy_types' => [], 'selection_hash' => $computed['selection_hash'], ]); $job = new RunInventorySyncJob( tenantId: (int) $tenant->getKey(), userId: (int) $user->getKey(), operationRun: $opRun, ); $job->handle($mockSync, app(AuditLogger::class), $opService); $opRun->refresh(); expect($opRun->status)->toBe('completed'); expect($opRun->outcome)->toBe('failed'); $context = is_array($opRun->context) ? $opRun->context : []; expect($context)->toHaveKey('result'); expect($context['result']['had_errors'] ?? null)->toBeTrue(); $counts = is_array($opRun->summary_counts) ? $opRun->summary_counts : []; expect((int) ($counts['processed'] ?? 0))->toBe(count($policyTypes)); expect((int) ($counts['skipped'] ?? 0))->toBe(count($policyTypes)); expect((int) ($counts['succeeded'] ?? 0))->toBe(0); expect((int) ($counts['failed'] ?? 0))->toBe(0); $failures = is_array($opRun->failure_summary) ? $opRun->failure_summary : []; expect($failures)->toBeArray(); expect($failures[0]['code'] ?? null)->toBe('inventory.skipped'); expect($failures[0]['message'] ?? null)->toBe('locked'); expect($user->notifications()->count())->toBe(1); $this->assertDatabaseHas('notifications', [ 'notifiable_id' => $user->getKey(), 'notifiable_type' => $user->getMorphClass(), 'type' => OperationRunCompleted::class, 'data->title' => 'Inventory sync failed', ]); });