create([ 'status' => 'active', 'name' => $anchorName, ]); [$user, $tenant] = createUserWithTenant( tenant: $tenant, role: 'owner', workspaceRole: 'readonly', ); return [$user, $tenant]; } function tenantRegistryPeer(User $user, Tenant $workspaceTenant, string $name): Tenant { $tenant = Tenant::factory()->create([ 'status' => 'active', 'workspace_id' => (int) $workspaceTenant->workspace_id, 'name' => $name, ]); createUserWithTenant( tenant: $tenant, user: $user, role: 'owner', workspaceRole: 'readonly', ); return $tenant; } function tenantRegistryList(Tenant $workspaceTenant, User $user, array $query = []) { test()->actingAs($user); session()->put(WorkspaceContext::SESSION_KEY, (int) $workspaceTenant->workspace_id); Filament::setTenant(null, true); request()->attributes->remove('tenant_resource.posture_snapshot'); session()->forget('tables.'.md5(ListTenants::class).'_filters'); session()->forget('tables.'.md5(ListTenants::class).'_search'); session()->forget('tables.'.md5(ListTenants::class).'_sort'); $factory = $query !== [] ? Livewire::withQueryParams($query)->actingAs($user) : Livewire::actingAs($user); return $factory->test(ListTenants::class); } function tenantRegistryBackupAssessment( int $tenantId, string $posture, ?string $reason = null, ?string $supportingMessage = null, ): TenantBackupHealthAssessment { return new TenantBackupHealthAssessment( tenantId: $tenantId, posture: $posture, primaryReason: $reason, headline: str($posture)->headline()->toString(), supportingMessage: $supportingMessage, latestRelevantBackupSetId: null, latestRelevantCompletedAt: now()->subMinutes(10), qualitySummary: null, freshnessEvaluation: new BackupFreshnessEvaluation( latestCompletedAt: now()->subMinutes(10), cutoffAt: now()->subHour(), isFresh: true, ), scheduleFollowUp: new BackupScheduleFollowUpEvaluation( hasEnabledSchedules: false, enabledScheduleCount: 0, overdueScheduleCount: 0, failedRecentRunCount: 0, neverSuccessfulCount: 0, needsFollowUp: false, primaryScheduleId: null, summaryMessage: null, ), healthyClaimAllowed: $posture === TenantBackupHealthAssessment::POSTURE_HEALTHY, primaryActionTarget: null, positiveClaimBoundary: 'Recent backup history does not prove tenant recovery.', ); } function tenantRegistryRecoveryEvidence( string $overviewState, string $summary = 'Bounded recovery evidence summary.', string $reason = 'no_recent_issues_visible', ): array { return [ 'overview_state' => $overviewState, 'summary' => $summary, 'claim_boundary' => 'Tenant-wide recovery is not proven.', 'reason' => $reason, 'latest_relevant_restore_run' => null, 'latest_relevant_attention' => null, 'latest_relevant_attention_state' => null, ]; } it('shows separate backup posture and recovery evidence signals without turning metadata into recovery truth', function (): void { CarbonImmutable::setTestNow(CarbonImmutable::create(2026, 4, 9, 9, 0, 0, 'UTC')); [$user, $absentTenant] = tenantRegistryBaseContext('Absent Backup Tenant'); workspaceOverviewSeedQuietTenantTruth($absentTenant); $weakenedTenant = tenantRegistryPeer($user, $absentTenant, 'Weakened Recovery Tenant'); workspaceOverviewSeedQuietTenantTruth($weakenedTenant); $weakenedBackup = workspaceOverviewSeedHealthyBackup($weakenedTenant, [ 'completed_at' => now()->subMinutes(20), ]); workspaceOverviewSeedRestoreHistory($weakenedTenant, $weakenedBackup, 'follow_up'); $metadataTenant = tenantRegistryPeer($user, $absentTenant, 'Metadata Drift Tenant'); workspaceOverviewSeedQuietTenantTruth($metadataTenant); $metadataBackup = workspaceOverviewSeedHealthyBackup($metadataTenant, [ 'completed_at' => now()->subMinutes(15), ]); workspaceOverviewSeedRestoreHistory($metadataTenant, $metadataBackup, 'completed'); Policy::factory()->for($metadataTenant)->create([ 'display_name' => 'Stale sync policy', 'last_synced_at' => now()->subDays(14), ]); $component = tenantRegistryList($absentTenant, $user) ->assertTableColumnExists('backup_posture') ->assertTableColumnExists('recovery_evidence') ->assertTableColumnVisible('backup_posture') ->assertTableColumnVisible('recovery_evidence') ->assertTableColumnFormattedStateSet('backup_posture', 'Absent', $absentTenant) ->assertTableColumnFormattedStateSet('recovery_evidence', 'Weakened', $weakenedTenant) ->assertTableColumnFormattedStateSet('backup_posture', 'Healthy', $metadataTenant) ->assertTableColumnFormattedStateSet('recovery_evidence', 'No recent issues visible', $metadataTenant) ->assertTableActionVisible('openTenant', $weakenedTenant) ->assertTableActionHasUrl('openTenant', TenantDashboard::getUrl(panel: 'tenant', tenant: $weakenedTenant), $weakenedTenant) ->assertDontSee('recoverable') ->assertDontSee('recovery proven') ->assertDontSee('validated overall'); expect($component->instance()->getTable()->getRecordUrl($weakenedTenant)) ->toBe(TenantResource::getUrl('view', ['record' => $weakenedTenant], panel: 'admin')); }); it('filters the registry to exact backup and recovery posture slices', function (): void { CarbonImmutable::setTestNow(CarbonImmutable::create(2026, 4, 9, 9, 0, 0, 'UTC')); [$user, $calmTenant] = tenantRegistryBaseContext('Calm Tenant'); workspaceOverviewSeedQuietTenantTruth($calmTenant); $calmBackup = workspaceOverviewSeedHealthyBackup($calmTenant, [ 'completed_at' => now()->subMinutes(10), ]); workspaceOverviewSeedRestoreHistory($calmTenant, $calmBackup, 'completed'); $degradedTenant = tenantRegistryPeer($user, $calmTenant, 'Degraded Backup Tenant'); workspaceOverviewSeedQuietTenantTruth($degradedTenant); $degradedBackup = workspaceOverviewSeedHealthyBackup($degradedTenant, [ 'completed_at' => now()->subMinutes(12), 'item_count' => 2, ], [ 'payload' => [], 'metadata' => [ 'source' => 'metadata_only', 'assignments_fetch_failed' => true, ], 'assignments' => [], ]); workspaceOverviewSeedRestoreHistory($degradedTenant, $degradedBackup, 'completed'); $unvalidatedTenant = tenantRegistryPeer($user, $calmTenant, 'Unvalidated Recovery Tenant'); workspaceOverviewSeedQuietTenantTruth($unvalidatedTenant); workspaceOverviewSeedHealthyBackup($unvalidatedTenant, [ 'completed_at' => now()->subMinutes(11), ]); tenantRegistryList($calmTenant, $user) ->assertTableColumnFormattedStateSet('recovery_evidence', 'Unvalidated', $unvalidatedTenant); $tenantResourceReflection = new ReflectionClass(TenantResource::class); $postureSnapshot = $tenantResourceReflection->getMethod('postureSnapshot'); $postureSnapshot->setAccessible(true); expect($postureSnapshot->invoke(null)['recovery_evidence_ids']['unvalidated'] ?? []) ->toBe([(int) $unvalidatedTenant->getKey()]); $backupFiltered = tenantRegistryList($calmTenant, $user) ->filterTable('backup_posture', [TenantBackupHealthAssessment::POSTURE_DEGRADED]); expect($backupFiltered->instance()->getFilteredTableQuery()?->pluck('tenants.name')->all()) ->toBe(['Degraded Backup Tenant']); $recoveryFiltered = tenantRegistryList($calmTenant, $user) ->filterTable('recovery_evidence', ['unvalidated']) ->assertSet('tableFilters.recovery_evidence.values', ['unvalidated']); expect($recoveryFiltered->instance()->getFilteredTableQuery()?->pluck('tenants.name')->all()) ->toBe(['Unvalidated Recovery Tenant']); }); it('orders the visible tenant registry worst-first with stable tenant-name tie breaks when triage sorting is requested', function (): void { CarbonImmutable::setTestNow(CarbonImmutable::create(2026, 4, 9, 9, 0, 0, 'UTC')); [$user, $absentTenant] = tenantRegistryBaseContext('Absent Backup Tenant'); workspaceOverviewSeedQuietTenantTruth($absentTenant); $alphaWeakenedTenant = tenantRegistryPeer($user, $absentTenant, 'Alpha Weakened Tenant'); workspaceOverviewSeedQuietTenantTruth($alphaWeakenedTenant); $alphaWeakenedBackup = workspaceOverviewSeedHealthyBackup($alphaWeakenedTenant, [ 'completed_at' => now()->subMinutes(20), ]); workspaceOverviewSeedRestoreHistory($alphaWeakenedTenant, $alphaWeakenedBackup, 'follow_up'); $zetaWeakenedTenant = tenantRegistryPeer($user, $absentTenant, 'Zeta Weakened Tenant'); workspaceOverviewSeedQuietTenantTruth($zetaWeakenedTenant); $zetaWeakenedBackup = workspaceOverviewSeedHealthyBackup($zetaWeakenedTenant, [ 'completed_at' => now()->subMinutes(21), ]); workspaceOverviewSeedRestoreHistory($zetaWeakenedTenant, $zetaWeakenedBackup, 'failed'); $staleTenant = tenantRegistryPeer($user, $absentTenant, 'Stale Backup Tenant'); workspaceOverviewSeedQuietTenantTruth($staleTenant); $staleBackup = workspaceOverviewSeedHealthyBackup($staleTenant, [ 'completed_at' => now()->subDays(2), ]); workspaceOverviewSeedRestoreHistory($staleTenant, $staleBackup, 'completed'); $unvalidatedTenant = tenantRegistryPeer($user, $absentTenant, 'Unvalidated Recovery Tenant'); workspaceOverviewSeedQuietTenantTruth($unvalidatedTenant); workspaceOverviewSeedHealthyBackup($unvalidatedTenant, [ 'completed_at' => now()->subMinutes(18), ]); $degradedTenant = tenantRegistryPeer($user, $absentTenant, 'Degraded Backup Tenant'); workspaceOverviewSeedQuietTenantTruth($degradedTenant); $degradedBackup = workspaceOverviewSeedHealthyBackup($degradedTenant, [ 'completed_at' => now()->subMinutes(17), 'item_count' => 2, ], [ 'payload' => [], 'metadata' => [ 'source' => 'metadata_only', 'assignments_fetch_failed' => true, ], 'assignments' => [], ]); workspaceOverviewSeedRestoreHistory($degradedTenant, $degradedBackup, 'completed'); $calmTenant = tenantRegistryPeer($user, $absentTenant, 'Calm Tenant'); workspaceOverviewSeedQuietTenantTruth($calmTenant); $calmBackup = workspaceOverviewSeedHealthyBackup($calmTenant, [ 'completed_at' => now()->subMinutes(14), ]); workspaceOverviewSeedRestoreHistory($calmTenant, $calmBackup, 'completed'); tenantRegistryList($absentTenant, $user, [ 'triage_sort' => 'worst_first', ]) ->assertSet('tableFilters.triage_sort.value', 'worst_first') ->assertCanSeeTableRecords([ $absentTenant, $alphaWeakenedTenant, $zetaWeakenedTenant, $staleTenant, $unvalidatedTenant, $degradedTenant, $calmTenant, ], inOrder: true); }); it('loads backup posture and recovery evidence with one batch per registry render instead of per-row fanout', function (): void { [$user, $firstTenant] = tenantRegistryBaseContext('Batch Tenant Alpha'); $secondTenant = tenantRegistryPeer($user, $firstTenant, 'Batch Tenant Beta'); $backupAssessments = [ (int) $firstTenant->getKey() => tenantRegistryBackupAssessment( tenantId: (int) $firstTenant->getKey(), posture: TenantBackupHealthAssessment::POSTURE_STALE, reason: TenantBackupHealthAssessment::REASON_LATEST_BACKUP_STALE, supportingMessage: 'The latest backup is outside the configured freshness window.', ), (int) $secondTenant->getKey() => tenantRegistryBackupAssessment( tenantId: (int) $secondTenant->getKey(), posture: TenantBackupHealthAssessment::POSTURE_HEALTHY, ), ]; $expectedTenantIds = [ (int) $firstTenant->getKey(), (int) $secondTenant->getKey(), ]; $backupResolver = new class($expectedTenantIds, $backupAssessments) { public int $assessManyCalls = 0; /** * @param list $expectedTenantIds * @param array $assessments */ public function __construct( private array $expectedTenantIds, private array $assessments, ) {} /** * @return array */ public function assessMany(iterable $tenantIds): array { $this->assessManyCalls++; expect(array_values(is_array($tenantIds) ? $tenantIds : iterator_to_array($tenantIds, false))) ->toBe($this->expectedTenantIds); return $this->assessments; } }; $restoreSafetyResolver = new class($expectedTenantIds, $backupAssessments) { public int $dashboardEvidenceCalls = 0; /** * @param list $expectedTenantIds * @param array $expectedAssessments */ public function __construct( private array $expectedTenantIds, private array $expectedAssessments, ) {} /** * @param array $resolvedAssessments * @return array> */ public function dashboardRecoveryEvidenceForTenants(array $tenantIds, array $resolvedAssessments): array { $this->dashboardEvidenceCalls++; expect($tenantIds)->toBe($this->expectedTenantIds) ->and($resolvedAssessments)->toBe($this->expectedAssessments); return [ $tenantIds[0] => tenantRegistryRecoveryEvidence( overviewState: 'unvalidated', summary: 'No recent restore history is available for this tenant.', reason: 'no_history', ), $tenantIds[1] => tenantRegistryRecoveryEvidence( overviewState: 'no_recent_issues_visible', ), ]; } }; app()->instance(TenantBackupHealthResolver::class, $backupResolver); app()->instance(RestoreSafetyResolver::class, $restoreSafetyResolver); tenantRegistryList($firstTenant, $user) ->assertCanSeeTableRecords([$firstTenant, $secondTenant]) ->assertTableColumnFormattedStateSet('backup_posture', 'Stale', $firstTenant) ->assertTableColumnFormattedStateSet('recovery_evidence', 'No recent issues visible', $secondTenant); expect($backupResolver->assessManyCalls)->toBe(1) ->and($restoreSafetyResolver->dashboardEvidenceCalls)->toBe(1); });