TenantAtlas/apps/platform/tests/Feature/EnvironmentReview/Spec359ReviewComposeReconciliationTest.php
Ahmed Darrazi 77c68d2d90
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m1s
feat: implement review compose reconciliation adapter (spec 359)
2026-06-06 16:41:10 +02:00

221 lines
9.4 KiB
PHP

<?php
declare(strict_types=1);
use App\Jobs\ComposeEnvironmentReviewJob;
use App\Models\EnvironmentReview;
use App\Models\OperationRun;
use App\Services\AdapterRunReconciler;
use App\Services\EnvironmentReviews\EnvironmentReviewFingerprint;
use App\Support\EnvironmentReviewStatus;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
it('finishes a late Spec359 job from matching published review truth without mutating the queued review to failed', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$snapshot = seedEnvironmentReviewEvidence($tenant);
$fingerprint = app(EnvironmentReviewFingerprint::class)->forSnapshot($tenant, $snapshot);
$run = OperationRun::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_id' => (int) $tenant->getKey(),
'user_id' => (int) $user->getKey(),
'initiator_name' => $user->name,
'type' => 'environment.review.compose',
'status' => OperationRunStatus::Queued->value,
'outcome' => OperationRunOutcome::Pending->value,
'context' => [
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_id' => (int) $tenant->getKey(),
'evidence_snapshot_id' => (int) $snapshot->getKey(),
'review_fingerprint' => $fingerprint,
],
]);
$review = EnvironmentReview::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_id' => (int) $tenant->getKey(),
'evidence_snapshot_id' => (int) $snapshot->getKey(),
'initiated_by_user_id' => (int) $user->getKey(),
'operation_run_id' => (int) $run->getKey(),
'fingerprint' => $fingerprint,
'status' => EnvironmentReviewStatus::Draft->value,
]);
$run->update([
'context' => array_replace(is_array($run->context) ? $run->context : [], [
'review_id' => (int) $review->getKey(),
]),
]);
$publishedRun = OperationRun::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_id' => (int) $tenant->getKey(),
'type' => 'environment.review.compose',
'status' => OperationRunStatus::Completed->value,
'outcome' => OperationRunOutcome::Succeeded->value,
]);
EnvironmentReview::factory()->published()->create([
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_id' => (int) $tenant->getKey(),
'evidence_snapshot_id' => (int) $snapshot->getKey(),
'initiated_by_user_id' => (int) $user->getKey(),
'operation_run_id' => (int) $publishedRun->getKey(),
'fingerprint' => $fingerprint,
]);
app()->call([new ComposeEnvironmentReviewJob((int) $review->getKey(), (int) $run->getKey()), 'handle']);
$run->refresh();
$review->refresh();
expect($run->status)->toBe(OperationRunStatus::Completed->value)
->and($run->outcome)->toBe(OperationRunOutcome::Succeeded->value)
->and($run->reconciliationDecision())->toBe('reconciled_succeeded')
->and($review->status)->toBe(EnvironmentReviewStatus::Draft->value);
});
it('blocks a late Spec359 job when a successor review exists but is not ready', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$snapshot = seedEnvironmentReviewEvidence($tenant);
$fingerprint = app(EnvironmentReviewFingerprint::class)->forSnapshot($tenant, $snapshot);
$run = OperationRun::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_id' => (int) $tenant->getKey(),
'user_id' => (int) $user->getKey(),
'initiator_name' => $user->name,
'type' => 'environment.review.compose',
'status' => OperationRunStatus::Queued->value,
'outcome' => OperationRunOutcome::Pending->value,
'context' => [
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_id' => (int) $tenant->getKey(),
'evidence_snapshot_id' => (int) $snapshot->getKey(),
'review_fingerprint' => $fingerprint,
],
]);
$successor = EnvironmentReview::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_id' => (int) $tenant->getKey(),
'evidence_snapshot_id' => (int) $snapshot->getKey(),
'initiated_by_user_id' => (int) $user->getKey(),
'fingerprint' => $fingerprint,
'status' => EnvironmentReviewStatus::Draft->value,
]);
$review = EnvironmentReview::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_id' => (int) $tenant->getKey(),
'evidence_snapshot_id' => (int) $snapshot->getKey(),
'initiated_by_user_id' => (int) $user->getKey(),
'operation_run_id' => (int) $run->getKey(),
'fingerprint' => $fingerprint,
'status' => EnvironmentReviewStatus::Superseded->value,
'superseded_by_review_id' => (int) $successor->getKey(),
]);
$run->update([
'context' => array_replace(is_array($run->context) ? $run->context : [], [
'review_id' => (int) $review->getKey(),
]),
]);
app()->call([new ComposeEnvironmentReviewJob((int) $review->getKey(), (int) $run->getKey()), 'handle']);
$run->refresh();
$review->refresh();
expect($run->status)->toBe(OperationRunStatus::Completed->value)
->and($run->outcome)->toBe(OperationRunOutcome::Blocked->value)
->and($run->reconciliationDecision())->toBe('blocked')
->and($review->status)->toBe(EnvironmentReviewStatus::Superseded->value);
});
it('records calm Spec359 attention copy for ambiguous matching review truth', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$snapshot = seedEnvironmentReviewEvidence($tenant);
$fingerprint = app(EnvironmentReviewFingerprint::class)->forSnapshot($tenant, $snapshot);
$run = OperationRun::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_id' => (int) $tenant->getKey(),
'user_id' => (int) $user->getKey(),
'initiator_name' => $user->name,
'type' => 'environment.review.compose',
'status' => OperationRunStatus::Queued->value,
'outcome' => OperationRunOutcome::Pending->value,
'context' => [
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_id' => (int) $tenant->getKey(),
'evidence_snapshot_id' => (int) $snapshot->getKey(),
'review_fingerprint' => $fingerprint,
],
]);
$review = EnvironmentReview::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_id' => (int) $tenant->getKey(),
'evidence_snapshot_id' => (int) $snapshot->getKey(),
'initiated_by_user_id' => (int) $user->getKey(),
'operation_run_id' => (int) $run->getKey(),
'fingerprint' => $fingerprint,
'status' => EnvironmentReviewStatus::Draft->value,
]);
$run->update([
'context' => array_replace(is_array($run->context) ? $run->context : [], [
'review_id' => (int) $review->getKey(),
]),
]);
$firstPublishedRun = OperationRun::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_id' => (int) $tenant->getKey(),
'type' => 'environment.review.compose',
'status' => OperationRunStatus::Completed->value,
'outcome' => OperationRunOutcome::Succeeded->value,
]);
$secondPublishedRun = OperationRun::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_id' => (int) $tenant->getKey(),
'type' => 'environment.review.compose',
'status' => OperationRunStatus::Completed->value,
'outcome' => OperationRunOutcome::Succeeded->value,
]);
EnvironmentReview::factory()->published()->create([
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_id' => (int) $tenant->getKey(),
'evidence_snapshot_id' => (int) $snapshot->getKey(),
'initiated_by_user_id' => (int) $user->getKey(),
'operation_run_id' => (int) $firstPublishedRun->getKey(),
'fingerprint' => $fingerprint,
]);
EnvironmentReview::factory()->published()->create([
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_id' => (int) $tenant->getKey(),
'evidence_snapshot_id' => (int) $snapshot->getKey(),
'initiated_by_user_id' => (int) $user->getKey(),
'operation_run_id' => (int) $secondPublishedRun->getKey(),
'fingerprint' => $fingerprint,
]);
app(AdapterRunReconciler::class)->reconcileOperationRun($run, false);
$run->refresh();
expect($run->status)->toBe(OperationRunStatus::Completed->value)
->and($run->outcome)->toBe(OperationRunOutcome::Failed->value)
->and($run->reconciliationDecision())->toBe('attention_required')
->and(data_get($run->failure_summary, '0.message'))->toBe('Multiple matching reviews are available, so this run needs manual review.')
->and((string) data_get($run->failure_summary, '0.message'))->not->toContain('SQLSTATE')
->and((string) data_get($run->failure_summary, '0.message'))->not->toContain('duplicate key');
});