operation_run_id; $run = OperationRun::factory()->create([ 'managed_environment_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'user_id' => (int) $user->getKey(), 'initiator_name' => $user->name, 'type' => 'tenant.evidence.snapshot.generate', 'status' => OperationRunStatus::Queued->value, 'outcome' => OperationRunOutcome::Pending->value, 'created_at' => now()->subMinutes(25), 'context' => [ 'managed_environment_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'fingerprint' => (string) $snapshot->fingerprint, ], ]); $change = app(AdapterRunReconciler::class)->reconcileOperationRun($run, false); expect($change['applied'] ?? null)->toBeTrue(); $run->refresh(); $snapshot->refresh(); expect($run->status)->toBe(OperationRunStatus::Completed->value) ->and($run->outcome)->toBe(OperationRunOutcome::Succeeded->value) ->and($run->reconciliationDecision())->toBe('reconciled_succeeded') ->and($run->reconciliationAdapter())->toBe('evidence_snapshot') ->and($run->reconciledRelatedEvidenceSnapshotId())->toBe((int) $snapshot->getKey()) ->and($run->summary_counts)->toMatchArray([ 'finding_count' => (int) data_get($snapshot->summary, 'finding_count', 0), 'report_count' => (int) data_get($snapshot->summary, 'report_count', 0), 'operation_count' => (int) data_get($snapshot->summary, 'operation_count', 0), ]) ->and($snapshot->status)->toBe(EvidenceSnapshotStatus::Active->value) ->and($snapshot->completeness_state)->toBe(EvidenceCompletenessState::Complete->value) ->and($snapshot->operation_run_id)->toBe($originalOperationRunId); $this->actingAs($user); setAdminPanelContext($tenant); $expected = EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], tenant: $tenant); $links = OperationRunLinks::related($run->fresh(), $tenant); $sharedLinks = app(RelatedNavigationResolver::class)->operationLinks($run->fresh(), $tenant); $sharedEntry = collect(app(RelatedNavigationResolver::class)->detailEntries( CrossResourceNavigationMatrix::SOURCE_OPERATION_RUN, $run->fresh(), ))->firstWhere('key', 'evidence_snapshot'); $truth = app(ArtifactTruthPresenter::class)->forOperationRun($run->fresh()); expect($links['Evidence Snapshot'] ?? null)->toBe($expected) ->and($sharedLinks['View evidence snapshot'] ?? null)->toBe($expected) ->and($sharedEntry['targetUrl'] ?? null)->toBe($expected) ->and($sharedEntry['actionLabel'] ?? null)->toBe('View evidence snapshot') ->and($truth?->relatedArtifactUrl)->toBe($expected); }); it('marks evidence-generation runs attention-required when only partial snapshot truth exists in Spec361', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $snapshot = seedPartialEnvironmentReviewEvidence($tenant); $run = OperationRun::factory()->create([ 'managed_environment_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'user_id' => (int) $user->getKey(), 'initiator_name' => $user->name, 'type' => 'tenant.evidence.snapshot.generate', 'status' => OperationRunStatus::Queued->value, 'outcome' => OperationRunOutcome::Pending->value, 'created_at' => now()->subMinutes(25), 'context' => [ 'managed_environment_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'fingerprint' => (string) $snapshot->fingerprint, ], ]); $change = app(AdapterRunReconciler::class)->reconcileOperationRun($run, false); expect($change['applied'] ?? null)->toBeTrue(); $run->refresh(); $snapshot->refresh(); expect($run->status)->toBe(OperationRunStatus::Completed->value) ->and($run->outcome)->toBe(OperationRunOutcome::Failed->value) ->and($run->reconciliationDecision())->toBe('attention_required') ->and((string) data_get($run->failure_summary, '0.message'))->toContain('evidence basis is incomplete') ->and($snapshot->status)->toBe(EvidenceSnapshotStatus::Active->value) ->and($snapshot->completeness_state)->toBe(EvidenceCompletenessState::Partial->value); }); it('keeps evidence-generation runs queued when the matching snapshot is still generating in Spec361', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $fingerprint = 'spec361-evidence-generating'; EvidenceSnapshot::query()->create([ 'managed_environment_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'initiated_by_user_id' => (int) $user->getKey(), 'status' => EvidenceSnapshotStatus::Generating->value, 'fingerprint' => $fingerprint, 'completeness_state' => EvidenceCompletenessState::Missing->value, 'summary' => [ 'finding_count' => 0, 'report_count' => 0, 'operation_count' => 0, ], ]); $run = OperationRun::factory()->create([ 'managed_environment_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'user_id' => (int) $user->getKey(), 'initiator_name' => $user->name, 'type' => 'tenant.evidence.snapshot.generate', 'status' => OperationRunStatus::Queued->value, 'outcome' => OperationRunOutcome::Pending->value, 'created_at' => now()->subMinutes(25), 'context' => [ 'managed_environment_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'fingerprint' => $fingerprint, ], ]); $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('still generating'); $run->refresh(); expect($run->status)->toBe(OperationRunStatus::Queued->value) ->and($run->outcome)->toBe(OperationRunOutcome::Pending->value); }); it('does not cross-scope reconcile evidence-generation runs in Spec361', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $foreignTenant = ManagedEnvironment::factory()->create([ 'workspace_id' => (int) $tenant->workspace_id, ]); $fingerprint = 'spec361-evidence-cross-scope'; EvidenceSnapshot::query()->create([ 'managed_environment_id' => (int) $foreignTenant->getKey(), 'workspace_id' => (int) $foreignTenant->workspace_id, 'status' => EvidenceSnapshotStatus::Active->value, 'fingerprint' => $fingerprint, 'completeness_state' => EvidenceCompletenessState::Complete->value, 'summary' => [ 'finding_count' => 1, 'report_count' => 1, 'operation_count' => 1, ], 'generated_at' => now(), ]); $run = OperationRun::factory()->create([ 'managed_environment_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'user_id' => (int) $user->getKey(), 'initiator_name' => $user->name, 'type' => 'tenant.evidence.snapshot.generate', 'status' => OperationRunStatus::Queued->value, 'outcome' => OperationRunOutcome::Pending->value, 'created_at' => now()->subMinutes(25), 'context' => [ 'managed_environment_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'fingerprint' => $fingerprint, ], ]); $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('No matching evidence snapshot'); $run->refresh(); expect($run->status)->toBe(OperationRunStatus::Queued->value) ->and($run->outcome)->toBe(OperationRunOutcome::Pending->value) ->and($run->reconciledRelatedEvidenceSnapshotId())->toBeNull(); });