TenantAtlas/apps/platform/tests/Feature/Operations/Spec361EvidenceSnapshotReconciliationTest.php
Ahmed Darrazi 86d1e0cf0d
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m3s
feat: implement report evidence reconciliation
2026-06-07 00:36:22 +02:00

216 lines
9.3 KiB
PHP

<?php
declare(strict_types=1);
use App\Filament\Resources\EvidenceSnapshotResource;
use App\Models\EvidenceSnapshot;
use App\Models\ManagedEnvironment;
use App\Models\OperationRun;
use App\Services\AdapterRunReconciler;
use App\Support\Evidence\EvidenceCompletenessState;
use App\Support\Evidence\EvidenceSnapshotStatus;
use App\Support\Navigation\CrossResourceNavigationMatrix;
use App\Support\Navigation\RelatedNavigationResolver;
use App\Support\OperationRunLinks;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\Ui\GovernanceArtifactTruth\ArtifactTruthPresenter;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
it('reconciles a stale evidence-generation run from a matching active snapshot and prefers canonical snapshot links in Spec361', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$snapshot = restateEnvironmentReviewEvidenceSnapshot(
seedEnvironmentReviewEvidence($tenant, operationRunCount: 2),
EvidenceCompletenessState::Complete,
);
$originalOperationRunId = $snapshot->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();
});