Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 4m7s
Implemented deterministic Baseline Result Semantics (Spec 383), introducing CompareSubjectResult and CompareEvidenceResult. Replaced generic arrays with strict Data Transfer Objects for Baseline engine output.
746 lines
28 KiB
PHP
746 lines
28 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Filament\Resources\BackupScheduleResource;
|
|
use App\Filament\Resources\BackupSetResource;
|
|
use App\Filament\Resources\FindingResource;
|
|
use App\Filament\Resources\RestoreRunResource;
|
|
use App\Filament\Widgets\Dashboard\NeedsAttention;
|
|
use App\Models\BackupItem;
|
|
use App\Models\BackupSchedule;
|
|
use App\Models\BackupSet;
|
|
use App\Models\BaselineProfile;
|
|
use App\Models\BaselineSnapshot;
|
|
use App\Models\BaselineTenantAssignment;
|
|
use App\Models\Finding;
|
|
use App\Models\FindingException;
|
|
use App\Models\OperationRun;
|
|
use App\Models\RestoreRun;
|
|
use App\Support\Auth\Capabilities;
|
|
use App\Support\BackupHealth\TenantBackupHealthAssessment;
|
|
use App\Support\Baselines\BaselineCompareReasonCode;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\OperationRunType;
|
|
use App\Support\Rbac\UiTooltips;
|
|
use App\Support\RestoreSafety\RestoreResultAttention;
|
|
use Carbon\CarbonImmutable;
|
|
use Illuminate\Support\Facades\Gate;
|
|
use Livewire\Livewire;
|
|
|
|
function createNeedsAttentionTenant(): array
|
|
{
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$profile = BaselineProfile::factory()->active()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
]);
|
|
|
|
$snapshot = BaselineSnapshot::factory()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'baseline_profile_id' => (int) $profile->getKey(),
|
|
]);
|
|
|
|
$profile->update(['active_snapshot_id' => (int) $snapshot->getKey()]);
|
|
|
|
BaselineTenantAssignment::factory()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'baseline_profile_id' => (int) $profile->getKey(),
|
|
]);
|
|
|
|
return [$user, $tenant, $profile, $snapshot];
|
|
}
|
|
|
|
function makeBackupHealthScheduleForNeedsAttention(\App\Models\ManagedEnvironment $tenant, array $attributes = []): BackupSchedule
|
|
{
|
|
return BackupSchedule::query()->create(array_merge([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'name' => 'Needs Attention backup schedule',
|
|
'is_enabled' => true,
|
|
'timezone' => 'UTC',
|
|
'frequency' => 'daily',
|
|
'time_of_day' => '01:00:00',
|
|
'days_of_week' => null,
|
|
'policy_types' => ['deviceConfiguration'],
|
|
'include_foundations' => true,
|
|
'retention_keep_last' => 30,
|
|
'next_run_at' => now()->addHour(),
|
|
], $attributes));
|
|
}
|
|
|
|
function makeHealthyBackupForRecoveryNeedsAttention(\App\Models\ManagedEnvironment $tenant, array $attributes = []): BackupSet
|
|
{
|
|
$backupSet = BackupSet::factory()->for($tenant)->recentCompleted()->create(array_merge([
|
|
'name' => 'Healthy recovery needs-attention backup',
|
|
'item_count' => 1,
|
|
], $attributes));
|
|
|
|
BackupItem::factory()->for($tenant)->for($backupSet)->create([
|
|
'payload' => ['id' => 'healthy-recovery-policy'],
|
|
'metadata' => [],
|
|
'assignments' => [],
|
|
]);
|
|
|
|
return $backupSet;
|
|
}
|
|
|
|
dataset('needs-attention-recovery-cases', [
|
|
'failed history' => [
|
|
fn (\App\Models\ManagedEnvironment $tenant, BackupSet $backupSet): RestoreRun => RestoreRun::factory()
|
|
->for($tenant)
|
|
->for($backupSet)
|
|
->failedOutcome()
|
|
->create([
|
|
'completed_at' => now()->subMinutes(10),
|
|
]),
|
|
'Recent restore failed',
|
|
'The restore did not complete successfully. Follow-up is still required.',
|
|
'failed',
|
|
],
|
|
'partial history' => [
|
|
fn (\App\Models\ManagedEnvironment $tenant, BackupSet $backupSet): RestoreRun => RestoreRun::factory()
|
|
->for($tenant)
|
|
->for($backupSet)
|
|
->partialOutcome()
|
|
->create([
|
|
'completed_at' => now()->subMinutes(10),
|
|
]),
|
|
'Recent restore is partial',
|
|
'The restore reached a terminal state, but some items or assignments still need follow-up.',
|
|
'partial',
|
|
],
|
|
'follow-up history' => [
|
|
fn (\App\Models\ManagedEnvironment $tenant, BackupSet $backupSet): RestoreRun => RestoreRun::factory()
|
|
->for($tenant)
|
|
->for($backupSet)
|
|
->completedWithFollowUp()
|
|
->create([
|
|
'completed_at' => now()->subMinutes(10),
|
|
]),
|
|
'Recent restore needs follow-up',
|
|
'The restore completed, but follow-up remains for skipped or non-applied work.',
|
|
RestoreResultAttention::STATE_COMPLETED_WITH_FOLLOW_UP,
|
|
],
|
|
]);
|
|
|
|
afterEach(function (): void {
|
|
CarbonImmutable::setTestNow();
|
|
});
|
|
|
|
it('shows a cautionary baseline posture in needs-attention when compare trust is limited', function (): void {
|
|
[$user, $tenant, $profile, $snapshot] = createNeedsAttentionTenant();
|
|
$this->actingAs($user);
|
|
|
|
OperationRun::factory()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'type' => OperationRunType::BaselineCompare->value,
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::PartiallySucceeded->value,
|
|
'completed_at' => now(),
|
|
'context' => [
|
|
'baseline_profile_id' => (int) $profile->getKey(),
|
|
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
|
'baseline_compare' => [
|
|
'reason_code' => BaselineCompareReasonCode::EvidenceCaptureIncomplete->value,
|
|
'coverage' => [
|
|
'effective_types' => ['deviceConfiguration'],
|
|
'covered_types' => ['deviceConfiguration'],
|
|
'uncovered_types' => [],
|
|
'proof' => true,
|
|
],
|
|
'evidence_gaps' => [
|
|
'count' => 2,
|
|
'by_reason' => [
|
|
'missing_local_evidence' => 2,
|
|
],
|
|
],
|
|
],
|
|
],
|
|
]);
|
|
|
|
setAdminPanelContext($tenant);
|
|
|
|
$component = Livewire::test(NeedsAttention::class)
|
|
->assertSee('Needs Attention')
|
|
->assertSee('Baseline compare posture')
|
|
->assertSee('The last compare finished, but normal result output was suppressed.')
|
|
->assertSee('Open Baseline Compare')
|
|
->assertDontSee('Current governance and findings signals look trustworthy.');
|
|
|
|
expect($component->html())->toContain('href=');
|
|
});
|
|
|
|
it('keeps needs-attention healthy only for trustworthy compare results', function (): void {
|
|
[$user, $tenant, $profile, $snapshot] = createNeedsAttentionTenant();
|
|
$this->actingAs($user);
|
|
|
|
$healthyBackup = BackupSet::factory()->for($tenant)->create([
|
|
'name' => 'Healthy compare backup',
|
|
'item_count' => 1,
|
|
'completed_at' => now()->subMinutes(20),
|
|
]);
|
|
|
|
BackupItem::factory()->for($tenant)->for($healthyBackup)->create([
|
|
'payload' => ['id' => 'healthy-policy'],
|
|
'metadata' => [],
|
|
'assignments' => [],
|
|
]);
|
|
|
|
RestoreRun::withoutEvents(fn (): RestoreRun => RestoreRun::factory()
|
|
->for($tenant)
|
|
->for($healthyBackup)
|
|
->completedOutcome()
|
|
->create([
|
|
'completed_at' => now()->subMinutes(10),
|
|
]));
|
|
|
|
OperationRun::factory()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'type' => OperationRunType::BaselineCompare->value,
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Succeeded->value,
|
|
'completed_at' => now()->subHour(),
|
|
'context' => [
|
|
'baseline_profile_id' => (int) $profile->getKey(),
|
|
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
|
'baseline_compare' => [
|
|
'reason_code' => BaselineCompareReasonCode::NoDriftDetected->value,
|
|
'coverage' => [
|
|
'effective_types' => ['deviceConfiguration'],
|
|
'covered_types' => ['deviceConfiguration'],
|
|
'uncovered_types' => [],
|
|
'proof' => true,
|
|
],
|
|
],
|
|
],
|
|
]);
|
|
|
|
setAdminPanelContext($tenant);
|
|
|
|
$component = Livewire::test(NeedsAttention::class)
|
|
->assertSee('Current governance and findings signals look trustworthy.')
|
|
->assertSee('Baseline compare looks trustworthy')
|
|
->assertSee('No recent restore issues visible')
|
|
->assertSee('No confirmed drift in the latest baseline compare.')
|
|
->assertDontSee('Baseline compare posture');
|
|
|
|
expect($component->html())->not->toContain('href=');
|
|
});
|
|
|
|
it('surfaces stale compare posture instead of a healthy fallback', function (): void {
|
|
[$user, $tenant, $profile, $snapshot] = createNeedsAttentionTenant();
|
|
$this->actingAs($user);
|
|
|
|
OperationRun::factory()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'type' => OperationRunType::BaselineCompare->value,
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Succeeded->value,
|
|
'completed_at' => now()->subDays(10),
|
|
'context' => [
|
|
'baseline_profile_id' => (int) $profile->getKey(),
|
|
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
|
'baseline_compare' => [
|
|
'reason_code' => BaselineCompareReasonCode::NoDriftDetected->value,
|
|
'coverage' => [
|
|
'effective_types' => ['deviceConfiguration'],
|
|
'covered_types' => ['deviceConfiguration'],
|
|
'uncovered_types' => [],
|
|
'proof' => true,
|
|
],
|
|
],
|
|
],
|
|
]);
|
|
|
|
setAdminPanelContext($tenant);
|
|
|
|
Livewire::test(NeedsAttention::class)
|
|
->assertSee('Baseline compare posture')
|
|
->assertSee('The latest baseline compare result is stale.')
|
|
->assertSee('Open Baseline Compare')
|
|
->assertDontSee('Current governance and findings signals look trustworthy.');
|
|
});
|
|
|
|
it('surfaces compare unavailability instead of a healthy fallback when no result exists yet', function (): void {
|
|
[$user, $tenant] = createNeedsAttentionTenant();
|
|
$this->actingAs($user);
|
|
|
|
setAdminPanelContext($tenant);
|
|
|
|
Livewire::test(NeedsAttention::class)
|
|
->assertSee('Baseline compare posture')
|
|
->assertSee('A current baseline compare result is not available yet.')
|
|
->assertSee('Open Baseline Compare')
|
|
->assertDontSee('Current governance and findings signals look trustworthy.');
|
|
});
|
|
|
|
it('surfaces overdue and lapsed-governance findings even when there are no new findings', function (): void {
|
|
[$user, $tenant] = createNeedsAttentionTenant();
|
|
$this->actingAs($user);
|
|
|
|
Finding::factory()->for($tenant)->create([
|
|
'status' => Finding::STATUS_TRIAGED,
|
|
'due_at' => now()->subDay(),
|
|
]);
|
|
|
|
Finding::factory()->for($tenant)->create([
|
|
'status' => Finding::STATUS_RISK_ACCEPTED,
|
|
]);
|
|
|
|
setAdminPanelContext($tenant);
|
|
|
|
Livewire::test(NeedsAttention::class)
|
|
->assertSee('Overdue findings')
|
|
->assertSee('Lapsed accepted-risk governance')
|
|
->assertSee('Open findings')
|
|
->assertDontSee('Current governance and findings signals look trustworthy.');
|
|
});
|
|
|
|
it('surfaces expiring governance from the shared aggregate with the matching findings action', function (): void {
|
|
[$user, $tenant, $profile, $snapshot] = createNeedsAttentionTenant();
|
|
$this->actingAs($user);
|
|
|
|
OperationRun::factory()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'type' => OperationRunType::BaselineCompare->value,
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Succeeded->value,
|
|
'completed_at' => now(),
|
|
'context' => [
|
|
'baseline_profile_id' => (int) $profile->getKey(),
|
|
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
|
'baseline_compare' => [
|
|
'reason_code' => BaselineCompareReasonCode::NoDriftDetected->value,
|
|
'coverage' => [
|
|
'effective_types' => ['deviceConfiguration'],
|
|
'covered_types' => ['deviceConfiguration'],
|
|
'uncovered_types' => [],
|
|
'proof' => true,
|
|
],
|
|
],
|
|
],
|
|
]);
|
|
|
|
$finding = Finding::factory()->riskAccepted()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
]);
|
|
|
|
FindingException::query()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'finding_id' => (int) $finding->getKey(),
|
|
'requested_by_user_id' => (int) $user->getKey(),
|
|
'owner_user_id' => (int) $user->getKey(),
|
|
'approved_by_user_id' => (int) $user->getKey(),
|
|
'status' => FindingException::STATUS_EXPIRING,
|
|
'current_validity_state' => FindingException::VALIDITY_EXPIRING,
|
|
'request_reason' => 'Expiring governance coverage',
|
|
'approval_reason' => 'Approved for coverage',
|
|
'requested_at' => now()->subDays(2),
|
|
'approved_at' => now()->subDay(),
|
|
'effective_from' => now()->subDay(),
|
|
'expires_at' => now()->addDays(2),
|
|
'review_due_at' => now()->addDay(),
|
|
'evidence_summary' => ['reference_count' => 0],
|
|
]);
|
|
|
|
setAdminPanelContext($tenant);
|
|
|
|
$component = Livewire::test(NeedsAttention::class)
|
|
->assertSee('Expiring accepted-risk governance')
|
|
->assertSee('Open findings')
|
|
->assertDontSee('Current governance and findings signals look trustworthy.');
|
|
|
|
expect($component->html())->toContain('href=');
|
|
});
|
|
|
|
it('keeps findings attention visible but non-clickable when the member lacks findings access', function (): void {
|
|
[$user, $tenant] = createNeedsAttentionTenant();
|
|
$this->actingAs($user);
|
|
|
|
Finding::factory()->for($tenant)->create([
|
|
'status' => Finding::STATUS_TRIAGED,
|
|
'due_at' => now()->subDay(),
|
|
]);
|
|
|
|
Gate::define(Capabilities::TENANT_FINDINGS_VIEW, fn (): bool => false);
|
|
|
|
setAdminPanelContext($tenant);
|
|
|
|
$component = Livewire::test(NeedsAttention::class)
|
|
->assertSee('Overdue findings')
|
|
->assertSee('Open findings')
|
|
->assertSee(UiTooltips::INSUFFICIENT_PERMISSION);
|
|
|
|
expect($component->html())
|
|
->not->toContain(FindingResource::getUrl('index', ['tab' => 'overdue'], panel: 'admin', tenant: $tenant))
|
|
->toContain('Open Baseline Compare');
|
|
});
|
|
|
|
it('separates stale active attention from terminal follow-up on tenant operations attention', function (): void {
|
|
[$user, $tenant] = createNeedsAttentionTenant();
|
|
$this->actingAs($user);
|
|
|
|
OperationRun::factory()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'type' => 'inventory_sync',
|
|
'status' => OperationRunStatus::Queued->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'created_at' => now()->subHour(),
|
|
]);
|
|
|
|
OperationRun::factory()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'type' => 'policy.sync',
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Failed->value,
|
|
]);
|
|
|
|
setAdminPanelContext($tenant);
|
|
|
|
Livewire::test(NeedsAttention::class)
|
|
->assertSee('Active operations look stale')
|
|
->assertSee('Operations need current follow-up')
|
|
->assertSee('Open stale operations')
|
|
->assertSee('Open current follow-up')
|
|
->assertDontSee('Current governance and findings signals look trustworthy.');
|
|
});
|
|
|
|
it('surfaces a no-backup attention item with a backup-sets destination', function (): void {
|
|
[$user, $tenant] = createNeedsAttentionTenant();
|
|
$this->actingAs($user);
|
|
|
|
setAdminPanelContext($tenant);
|
|
|
|
$component = Livewire::test(NeedsAttention::class)
|
|
->assertSee('No usable backup basis')
|
|
->assertSee('Create or finish a backup set before relying on restore input.')
|
|
->assertSee('Open backup sets')
|
|
->assertDontSee('Backups are recent and healthy');
|
|
|
|
expect($component->html())->toContain(BackupSetResource::getUrl('index', [
|
|
'backup_health_reason' => TenantBackupHealthAssessment::REASON_NO_BACKUP_BASIS,
|
|
], panel: 'admin', tenant: $tenant));
|
|
});
|
|
|
|
it('surfaces stale latest-backup attention with the matching latest-backup drill-through', function (): void {
|
|
CarbonImmutable::setTestNow(CarbonImmutable::create(2026, 4, 7, 12, 0, 0, 'UTC'));
|
|
|
|
[$user, $tenant] = createNeedsAttentionTenant();
|
|
$this->actingAs($user);
|
|
|
|
$staleBackup = BackupSet::factory()->for($tenant)->create([
|
|
'name' => 'Stale backup',
|
|
'item_count' => 1,
|
|
'completed_at' => now()->subDays(2),
|
|
]);
|
|
|
|
BackupItem::factory()->for($tenant)->for($staleBackup)->create([
|
|
'payload' => ['id' => 'policy-stale'],
|
|
'metadata' => [],
|
|
'assignments' => [],
|
|
]);
|
|
|
|
setAdminPanelContext($tenant);
|
|
|
|
$staleComponent = Livewire::test(NeedsAttention::class)
|
|
->assertSee('Latest backup is stale')
|
|
->assertSee('Open latest backup')
|
|
->assertDontSee('Backups are recent and healthy');
|
|
|
|
expect($staleComponent->html())->toContain(BackupSetResource::getUrl('view', [
|
|
'record' => (int) $staleBackup->getKey(),
|
|
'backup_health_reason' => TenantBackupHealthAssessment::REASON_LATEST_BACKUP_STALE,
|
|
], panel: 'admin', tenant: $tenant));
|
|
});
|
|
|
|
it('surfaces degraded latest-backup attention with the matching latest-backup drill-through', function (): void {
|
|
CarbonImmutable::setTestNow(CarbonImmutable::create(2026, 4, 7, 12, 0, 0, 'UTC'));
|
|
|
|
[$user, $tenant] = createNeedsAttentionTenant();
|
|
$this->actingAs($user);
|
|
|
|
$degradedBackup = BackupSet::factory()->for($tenant)->create([
|
|
'name' => 'Degraded backup',
|
|
'item_count' => 1,
|
|
'completed_at' => now()->subMinutes(45),
|
|
]);
|
|
|
|
BackupItem::factory()->for($tenant)->for($degradedBackup)->create([
|
|
'payload' => [],
|
|
'metadata' => [
|
|
'source' => 'metadata_only',
|
|
'assignments_fetch_failed' => true,
|
|
],
|
|
'assignments' => [],
|
|
]);
|
|
|
|
setAdminPanelContext($tenant);
|
|
|
|
$degradedComponent = Livewire::test(NeedsAttention::class)
|
|
->assertSee('Latest backup is degraded')
|
|
->assertSee('Open latest backup')
|
|
->assertDontSee('Backups are recent and healthy');
|
|
|
|
expect($degradedComponent->html())->toContain(BackupSetResource::getUrl('view', [
|
|
'record' => (int) $degradedBackup->getKey(),
|
|
'backup_health_reason' => TenantBackupHealthAssessment::REASON_LATEST_BACKUP_DEGRADED,
|
|
], panel: 'admin', tenant: $tenant));
|
|
});
|
|
|
|
it('surfaces schedule follow-up instead of a healthy backup check when automation needs review', function (): void {
|
|
CarbonImmutable::setTestNow(CarbonImmutable::create(2026, 4, 7, 12, 0, 0, 'UTC'));
|
|
|
|
[$user, $tenant] = createNeedsAttentionTenant();
|
|
$this->actingAs($user);
|
|
|
|
$healthyBackup = BackupSet::factory()->for($tenant)->create([
|
|
'name' => 'Healthy backup',
|
|
'item_count' => 1,
|
|
'completed_at' => now()->subMinutes(20),
|
|
]);
|
|
|
|
BackupItem::factory()->for($tenant)->for($healthyBackup)->create([
|
|
'payload' => ['id' => 'healthy-policy'],
|
|
'metadata' => [],
|
|
'assignments' => [],
|
|
]);
|
|
|
|
makeBackupHealthScheduleForNeedsAttention($tenant, [
|
|
'name' => 'Overdue schedule',
|
|
'last_run_at' => null,
|
|
'last_run_status' => null,
|
|
'next_run_at' => now()->subHours(2),
|
|
]);
|
|
|
|
setAdminPanelContext($tenant);
|
|
|
|
$component = Livewire::test(NeedsAttention::class)
|
|
->assertSee('Backup schedules need follow-up')
|
|
->assertSee('not produced a successful run')
|
|
->assertSee('Open backup schedules')
|
|
->assertDontSee('Backups are recent and healthy');
|
|
|
|
expect($component->html())->toContain(BackupScheduleResource::getUrl('index', [
|
|
'backup_health_reason' => TenantBackupHealthAssessment::REASON_SCHEDULE_FOLLOW_UP,
|
|
], panel: 'admin', tenant: $tenant));
|
|
});
|
|
|
|
it('adds the healthy backup check only when the latest backup basis genuinely earns it', function (): void {
|
|
CarbonImmutable::setTestNow(CarbonImmutable::create(2026, 4, 7, 12, 0, 0, 'UTC'));
|
|
|
|
[$user, $tenant, $profile, $snapshot] = createNeedsAttentionTenant();
|
|
$this->actingAs($user);
|
|
|
|
$healthyBackup = BackupSet::factory()->for($tenant)->create([
|
|
'name' => 'Healthy backup',
|
|
'item_count' => 1,
|
|
'completed_at' => now()->subMinutes(10),
|
|
]);
|
|
|
|
BackupItem::factory()->for($tenant)->for($healthyBackup)->create([
|
|
'payload' => ['id' => 'healthy-policy'],
|
|
'metadata' => [],
|
|
'assignments' => [],
|
|
]);
|
|
|
|
RestoreRun::withoutEvents(fn (): RestoreRun => RestoreRun::factory()
|
|
->for($tenant)
|
|
->for($healthyBackup)
|
|
->completedOutcome()
|
|
->create([
|
|
'completed_at' => now()->subMinutes(10),
|
|
]));
|
|
|
|
OperationRun::factory()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'type' => OperationRunType::BaselineCompare->value,
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Succeeded->value,
|
|
'completed_at' => now()->subHour(),
|
|
'context' => [
|
|
'baseline_profile_id' => (int) $profile->getKey(),
|
|
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
|
'baseline_compare' => [
|
|
'reason_code' => BaselineCompareReasonCode::NoDriftDetected->value,
|
|
'coverage' => [
|
|
'effective_types' => ['deviceConfiguration'],
|
|
'covered_types' => ['deviceConfiguration'],
|
|
'uncovered_types' => [],
|
|
'proof' => true,
|
|
],
|
|
],
|
|
],
|
|
]);
|
|
|
|
setAdminPanelContext($tenant);
|
|
|
|
Livewire::test(NeedsAttention::class)
|
|
->assertSee('Backups are recent and healthy')
|
|
->assertSee('No recent restore issues visible')
|
|
->assertSee('Baseline compare looks trustworthy')
|
|
->assertDontSee('Backup schedules need follow-up')
|
|
->assertDontSee('No usable backup basis');
|
|
});
|
|
|
|
it('surfaces missing restore history and suppresses the healthy fallback when recovery evidence is unvalidated', function (): void {
|
|
CarbonImmutable::setTestNow(CarbonImmutable::create(2026, 4, 7, 12, 0, 0, 'UTC'));
|
|
|
|
[$user, $tenant] = createNeedsAttentionTenant();
|
|
$this->actingAs($user);
|
|
|
|
$backupSet = makeHealthyBackupForRecoveryNeedsAttention($tenant);
|
|
|
|
RestoreRun::factory()
|
|
->for($tenant)
|
|
->for($backupSet)
|
|
->previewOnly()
|
|
->create([
|
|
'completed_at' => now()->subMinutes(5),
|
|
]);
|
|
|
|
setAdminPanelContext($tenant);
|
|
|
|
$component = Livewire::test(NeedsAttention::class)
|
|
->assertSee('Recovery evidence is unvalidated')
|
|
->assertSee('No executed restore history is visible in the latest tenant restore records.')
|
|
->assertSee('Backup health reflects backup inputs only and does not prove restore success.')
|
|
->assertSee('Open restore history')
|
|
->assertDontSee('Current governance and findings signals look trustworthy.')
|
|
->assertDontSee('Backups are recent and healthy');
|
|
|
|
expect($component->html())->toContain(RestoreRunResource::getUrl('index', [
|
|
'recovery_posture_reason' => 'no_history',
|
|
], panel: 'admin', tenant: $tenant));
|
|
});
|
|
|
|
it('surfaces recent weak restore history in needs-attention with the matching restore drillthrough', function (
|
|
Closure $makeRestoreRun,
|
|
string $expectedTitle,
|
|
string $expectedBody,
|
|
string $expectedReason,
|
|
): void {
|
|
CarbonImmutable::setTestNow(CarbonImmutable::create(2026, 4, 7, 12, 0, 0, 'UTC'));
|
|
|
|
[$user, $tenant] = createNeedsAttentionTenant();
|
|
$this->actingAs($user);
|
|
|
|
makeHealthyBackupForRecoveryNeedsAttention($tenant);
|
|
|
|
$restoreBackupSet = BackupSet::factory()->for($tenant)->create([
|
|
'name' => 'Recovery attention restore backup',
|
|
]);
|
|
|
|
$restoreRun = $makeRestoreRun($tenant, $restoreBackupSet);
|
|
|
|
setAdminPanelContext($tenant);
|
|
|
|
$component = Livewire::test(NeedsAttention::class)
|
|
->assertSee($expectedTitle)
|
|
->assertSee($expectedBody)
|
|
->assertSee('Open restore run')
|
|
->assertDontSee('Current governance and findings signals look trustworthy.');
|
|
|
|
expect($component->html())->toContain(RestoreRunResource::getUrl('view', [
|
|
'record' => (int) $restoreRun->getKey(),
|
|
'recovery_posture_reason' => $expectedReason,
|
|
], panel: 'admin', tenant: $tenant));
|
|
})->with('needs-attention-recovery-cases');
|
|
|
|
it('adds a calm recovery healthy-check without claiming tenant-wide recovery proof', function (): void {
|
|
CarbonImmutable::setTestNow(CarbonImmutable::create(2026, 4, 7, 12, 0, 0, 'UTC'));
|
|
|
|
[$user, $tenant, $profile, $snapshot] = createNeedsAttentionTenant();
|
|
$this->actingAs($user);
|
|
|
|
makeHealthyBackupForRecoveryNeedsAttention($tenant);
|
|
|
|
OperationRun::factory()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'type' => OperationRunType::BaselineCompare->value,
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Succeeded->value,
|
|
'completed_at' => now()->subHour(),
|
|
'context' => [
|
|
'baseline_profile_id' => (int) $profile->getKey(),
|
|
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
|
'baseline_compare' => [
|
|
'reason_code' => BaselineCompareReasonCode::NoDriftDetected->value,
|
|
'coverage' => [
|
|
'effective_types' => ['deviceConfiguration'],
|
|
'covered_types' => ['deviceConfiguration'],
|
|
'uncovered_types' => [],
|
|
'proof' => true,
|
|
],
|
|
],
|
|
],
|
|
]);
|
|
|
|
$restoreBackupSet = BackupSet::factory()->for($tenant)->create([
|
|
'name' => 'Calm recovery restore backup',
|
|
]);
|
|
|
|
RestoreRun::withoutEvents(fn (): RestoreRun => RestoreRun::factory()
|
|
->for($tenant)
|
|
->for($restoreBackupSet)
|
|
->completedOutcome()
|
|
->create([
|
|
'completed_at' => now()->subMinutes(10),
|
|
]));
|
|
|
|
setAdminPanelContext($tenant);
|
|
|
|
Livewire::test(NeedsAttention::class)
|
|
->assertSee('Current governance and findings signals look trustworthy.')
|
|
->assertSee('Backups are recent and healthy')
|
|
->assertSee('No recent restore issues visible')
|
|
->assertSee('Recent executed restore history exists without a current follow-up signal.')
|
|
->assertSee('Target environment recovery is not proven.')
|
|
->assertDontSee('Recovery evidence is unvalidated')
|
|
->assertDontSee('Recent restore failed');
|
|
});
|
|
|
|
it('keeps backup-health attention visible but non-clickable when the member lacks backup view access', function (): void {
|
|
CarbonImmutable::setTestNow(CarbonImmutable::create(2026, 4, 7, 12, 0, 0, 'UTC'));
|
|
|
|
[$user, $tenant] = createNeedsAttentionTenant();
|
|
$this->actingAs($user);
|
|
|
|
$backupSet = BackupSet::factory()->for($tenant)->create([
|
|
'name' => 'Stale hidden backup',
|
|
'item_count' => 1,
|
|
'completed_at' => now()->subDays(2),
|
|
]);
|
|
|
|
BackupItem::factory()->for($tenant)->for($backupSet)->create([
|
|
'payload' => ['id' => 'policy-stale'],
|
|
'metadata' => [],
|
|
'assignments' => [],
|
|
]);
|
|
|
|
Gate::define(Capabilities::TENANT_VIEW, fn (): bool => false);
|
|
|
|
setAdminPanelContext($tenant);
|
|
|
|
$component = Livewire::test(NeedsAttention::class)
|
|
->assertSee('Latest backup is stale')
|
|
->assertSee('Open latest backup')
|
|
->assertSee(UiTooltips::INSUFFICIENT_PERMISSION);
|
|
|
|
expect($component->html())->not->toContain(BackupSetResource::getUrl('view', [
|
|
'record' => (int) $backupSet->getKey(),
|
|
'backup_health_reason' => TenantBackupHealthAssessment::REASON_LATEST_BACKUP_STALE,
|
|
], panel: 'admin', tenant: $tenant));
|
|
});
|