Spec 359: add a narrow review-compose reconciliation path, deterministic duplicate/superseded recovery, shared review truth resolution, and bounded unit/feature/browser coverage. PGSQL validation remains locally blocked because the pgsql host/Docker runtime was unavailable. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #430
221 lines
9.4 KiB
PHP
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');
|
|
});
|