Implemented sync capture backup operation semantics as requested. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #433
205 lines
7.7 KiB
PHP
205 lines
7.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Filament\Resources\BaselineSnapshotResource;
|
|
use App\Models\BaselineProfile;
|
|
use App\Models\BaselineSnapshot;
|
|
use App\Models\OperationRun;
|
|
use App\Services\AdapterRunReconciler;
|
|
use App\Support\Baselines\BaselineReasonCodes;
|
|
use App\Support\Baselines\BaselineSnapshotLifecycleState;
|
|
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 legacy baseline capture runs from a matching complete snapshot in Spec362', function (): void {
|
|
[, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$profile = BaselineProfile::factory()->active()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
]);
|
|
|
|
$run = OperationRun::factory()->forTenant($tenant)->create([
|
|
'type' => 'baseline_capture',
|
|
'status' => OperationRunStatus::Queued->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'created_at' => now()->subMinutes(20),
|
|
'context' => [
|
|
'baseline_profile_id' => (int) $profile->getKey(),
|
|
],
|
|
]);
|
|
|
|
$snapshot = BaselineSnapshot::factory()->complete()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'baseline_profile_id' => (int) $profile->getKey(),
|
|
'summary_jsonb' => ['total_items' => 3],
|
|
'completion_meta_jsonb' => [
|
|
'producer_run_id' => (int) $run->getKey(),
|
|
'persisted_items' => 3,
|
|
'expected_items' => 3,
|
|
'was_empty_capture' => false,
|
|
],
|
|
]);
|
|
|
|
$result = app(AdapterRunReconciler::class)->reconcile([
|
|
'type' => 'baseline_capture',
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'older_than_minutes' => 10,
|
|
'limit' => 10,
|
|
'dry_run' => false,
|
|
]);
|
|
|
|
expect($result['candidates'] ?? null)->toBe(1)
|
|
->and($result['reconciled'] ?? null)->toBe(1);
|
|
|
|
$run->refresh();
|
|
$snapshot->refresh();
|
|
|
|
expect($run->status)->toBe(OperationRunStatus::Completed->value)
|
|
->and($run->outcome)->toBe(OperationRunOutcome::Succeeded->value)
|
|
->and($run->reconciliationAdapter())->toBe('baseline_capture')
|
|
->and($run->reconciledRelatedBaselineSnapshotId())->toBe((int) $snapshot->getKey())
|
|
->and($run->relatedArtifactId())->toBe((int) $snapshot->getKey())
|
|
->and($run->summary_counts)->toMatchArray([
|
|
'total' => 3,
|
|
'processed' => 3,
|
|
'succeeded' => 3,
|
|
'failed' => 0,
|
|
])
|
|
->and($snapshot->lifecycleState())->toBe(BaselineSnapshotLifecycleState::Complete);
|
|
|
|
$expected = BaselineSnapshotResource::getUrl('view', ['record' => $snapshot], panel: 'admin');
|
|
$links = OperationRunLinks::related($run->fresh(), $tenant);
|
|
$truth = app(ArtifactTruthPresenter::class)->forOperationRun($run->fresh());
|
|
|
|
expect($links['Baseline Snapshot'] ?? null)->toBe($expected)
|
|
->and($truth->relatedArtifactUrl)->toBe($expected);
|
|
});
|
|
|
|
it('marks baseline capture runs partially succeeded when a usable snapshot still carries gaps in Spec362', function (): void {
|
|
[, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$profile = BaselineProfile::factory()->active()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
]);
|
|
|
|
$snapshot = BaselineSnapshot::factory()->complete()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'baseline_profile_id' => (int) $profile->getKey(),
|
|
'summary_jsonb' => [
|
|
'total_items' => 2,
|
|
'gaps' => ['count' => 1],
|
|
],
|
|
]);
|
|
|
|
$run = OperationRun::factory()->forTenant($tenant)->create([
|
|
'type' => 'baseline.capture',
|
|
'status' => OperationRunStatus::Queued->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'created_at' => now()->subMinutes(20),
|
|
'context' => [
|
|
'baseline_profile_id' => (int) $profile->getKey(),
|
|
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
|
'result' => [
|
|
'items_captured' => 2,
|
|
],
|
|
'baseline_capture' => [
|
|
'subjects_total' => 3,
|
|
'gaps' => ['count' => 1],
|
|
],
|
|
],
|
|
]);
|
|
|
|
$change = app(AdapterRunReconciler::class)->reconcileOperationRun($run, false);
|
|
|
|
expect($change['applied'] ?? null)->toBeTrue();
|
|
|
|
$run->refresh();
|
|
|
|
expect($run->outcome)->toBe(OperationRunOutcome::PartiallySucceeded->value)
|
|
->and($run->reconciliationDecision())->toBe('reconciled_partially_succeeded')
|
|
->and($run->summary_counts)->toMatchArray([
|
|
'total' => 3,
|
|
'processed' => 3,
|
|
'succeeded' => 2,
|
|
'failed' => 1,
|
|
]);
|
|
});
|
|
|
|
it('marks baseline capture runs blocked when precondition proof is explicit and no usable snapshot exists in Spec362', function (): void {
|
|
[, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$profile = BaselineProfile::factory()->active()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
]);
|
|
|
|
$run = OperationRun::factory()->forTenant($tenant)->create([
|
|
'type' => 'baseline.capture',
|
|
'status' => OperationRunStatus::Queued->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'created_at' => now()->subMinutes(20),
|
|
'context' => [
|
|
'baseline_profile_id' => (int) $profile->getKey(),
|
|
'baseline_capture' => [
|
|
'reason_code' => BaselineReasonCodes::CAPTURE_INVENTORY_BLOCKED,
|
|
'eligibility' => [
|
|
'changed_after_enqueue' => true,
|
|
],
|
|
],
|
|
],
|
|
]);
|
|
|
|
$change = app(AdapterRunReconciler::class)->reconcileOperationRun($run, false);
|
|
|
|
expect($change['applied'] ?? null)->toBeTrue();
|
|
|
|
$run->refresh();
|
|
|
|
expect($run->outcome)->toBe(OperationRunOutcome::Blocked->value)
|
|
->and($run->reconciliationDecision())->toBe('blocked')
|
|
->and((string) data_get($run->failure_summary, '0.message'))->toContain('latest inventory sync changed after the run was queued');
|
|
});
|
|
|
|
it('fails closed when an explicit baseline snapshot crosses the queued capture scope in Spec362', function (): void {
|
|
[, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$profile = BaselineProfile::factory()->active()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
]);
|
|
$foreignProfile = BaselineProfile::factory()->active()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
]);
|
|
$foreignSnapshot = BaselineSnapshot::factory()->complete()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'baseline_profile_id' => (int) $foreignProfile->getKey(),
|
|
]);
|
|
|
|
$run = OperationRun::factory()->forTenant($tenant)->create([
|
|
'type' => 'baseline.capture',
|
|
'status' => OperationRunStatus::Queued->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'created_at' => now()->subMinutes(20),
|
|
'context' => [
|
|
'baseline_profile_id' => (int) $profile->getKey(),
|
|
'baseline_snapshot_id' => (int) $foreignSnapshot->getKey(),
|
|
],
|
|
]);
|
|
|
|
$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('queued capture scope safely');
|
|
|
|
$run->refresh();
|
|
|
|
expect($run->status)->toBe(OperationRunStatus::Queued->value)
|
|
->and($run->outcome)->toBe(OperationRunOutcome::Pending->value)
|
|
->and($run->reconciledRelatedBaselineSnapshotId())->toBeNull();
|
|
});
|