TenantAtlas/apps/platform/tests/Browser/Spec362SyncCaptureBackupSemanticsSmokeTest.php
ahmido 548a37c888 feat: implement sync capture backup operation semantics (#433)
Implemented sync capture backup operation semantics as requested.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #433
2026-06-07 01:19:08 +00:00

229 lines
8.3 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\BackupSet;
use App\Models\BaselineProfile;
use App\Models\BaselineSnapshot;
use App\Models\ManagedEnvironment;
use App\Models\OperationRun;
use App\Models\User;
use App\Services\OperationRunService;
use App\Support\Inventory\InventoryCoverage;
use App\Support\OperationRunLinks;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\Workspaces\WorkspaceContext;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
pest()->browser()->timeout(60_000);
it('Spec362 smokes selected-family reconciled truth across the existing operations hub and detail pages', function (): void {
[$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
spec362AuthenticateBrowser($this, $user, $environment);
[$inventoryRun, $baselineRun, $backupRun] = spec362BrowserSeedSelectedFamilyRuns($environment, $user);
visit(OperationRunLinks::index($environment))
->resize(1440, 1100)
->waitForText('Operations Hub')
->assertSee('Inventory sync')
->assertSee('Baseline capture')
->assertSee('Backup schedule run')
->assertNoJavaScriptErrors()
->assertNoConsoleLogs();
visit(OperationRunLinks::tenantlessView($inventoryRun))
->waitForText('Monitoring detail')
->assertSee('Inventory sync')
->assertSee('Completed with follow-up')
->assertSee('Automatically reconciled')
->assertNoJavaScriptErrors()
->assertNoConsoleLogs();
visit(OperationRunLinks::tenantlessView($baselineRun))
->waitForText('Monitoring detail')
->assertSee('Baseline capture')
->assertSee('Completed successfully')
->assertSee('Automatically reconciled')
->assertNoJavaScriptErrors()
->assertNoConsoleLogs();
visit(OperationRunLinks::tenantlessView($backupRun))
->waitForText('Monitoring detail')
->assertSee('Backup schedule run')
->assertSee('Completed with follow-up')
->assertSee('Automatically reconciled')
->assertNoJavaScriptErrors()
->assertNoConsoleLogs();
});
function spec362AuthenticateBrowser(mixed $test, User $user, ManagedEnvironment $environment): void
{
$workspaceId = (int) $environment->workspace_id;
$test->actingAs($user)->withSession([
WorkspaceContext::SESSION_KEY => $workspaceId,
WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [
(string) $workspaceId => (int) $environment->getKey(),
],
]);
session()->put(WorkspaceContext::SESSION_KEY, $workspaceId);
session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [
(string) $workspaceId => (int) $environment->getKey(),
]);
setAdminPanelContext($environment);
}
/**
* @return array{0: OperationRun, 1: OperationRun, 2: OperationRun}
*/
function spec362BrowserSeedSelectedFamilyRuns(ManagedEnvironment $environment, User $user): array
{
$profile = BaselineProfile::factory()->active()->create([
'workspace_id' => (int) $environment->workspace_id,
]);
$snapshot = BaselineSnapshot::factory()->complete()->create([
'workspace_id' => (int) $environment->workspace_id,
'baseline_profile_id' => (int) $profile->getKey(),
'summary_jsonb' => ['total_items' => 2],
]);
$backupSet = BackupSet::factory()->for($environment)->create([
'status' => 'partial',
'item_count' => 2,
'metadata' => [
'failures' => [
['policy_id' => 'policy-1'],
],
],
]);
$inventoryRun = OperationRun::factory()->forTenant($environment)->create([
'user_id' => (int) $user->getKey(),
'initiator_name' => $user->name,
'type' => 'inventory.sync',
'status' => OperationRunStatus::Queued->value,
'outcome' => OperationRunOutcome::Pending->value,
'context' => [
'policy_types' => ['conditionalAccessPolicy', 'deviceConfiguration'],
'inventory' => [
'coverage' => InventoryCoverage::buildPayload([
'conditionalAccessPolicy' => 'succeeded',
'deviceConfiguration' => 'skipped',
], []),
],
],
]);
$baselineRun = OperationRun::factory()->forTenant($environment)->create([
'user_id' => (int) $user->getKey(),
'initiator_name' => $user->name,
'type' => 'baseline.capture',
'status' => OperationRunStatus::Queued->value,
'outcome' => OperationRunOutcome::Pending->value,
'context' => [
'baseline_profile_id' => (int) $profile->getKey(),
'baseline_snapshot_id' => (int) $snapshot->getKey(),
],
]);
$backupRun = OperationRun::factory()->forTenant($environment)->create([
'user_id' => (int) $user->getKey(),
'initiator_name' => $user->name,
'type' => 'backup.schedule.execute',
'status' => OperationRunStatus::Queued->value,
'outcome' => OperationRunOutcome::Pending->value,
'context' => [
'backup_set_id' => (int) $backupSet->getKey(),
],
]);
$service = app(OperationRunService::class);
$inventoryRun = $service->updateRunWithReconciliation(
run: $inventoryRun,
status: OperationRunStatus::Completed->value,
outcome: OperationRunOutcome::PartiallySucceeded->value,
summaryCounts: [
'total' => 2,
'processed' => 2,
'succeeded' => 1,
'failed' => 0,
'skipped' => 1,
],
failures: [[
'code' => 'inventory.partial',
'reason_code' => 'run.adapter_out_of_sync',
'message' => 'The inventory sync recorded usable coverage, but some selected policy types did not complete cleanly.',
]],
reasonCode: 'run.adapter_out_of_sync',
reasonMessage: 'The inventory sync recorded usable coverage, but some selected policy types did not complete cleanly.',
source: 'adapter_reconciler',
evidence: ['adapter' => 'inventory_sync'],
adapter: 'inventory_sync',
decision: 'reconciled_partially_succeeded',
)->fresh();
$baselineRun = $service->updateRunWithReconciliation(
run: $baselineRun,
status: OperationRunStatus::Completed->value,
outcome: OperationRunOutcome::Succeeded->value,
summaryCounts: [
'total' => 2,
'processed' => 2,
'succeeded' => 2,
'failed' => 0,
],
failures: [],
reasonCode: 'run.adapter_out_of_sync',
reasonMessage: 'The baseline capture already produced a complete snapshot before the run finished updating.',
source: 'adapter_reconciler',
evidence: ['adapter' => 'baseline_capture'],
adapter: 'baseline_capture',
decision: 'reconciled_succeeded',
related: [
'type' => 'baseline_snapshot',
'id' => (int) $snapshot->getKey(),
'lifecycle_state' => 'complete',
],
)->fresh();
$backupRun = $service->updateRunWithReconciliation(
run: $backupRun,
status: OperationRunStatus::Completed->value,
outcome: OperationRunOutcome::PartiallySucceeded->value,
summaryCounts: [
'total' => 3,
'processed' => 3,
'succeeded' => 2,
'failed' => 1,
'created' => 1,
'updated' => 2,
'items' => 3,
],
failures: [[
'code' => 'backup_schedule.partial',
'reason_code' => 'run.adapter_out_of_sync',
'message' => 'The backup schedule produced a usable backup set, but some captures did not finish cleanly.',
]],
reasonCode: 'run.adapter_out_of_sync',
reasonMessage: 'The backup schedule produced a usable backup set, but some captures did not finish cleanly.',
source: 'adapter_reconciler',
evidence: ['adapter' => 'backup_schedule_execution'],
adapter: 'backup_schedule_execution',
decision: 'reconciled_partially_succeeded',
related: [
'type' => 'backup_set',
'id' => (int) $backupSet->getKey(),
'status' => 'partial',
],
)->fresh();
return [$inventoryRun, $baselineRun, $backupRun];
}