'Spec 371 Healthy Backup', 'created_by' => 'operator@example.test', 'completed_at' => now()->subHour(), 'metadata' => ['source' => 'spec371'], ]); $response = $this->actingAs($user) ->get(BackupSetResource::getUrl('view', ['record' => $backupSet], tenant: $tenant)) ->assertOk() ->assertSee('Restore-point decision') ->assertSee('Usable for restore review') ->assertSee('Captured item inventory is available for operator review.') ->assertSee('Review included items below before starting any separate restore workflow.') ->assertSee('Technical and lifecycle detail') ->assertDontSee('Item review state') ->assertDontSee('This backup set has captured items ready for operator review before any separate restore workflow starts.') ->assertDontSee('Usable for review') ->assertDontSee('No degradations detected across'); spec371AssertOrdered($response->getContent(), [ 'Backup set #'.$backupSet->getKey(), 'Restore-point decision', 'Usable for restore review', 'Technical and lifecycle detail', ]); }); it('shows degraded backup input as action-needed before restore review', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); setAdminPanelContext($tenant); $backupSet = BackupSet::factory() ->for($tenant) ->degradedCompleted() ->create([ 'name' => 'Spec 371 Degraded Backup', ]); $response = $this->actingAs($user) ->get(BackupSetResource::getUrl('view', ['record' => $backupSet], tenant: $tenant)) ->assertOk() ->assertSee('Action needed before restore review') ->assertSee('Current degradation') ->assertSee('1 degraded item') ->assertSee('Treat this backup as investigation input until degraded items are reviewed.') ->assertSee('Review degraded included items and source operation before continuing into restore.'); spec371AssertOrdered($response->getContent(), [ 'Restore-point decision', 'Current degradation', 'Technical and lifecycle detail', ]); }); it('explains an empty backup set blocker before technical context', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); setAdminPanelContext($tenant); $backupSet = BackupSet::factory() ->for($tenant) ->create([ 'name' => 'Spec 371 Empty Backup', 'item_count' => 0, 'metadata' => [], ]); $response = $this->actingAs($user) ->get(BackupSetResource::getUrl('view', ['record' => $backupSet], tenant: $tenant)) ->assertOk() ->assertSee('Blocked until items are captured') ->assertSee('Current blocker') ->assertSee('No backup items were captured.') ->assertSee('Create or refresh a backup set before starting restore review.') ->assertDontSee('Backup quality, lifecycle status, and related operations stay ahead of raw backup metadata.'); spec371AssertOrdered($response->getContent(), [ 'Restore-point decision', 'Current blocker', 'Technical and lifecycle detail', ]); }); it('keeps the backup sets list scan-first without repeated healthy zero-noise', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); setAdminPanelContext($tenant); spec371HealthyBackupSet($tenant, ['name' => 'Spec 371 Healthy Row']); BackupSet::factory() ->for($tenant) ->degradedCompleted() ->create(['name' => 'Spec 371 Degraded Row']); $this->actingAs($user) ->get(BackupSetResource::getUrl('index', tenant: $tenant)) ->assertOk() ->assertSee('Restore-point decision') ->assertSee('Items captured') ->assertSee('Usable for restore review') ->assertSee('Action needed before restore review') ->assertSee('1 degraded item') ->assertDontSee('No degradations detected across'); }); it('shows a capability-aware empty state with one create backup CTA for authorized users', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); setAdminPanelContext($tenant); $this->actingAs($user) ->get(BackupSetResource::getUrl('index', tenant: $tenant)) ->assertOk() ->assertSee('No backup sets') ->assertSee('Create a backup set to capture policy versions and assignments for later restore review.') ->assertSee('Create backup set'); }); it('preserves destructive detail action confirmation and readonly blocking', function (): void { [$owner, $tenant] = createUserWithTenant(role: 'owner'); $readonly = User::factory()->create(); createUserWithTenant(tenant: $tenant, user: $readonly, role: 'readonly'); $backupSet = spec371HealthyBackupSet($tenant, ['name' => 'Spec 371 Action Safety']); $this->actingAs($owner); setAdminPanelContext($tenant); Livewire::actingAs($owner) ->test(ViewBackupSet::class, ['record' => (string) $backupSet->getKey()]) ->assertActionExists('archive', fn (Action $action): bool => $action->isConfirmationRequired()); $this->actingAs($readonly); setAdminPanelContext($tenant); Livewire::actingAs($readonly) ->test(ViewBackupSet::class, ['record' => (string) $backupSet->getKey()]) ->assertActionDisabled('archive') ->callAction('archive'); expect($backupSet->fresh()?->trashed())->toBeFalse(); }); it('denies wrong-environment backup set access as not found before content leaks', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $foreignTenant = ManagedEnvironment::factory()->active()->create([ 'workspace_id' => (int) $tenant->workspace_id, 'name' => 'Spec 371 Foreign Environment', ]); $foreignBackupSet = spec371HealthyBackupSet($foreignTenant, [ 'name' => 'Spec 371 Foreign Backup', ]); setAdminPanelContext($tenant); $this->actingAs($user) ->get(BackupSetResource::getUrl('view', ['record' => (string) $foreignBackupSet->getKey()], tenant: $tenant)) ->assertNotFound(); }); /** * @param array $attributes */ function spec371HealthyBackupSet(ManagedEnvironment $tenant, array $attributes = []): BackupSet { $backupSet = BackupSet::factory() ->for($tenant) ->create(array_merge([ 'name' => 'Spec 371 Healthy Backup', 'item_count' => 1, 'completed_at' => now()->subMinutes(30), 'metadata' => [], ], $attributes)); BackupItem::factory() ->for($tenant) ->for($backupSet) ->create([ 'payload' => ['id' => 'spec-371-policy'], 'metadata' => [], 'assignments' => [], ]); return $backupSet; } /** * @param list $needles */ function spec371AssertOrdered(string $content, array $needles): void { $lastPosition = -1; foreach ($needles as $needle) { $position = strpos($content, $needle); expect($position)->not->toBeFalse(); expect($position)->toBeGreaterThan($lastPosition); $lastPosition = (int) $position; } }