270 lines
12 KiB
PHP
270 lines
12 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Filament\Resources\ReviewPackResource;
|
|
use App\Models\ManagedEnvironment;
|
|
use App\Models\OperationRun;
|
|
use App\Models\ReviewPack;
|
|
use App\Services\AdapterRunReconciler;
|
|
use App\Services\ReviewPackService;
|
|
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\ReviewPackStatus;
|
|
use App\Support\Ui\GovernanceArtifactTruth\ArtifactTruthPresenter;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
it('reconciles a stale review-pack run from a matching ready pack and prefers canonical pack links in Spec361', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
$snapshot = seedEnvironmentReviewEvidence($tenant, operationRunCount: 3);
|
|
$review = composeEnvironmentReviewForTest($tenant, $user, $snapshot);
|
|
$options = [
|
|
'include_pii' => false,
|
|
'include_operations' => true,
|
|
];
|
|
$fingerprint = app(ReviewPackService::class)->computeFingerprintForReview($review, $options);
|
|
|
|
$publishedRun = OperationRun::factory()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'type' => 'environment.review_pack.generate',
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Succeeded->value,
|
|
]);
|
|
|
|
$pack = ReviewPack::factory()->ready()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'environment_review_id' => (int) $review->getKey(),
|
|
'evidence_snapshot_id' => (int) $snapshot->getKey(),
|
|
'operation_run_id' => (int) $publishedRun->getKey(),
|
|
'initiated_by_user_id' => (int) $user->getKey(),
|
|
'fingerprint' => $fingerprint,
|
|
'options' => $options,
|
|
'summary' => [
|
|
'finding_count' => 4,
|
|
'report_count' => 2,
|
|
'operation_count' => 3,
|
|
],
|
|
]);
|
|
|
|
$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' => 'environment.review_pack.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,
|
|
'environment_review_id' => (int) $review->getKey(),
|
|
'evidence_snapshot_id' => (int) $snapshot->getKey(),
|
|
'include_pii' => $options['include_pii'],
|
|
'include_operations' => $options['include_operations'],
|
|
],
|
|
]);
|
|
|
|
$change = app(AdapterRunReconciler::class)->reconcileOperationRun($run, false);
|
|
|
|
expect($change['applied'] ?? null)->toBeTrue();
|
|
|
|
$run->refresh();
|
|
$pack->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('review_pack')
|
|
->and($run->reconciledRelatedReviewPackId())->toBe((int) $pack->getKey())
|
|
->and($run->summary_counts)->toMatchArray([
|
|
'finding_count' => 4,
|
|
'report_count' => 2,
|
|
'operation_count' => 3,
|
|
])
|
|
->and($pack->status)->toBe(ReviewPackStatus::Ready->value)
|
|
->and($pack->fingerprint)->toBe($fingerprint);
|
|
|
|
$this->actingAs($user);
|
|
setAdminPanelContext($tenant);
|
|
|
|
$expected = ReviewPackResource::getUrl('view', ['record' => $pack], 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', 'review_pack');
|
|
$truth = app(ArtifactTruthPresenter::class)->forOperationRun($run->fresh());
|
|
|
|
expect($links['Review Pack'] ?? null)->toBe($expected)
|
|
->and($sharedLinks['View review pack'] ?? null)->toBe($expected)
|
|
->and($sharedEntry['targetUrl'] ?? null)->toBe($expected)
|
|
->and($sharedEntry['actionLabel'] ?? null)->toBe('View review pack')
|
|
->and($truth?->relatedArtifactUrl)->toBe($expected);
|
|
});
|
|
|
|
it('keeps review-pack runs queued when the matching pack is still generating in Spec361', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
$snapshot = seedEnvironmentReviewEvidence($tenant);
|
|
$review = composeEnvironmentReviewForTest($tenant, $user, $snapshot);
|
|
$options = [
|
|
'include_pii' => true,
|
|
'include_operations' => false,
|
|
];
|
|
$fingerprint = app(ReviewPackService::class)->computeFingerprintForReview($review, $options);
|
|
|
|
ReviewPack::factory()->generating()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'environment_review_id' => (int) $review->getKey(),
|
|
'evidence_snapshot_id' => (int) $snapshot->getKey(),
|
|
'initiated_by_user_id' => (int) $user->getKey(),
|
|
'fingerprint' => $fingerprint,
|
|
'options' => $options,
|
|
]);
|
|
|
|
$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' => 'environment.review_pack.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,
|
|
'environment_review_id' => (int) $review->getKey(),
|
|
'evidence_snapshot_id' => (int) $snapshot->getKey(),
|
|
'include_pii' => $options['include_pii'],
|
|
'include_operations' => $options['include_operations'],
|
|
],
|
|
]);
|
|
|
|
$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('marks review-pack runs attention-required when the matching pack is expired in Spec361', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
$snapshot = seedEnvironmentReviewEvidence($tenant);
|
|
$review = composeEnvironmentReviewForTest($tenant, $user, $snapshot);
|
|
$options = [
|
|
'include_pii' => true,
|
|
'include_operations' => true,
|
|
];
|
|
$fingerprint = app(ReviewPackService::class)->computeFingerprintForReview($review, $options);
|
|
|
|
ReviewPack::factory()->expired()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'environment_review_id' => (int) $review->getKey(),
|
|
'evidence_snapshot_id' => (int) $snapshot->getKey(),
|
|
'initiated_by_user_id' => (int) $user->getKey(),
|
|
'fingerprint' => $fingerprint,
|
|
'options' => $options,
|
|
]);
|
|
|
|
$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' => 'environment.review_pack.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,
|
|
'environment_review_id' => (int) $review->getKey(),
|
|
'evidence_snapshot_id' => (int) $snapshot->getKey(),
|
|
'include_pii' => $options['include_pii'],
|
|
'include_operations' => $options['include_operations'],
|
|
],
|
|
]);
|
|
|
|
$change = app(AdapterRunReconciler::class)->reconcileOperationRun($run, false);
|
|
|
|
expect($change['applied'] ?? null)->toBeTrue();
|
|
|
|
$run->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('already expired');
|
|
});
|
|
|
|
it('does not cross-scope reconcile review-pack runs in Spec361', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
$foreignTenant = ManagedEnvironment::factory()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
]);
|
|
$snapshot = seedEnvironmentReviewEvidence($tenant);
|
|
$review = composeEnvironmentReviewForTest($tenant, $user, $snapshot);
|
|
$options = [
|
|
'include_pii' => false,
|
|
'include_operations' => false,
|
|
];
|
|
$fingerprint = app(ReviewPackService::class)->computeFingerprintForReview($review, $options);
|
|
|
|
ReviewPack::factory()->ready()->create([
|
|
'managed_environment_id' => (int) $foreignTenant->getKey(),
|
|
'workspace_id' => (int) $foreignTenant->workspace_id,
|
|
'environment_review_id' => (int) $review->getKey(),
|
|
'evidence_snapshot_id' => (int) $snapshot->getKey(),
|
|
'initiated_by_user_id' => (int) $user->getKey(),
|
|
'fingerprint' => $fingerprint,
|
|
'options' => $options,
|
|
]);
|
|
|
|
$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' => 'environment.review_pack.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,
|
|
'environment_review_id' => (int) $review->getKey(),
|
|
'evidence_snapshot_id' => (int) $snapshot->getKey(),
|
|
'include_pii' => $options['include_pii'],
|
|
'include_operations' => $options['include_operations'],
|
|
],
|
|
]);
|
|
|
|
$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 review pack');
|
|
|
|
$run->refresh();
|
|
|
|
expect($run->status)->toBe(OperationRunStatus::Queued->value)
|
|
->and($run->outcome)->toBe(OperationRunOutcome::Pending->value)
|
|
->and($run->reconciledRelatedReviewPackId())->toBeNull();
|
|
});
|