TenantAtlas/tests/Feature/Filament/NeedsAttentionWidgetTest.php
ahmido 55aef627aa feat: harden finding governance health surfaces (#197)
## Summary
- harden findings and finding-exception Filament surfaces so workflow state, governance validity, overdue urgency, and next action are operator-first
- add tenant stats widgets, segmented tabs, richer governance warnings, and baseline/dashboard attention propagation for overdue and lapsed governance states
- add Spec 166 artifacts plus regression coverage for findings, badges, baseline summaries, tenantless operation viewer behavior, and critical table standards

## Verification
- `vendor/bin/sail bin pint --dirty --format agent`
- `vendor/bin/sail artisan test --compact`

## Filament Notes
- Livewire v4.0+ compliance: yes, implementation stays on Filament v5 / Livewire v4 APIs only
- Provider registration: unchanged, Laravel 12 panel/provider registration remains in `bootstrap/providers.php`
- Global search: unchanged in this slice; `FindingExceptionResource` stays not globally searchable, no new globally searchable resource was introduced
- Destructive actions: existing revoke/reject/approve/renew/workflow mutations remain capability-gated and confirmation-gated where already defined
- Asset strategy: no new assets added; existing deploy process remains unchanged, including `php artisan filament:assets` when registered assets are used
- Testing plan delivered: findings list/detail, exception register, dashboard attention, baseline summary, badge semantics, and tenantless operation viewer coverage

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #197
2026-03-28 10:11:12 +00:00

196 lines
7.4 KiB
PHP

<?php
declare(strict_types=1);
use App\Filament\Widgets\Dashboard\NeedsAttention;
use App\Models\BaselineProfile;
use App\Models\BaselineSnapshot;
use App\Models\BaselineTenantAssignment;
use App\Models\Finding;
use App\Models\OperationRun;
use App\Support\Baselines\BaselineCompareReasonCode;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\OperationRunType;
use Filament\Facades\Filament;
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,
'tenant_id' => (int) $tenant->getKey(),
'baseline_profile_id' => (int) $profile->getKey(),
]);
return [$user, $tenant, $profile, $snapshot];
}
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([
'tenant_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' => [
'policy_record_missing' => 2,
],
],
],
],
]);
Filament::setCurrentPanel(Filament::getPanel('tenant'));
Filament::setTenant($tenant, true);
$component = Livewire::test(NeedsAttention::class)
->assertSee('Needs Attention')
->assertSee('Baseline compare posture')
->assertSee('The last compare finished, but normal result output was suppressed.')
->assertSee('Review compare detail')
->assertDontSee('Current dashboard signals look trustworthy.');
expect($component->html())->not->toContain('href=');
});
it('keeps needs-attention non-navigational and healthy only for trustworthy compare results', function (): void {
[$user, $tenant, $profile, $snapshot] = createNeedsAttentionTenant();
$this->actingAs($user);
OperationRun::factory()->create([
'tenant_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,
],
],
],
]);
Filament::setCurrentPanel(Filament::getPanel('tenant'));
Filament::setTenant($tenant, true);
$component = Livewire::test(NeedsAttention::class)
->assertSee('Current dashboard signals look trustworthy.')
->assertSee('Baseline compare looks trustworthy')
->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([
'tenant_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,
],
],
],
]);
Filament::setCurrentPanel(Filament::getPanel('tenant'));
Filament::setTenant($tenant, true);
Livewire::test(NeedsAttention::class)
->assertSee('Baseline compare posture')
->assertSee('The latest baseline compare result is stale.')
->assertSee('Open Baseline Compare')
->assertDontSee('Current dashboard 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);
Filament::setCurrentPanel(Filament::getPanel('tenant'));
Filament::setTenant($tenant, true);
Livewire::test(NeedsAttention::class)
->assertSee('Baseline compare posture')
->assertSee('A current baseline compare result is not available yet.')
->assertSee('Open Baseline Compare')
->assertDontSee('Current dashboard 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,
]);
Filament::setCurrentPanel(Filament::getPanel('tenant'));
Filament::setTenant($tenant, true);
Livewire::test(NeedsAttention::class)
->assertSee('Overdue findings')
->assertSee('Lapsed accepted-risk governance')
->assertDontSee('Current dashboard signals look trustworthy.');
});