active()->create([ 'workspace_id' => (int) $tenant->workspace_id, 'scope_jsonb' => ['policy_types' => ['deviceConfiguration'], 'foundation_types' => []], ]); $snapshot = BaselineSnapshot::factory()->incomplete()->create([ 'workspace_id' => (int) $tenant->workspace_id, 'baseline_profile_id' => (int) $profile->getKey(), ]); createInventorySyncOperationRunWithCoverage($tenant, ['deviceConfiguration' => 'succeeded']); $run = OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'user_id' => (int) $user->getKey(), 'type' => OperationRunType::BaselineCompare->value, 'status' => OperationRunStatus::Queued->value, 'outcome' => OperationRunOutcome::Pending->value, 'context' => [ 'baseline_profile_id' => (int) $profile->getKey(), 'baseline_snapshot_id' => (int) $snapshot->getKey(), 'effective_scope' => ['policy_types' => ['deviceConfiguration'], 'foundation_types' => []], ], ]); (new CompareBaselineToTenantJob($run))->handle( app(BaselineSnapshotIdentity::class), app(AuditLogger::class), app(OperationRunService::class), ); $run->refresh(); $context = is_array($run->context) ? $run->context : []; expect($run->status)->toBe(OperationRunStatus::Completed->value) ->and($run->outcome)->toBe(OperationRunOutcome::Blocked->value) ->and(data_get($context, 'baseline_compare.reason_code'))->toBe(BaselineReasonCodes::COMPARE_SNAPSHOT_INCOMPLETE) ->and(data_get($context, 'result.snapshot_id'))->toBe((int) $snapshot->getKey()) ->and(data_get($run->failure_summary, '0.reason_code'))->toBe(BaselineReasonCodes::COMPARE_SNAPSHOT_INCOMPLETE); }); it('blocks compare execution when the queued snapshot is no longer the effective current baseline', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $profile = BaselineProfile::factory()->active()->create([ 'workspace_id' => (int) $tenant->workspace_id, 'scope_jsonb' => ['policy_types' => ['deviceConfiguration'], 'foundation_types' => []], ]); $historicalSnapshot = BaselineSnapshot::factory()->complete()->create([ 'workspace_id' => (int) $tenant->workspace_id, 'baseline_profile_id' => (int) $profile->getKey(), 'captured_at' => now()->subDay(), 'completed_at' => now()->subDay(), ]); $currentSnapshot = BaselineSnapshot::factory()->complete()->create([ 'workspace_id' => (int) $tenant->workspace_id, 'baseline_profile_id' => (int) $profile->getKey(), 'captured_at' => now(), 'completed_at' => now(), ]); $profile->update(['active_snapshot_id' => (int) $currentSnapshot->getKey()]); createInventorySyncOperationRunWithCoverage($tenant, ['deviceConfiguration' => 'succeeded']); $run = OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'user_id' => (int) $user->getKey(), 'type' => OperationRunType::BaselineCompare->value, 'status' => OperationRunStatus::Queued->value, 'outcome' => OperationRunOutcome::Pending->value, 'context' => [ 'baseline_profile_id' => (int) $profile->getKey(), 'baseline_snapshot_id' => (int) $historicalSnapshot->getKey(), 'effective_scope' => ['policy_types' => ['deviceConfiguration'], 'foundation_types' => []], ], ]); (new CompareBaselineToTenantJob($run))->handle( app(BaselineSnapshotIdentity::class), app(AuditLogger::class), app(OperationRunService::class), ); $run->refresh(); $context = is_array($run->context) ? $run->context : []; expect($run->outcome)->toBe(OperationRunOutcome::Blocked->value) ->and(data_get($context, 'baseline_compare.reason_code'))->toBe(BaselineReasonCodes::COMPARE_SNAPSHOT_SUPERSEDED) ->and(data_get($context, 'baseline_compare.effective_snapshot_id'))->toBe((int) $currentSnapshot->getKey()) ->and(data_get($context, 'baseline_compare.latest_attempted_snapshot_id'))->toBe((int) $currentSnapshot->getKey()) ->and(data_get($run->failure_summary, '0.reason_code'))->toBe(BaselineReasonCodes::COMPARE_SNAPSHOT_SUPERSEDED); }); it('marks compare runs as partially succeeded when strategy-owned processing fails before subject classification completes', function (): void { Queue::fake(); [$user, $tenant] = createUserWithTenant(role: 'owner'); app()->instance(GovernanceSubjectTaxonomyRegistry::class, new FakeGovernanceSubjectTaxonomyRegistry); app()->instance(CompareStrategyRegistry::class, new CompareStrategyRegistry([ app(FailingCompareStrategy::class), ])); $profile = BaselineProfile::factory()->active()->create([ 'workspace_id' => (int) $tenant->workspace_id, 'scope_jsonb' => [ 'version' => 2, 'entries' => [[ 'domain_key' => 'entra', 'subject_class' => 'control', 'subject_type_keys' => ['conditionalAccessPolicy'], 'filters' => [], ]], ], ]); $snapshot = BaselineSnapshot::factory()->create([ 'workspace_id' => (int) $tenant->workspace_id, 'baseline_profile_id' => (int) $profile->getKey(), 'captured_at' => now()->subMinute(), ]); $profile->update(['active_snapshot_id' => (int) $snapshot->getKey()]); BaselineTenantAssignment::factory()->create([ 'workspace_id' => (int) $tenant->workspace_id, 'tenant_id' => (int) $tenant->getKey(), 'baseline_profile_id' => (int) $profile->getKey(), ]); BaselineSnapshotItem::factory()->create([ 'baseline_snapshot_id' => (int) $snapshot->getKey(), 'subject_type' => 'control', 'subject_external_id' => 'conditional-access-policy-1', 'subject_key' => 'conditional-access-policy-1', 'policy_type' => 'conditionalAccessPolicy', 'baseline_hash' => hash('sha256', 'baseline'), 'meta_jsonb' => ['display_name' => 'Conditional Access Policy'], ]); $inventorySyncRun = createInventorySyncOperationRunWithCoverage($tenant, ['conditionalAccessPolicy' => 'succeeded']); InventoryItem::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'external_id' => 'conditional-access-policy-1', 'policy_type' => 'conditionalAccessPolicy', 'display_name' => 'Conditional Access Policy', 'meta_jsonb' => ['display_name' => 'Conditional Access Policy'], 'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(), 'last_seen_at' => now(), ]); $result = app(BaselineCompareService::class)->startCompare($tenant, $user); expect($result['ok'])->toBeTrue(); /** @var OperationRun $run */ $run = $result['run']; (new CompareBaselineToTenantJob($run))->handle( app(BaselineSnapshotIdentity::class), app(AuditLogger::class), app(OperationRunService::class), ); $run->refresh(); expect($run->outcome)->toBe(OperationRunOutcome::PartiallySucceeded->value) ->and(data_get($run->context, 'baseline_compare.reason_code'))->toBe(BaselineCompareReasonCode::StrategyFailed->value) ->and(data_get($run->context, 'baseline_compare.strategy.execution_diagnostics.failed'))->toBeTrue() ->and(data_get($run->context, 'baseline_compare.strategy.execution_diagnostics.exception_class'))->toBe(RuntimeException::class) ->and(data_get($run->context, 'baseline_compare.strategy.state_counts'))->toBe([]) ->and(data_get($run->context, 'baseline_compare.evidence_gaps.by_reason.strategy_failed'))->toBe(1); });