active()->create([ 'workspace_id' => (int) $tenant->workspace_id, ]); $run = OperationRun::factory()->forTenant($tenant)->create([ 'type' => 'baseline_capture', 'status' => OperationRunStatus::Queued->value, 'outcome' => OperationRunOutcome::Pending->value, 'created_at' => now()->subMinutes(20), 'context' => [ 'baseline_profile_id' => (int) $profile->getKey(), ], ]); $snapshot = BaselineSnapshot::factory()->complete()->create([ 'workspace_id' => (int) $tenant->workspace_id, 'baseline_profile_id' => (int) $profile->getKey(), 'summary_jsonb' => ['total_items' => 3], 'completion_meta_jsonb' => [ 'producer_run_id' => (int) $run->getKey(), 'persisted_items' => 3, 'expected_items' => 3, 'was_empty_capture' => false, ], ]); $result = app(AdapterRunReconciler::class)->reconcile([ 'type' => 'baseline_capture', 'managed_environment_id' => (int) $tenant->getKey(), 'older_than_minutes' => 10, 'limit' => 10, 'dry_run' => false, ]); expect($result['candidates'] ?? null)->toBe(1) ->and($result['reconciled'] ?? null)->toBe(1); $run->refresh(); $snapshot->refresh(); expect($run->status)->toBe(OperationRunStatus::Completed->value) ->and($run->outcome)->toBe(OperationRunOutcome::Succeeded->value) ->and($run->reconciliationAdapter())->toBe('baseline_capture') ->and($run->reconciledRelatedBaselineSnapshotId())->toBe((int) $snapshot->getKey()) ->and($run->relatedArtifactId())->toBe((int) $snapshot->getKey()) ->and($run->summary_counts)->toMatchArray([ 'total' => 3, 'processed' => 3, 'succeeded' => 3, 'failed' => 0, ]) ->and($snapshot->lifecycleState())->toBe(BaselineSnapshotLifecycleState::Complete); $expected = BaselineSnapshotResource::getUrl('view', ['record' => $snapshot], panel: 'admin'); $links = OperationRunLinks::related($run->fresh(), $tenant); $truth = app(ArtifactTruthPresenter::class)->forOperationRun($run->fresh()); expect($links['Baseline Snapshot'] ?? null)->toBe($expected) ->and($truth->relatedArtifactUrl)->toBe($expected); }); it('marks baseline capture runs partially succeeded when a usable snapshot still carries gaps in Spec362', function (): void { [, $tenant] = createUserWithTenant(role: 'owner'); $profile = BaselineProfile::factory()->active()->create([ 'workspace_id' => (int) $tenant->workspace_id, ]); $snapshot = BaselineSnapshot::factory()->complete()->create([ 'workspace_id' => (int) $tenant->workspace_id, 'baseline_profile_id' => (int) $profile->getKey(), 'summary_jsonb' => [ 'total_items' => 2, 'gaps' => ['count' => 1], ], ]); $run = OperationRun::factory()->forTenant($tenant)->create([ 'type' => 'baseline.capture', 'status' => OperationRunStatus::Queued->value, 'outcome' => OperationRunOutcome::Pending->value, 'created_at' => now()->subMinutes(20), 'context' => [ 'baseline_profile_id' => (int) $profile->getKey(), 'baseline_snapshot_id' => (int) $snapshot->getKey(), 'result' => [ 'items_captured' => 2, ], 'baseline_capture' => [ 'subjects_total' => 3, 'gaps' => ['count' => 1], ], ], ]); $change = app(AdapterRunReconciler::class)->reconcileOperationRun($run, false); expect($change['applied'] ?? null)->toBeTrue(); $run->refresh(); expect($run->outcome)->toBe(OperationRunOutcome::PartiallySucceeded->value) ->and($run->reconciliationDecision())->toBe('reconciled_partially_succeeded') ->and($run->summary_counts)->toMatchArray([ 'total' => 3, 'processed' => 3, 'succeeded' => 2, 'failed' => 1, ]); }); it('marks baseline capture runs blocked when precondition proof is explicit and no usable snapshot exists in Spec362', function (): void { [, $tenant] = createUserWithTenant(role: 'owner'); $profile = BaselineProfile::factory()->active()->create([ 'workspace_id' => (int) $tenant->workspace_id, ]); $run = OperationRun::factory()->forTenant($tenant)->create([ 'type' => 'baseline.capture', 'status' => OperationRunStatus::Queued->value, 'outcome' => OperationRunOutcome::Pending->value, 'created_at' => now()->subMinutes(20), 'context' => [ 'baseline_profile_id' => (int) $profile->getKey(), 'baseline_capture' => [ 'reason_code' => BaselineReasonCodes::CAPTURE_INVENTORY_BLOCKED, 'eligibility' => [ 'changed_after_enqueue' => true, ], ], ], ]); $change = app(AdapterRunReconciler::class)->reconcileOperationRun($run, false); expect($change['applied'] ?? null)->toBeTrue(); $run->refresh(); expect($run->outcome)->toBe(OperationRunOutcome::Blocked->value) ->and($run->reconciliationDecision())->toBe('blocked') ->and((string) data_get($run->failure_summary, '0.message'))->toContain('latest inventory sync changed after the run was queued'); }); it('fails closed when an explicit baseline snapshot crosses the queued capture scope in Spec362', function (): void { [, $tenant] = createUserWithTenant(role: 'owner'); $profile = BaselineProfile::factory()->active()->create([ 'workspace_id' => (int) $tenant->workspace_id, ]); $foreignProfile = BaselineProfile::factory()->active()->create([ 'workspace_id' => (int) $tenant->workspace_id, ]); $foreignSnapshot = BaselineSnapshot::factory()->complete()->create([ 'workspace_id' => (int) $tenant->workspace_id, 'baseline_profile_id' => (int) $foreignProfile->getKey(), ]); $run = OperationRun::factory()->forTenant($tenant)->create([ 'type' => 'baseline.capture', 'status' => OperationRunStatus::Queued->value, 'outcome' => OperationRunOutcome::Pending->value, 'created_at' => now()->subMinutes(20), 'context' => [ 'baseline_profile_id' => (int) $profile->getKey(), 'baseline_snapshot_id' => (int) $foreignSnapshot->getKey(), ], ]); $change = app(AdapterRunReconciler::class)->reconcileOperationRun($run, true); expect($change['applied'] ?? null)->toBeFalse() ->and($change['decision'] ?? null)->toBe('not_reconciled') ->and((string) ($change['reason_message'] ?? ''))->toContain('queued capture scope safely'); $run->refresh(); expect($run->status)->toBe(OperationRunStatus::Queued->value) ->and($run->outcome)->toBe(OperationRunOutcome::Pending->value) ->and($run->reconciledRelatedBaselineSnapshotId())->toBeNull(); });