TenantAtlas/apps/platform/tests/Unit/Support/GovernanceInbox/GovernanceInboxSectionBuilderTest.php
ahmido 72bfb37ba7
Some checks failed
Main Confidence / confidence (push) Failing after 57s
feat: add decision-based governance inbox (#291)
## Summary
- add a read-first governance inbox page at `/admin/governance/inbox`
- aggregate assigned findings, intake, stale operations, alert-delivery failures, and review follow-up into one canonical routing surface
- add focused coverage for inbox authorization, navigation context, page behavior, and section builder logic
- include the Spec Kit artifacts for spec 250

## Notes
- branch is synced with `dev`
- this PR supersedes #290 for the governance inbox work

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #291
2026-04-28 10:13:09 +00:00

197 lines
7.3 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\AlertDelivery;
use App\Models\Finding;
use App\Models\OperationRun;
use App\Models\Tenant;
use App\Models\TenantTriageReview;
use App\Models\User;
use App\Models\Workspace;
use App\Support\Auth\Capabilities;
use App\Support\BackupHealth\TenantBackupHealthResolver;
use App\Support\GovernanceInbox\GovernanceInboxSectionBuilder;
use App\Support\Navigation\CanonicalNavigationContext;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\PortfolioTriage\PortfolioArrivalContextToken;
use App\Support\PortfolioTriage\TenantTriageReviewFingerprint;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
it('builds visible governance inbox sections in canonical order with source links', function (): void {
$workspace = Workspace::factory()->create();
$user = User::factory()->create();
$alphaTenant = Tenant::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'name' => 'Alpha Tenant',
'external_id' => 'alpha-tenant',
]);
$bravoTenant = Tenant::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'name' => 'Bravo Tenant',
'external_id' => 'bravo-tenant',
]);
Finding::factory()
->for($alphaTenant)
->assignedTo((int) $user->getKey())
->ownedBy((int) $user->getKey())
->overdueByHours()
->create([
'status' => Finding::STATUS_IN_PROGRESS,
'subject_external_id' => 'assigned-finding',
]);
Finding::factory()
->for($bravoTenant)
->reopened()
->create([
'subject_external_id' => 'intake-finding',
]);
OperationRun::factory()
->forTenant($alphaTenant)
->create([
'status' => OperationRunStatus::Completed->value,
'outcome' => OperationRunOutcome::Failed->value,
'completed_at' => now()->subMinute(),
]);
OperationRun::factory()
->forTenant($bravoTenant)
->create([
'status' => OperationRunStatus::Queued->value,
'outcome' => OperationRunOutcome::Pending->value,
'created_at' => now()->subMinutes(6),
]);
AlertDelivery::factory()->create([
'tenant_id' => null,
'workspace_id' => (int) $workspace->getKey(),
'status' => AlertDelivery::STATUS_FAILED,
'event_type' => 'alerts.failed_delivery',
'payload' => [
'title' => 'Delivery failed',
'body' => 'Alert delivery could not be completed.',
],
]);
$backupHealthResolver = app(TenantBackupHealthResolver::class);
$fingerprints = app(TenantTriageReviewFingerprint::class);
$alphaBackupFingerprint = $fingerprints->forBackupHealth($backupHealthResolver->assess($alphaTenant));
$bravoBackupFingerprint = $fingerprints->forBackupHealth($backupHealthResolver->assess($bravoTenant));
expect($alphaBackupFingerprint)->not->toBeNull()
->and($bravoBackupFingerprint)->not->toBeNull();
TenantTriageReview::factory()
->for($alphaTenant)
->followUpNeeded()
->create([
'workspace_id' => (int) $workspace->getKey(),
'reviewed_by_user_id' => (int) $user->getKey(),
'concern_family' => PortfolioArrivalContextToken::FAMILY_BACKUP_HEALTH,
'review_fingerprint' => $alphaBackupFingerprint['fingerprint'],
'review_snapshot' => $alphaBackupFingerprint['snapshot'],
'reviewed_at' => now()->subDay(),
]);
TenantTriageReview::factory()
->for($bravoTenant)
->reviewed()
->create([
'workspace_id' => (int) $workspace->getKey(),
'reviewed_by_user_id' => (int) $user->getKey(),
'concern_family' => PortfolioArrivalContextToken::FAMILY_BACKUP_HEALTH,
'review_fingerprint' => hash('sha256', 'stale-review-fingerprint'),
'review_snapshot' => $bravoBackupFingerprint['snapshot'],
'reviewed_at' => now()->subDays(2),
]);
$context = new CanonicalNavigationContext(
sourceSurface: 'governance.inbox',
canonicalRouteName: 'filament.admin.pages.governance.inbox',
backLinkLabel: 'Back to governance inbox',
backLinkUrl: '/admin/governance/inbox',
);
$payload = app(GovernanceInboxSectionBuilder::class)->build(
user: $user,
workspace: $workspace,
authorizedTenants: [$alphaTenant, $bravoTenant],
visibleFindingTenants: [$alphaTenant, $bravoTenant],
reviewTenants: [$alphaTenant, $bravoTenant],
canViewAlerts: true,
navigationContext: $context,
);
expect(collect($payload['sections'])->pluck('key')->all())
->toBe([
'assigned_findings',
'intake_findings',
'stale_operations',
'alert_delivery_failures',
'review_follow_up',
])
->and($payload['family_counts'])->toMatchArray([
'assigned_findings' => 1,
'intake_findings' => 1,
'stale_operations' => 2,
'alert_delivery_failures' => 1,
'review_follow_up' => 2,
]);
$sections = collect($payload['sections'])->keyBy('key');
expect($sections['assigned_findings']['dominant_action_url'])
->toContain('/admin/findings/my-work')
->toContain('nav%5Bback_label%5D=Back+to+governance+inbox')
->and($sections['stale_operations']['dominant_action_label'])->toBe('Open terminal follow-up')
->and($sections['stale_operations']['dominant_action_url'])->toContain('problemClass=terminal_follow_up')
->and($sections['alert_delivery_failures']['dominant_action_url'])->toContain('tableFilters%5Bstatus%5D%5Bvalue%5D=failed')
->and(collect($sections['review_follow_up']['entries'])->pluck('status_label')->all())
->toBe(['Follow-up needed', 'Changed since review']);
});
it('keeps an explicitly selected visible family with an honest empty state when tenant filtering removes every row', function (): void {
$workspace = Workspace::factory()->create();
$user = User::factory()->create();
$alphaTenant = Tenant::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'name' => 'Alpha Tenant',
'external_id' => 'alpha-tenant',
]);
$bravoTenant = Tenant::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'name' => 'Bravo Tenant',
'external_id' => 'bravo-tenant',
]);
AlertDelivery::factory()->create([
'tenant_id' => null,
'workspace_id' => (int) $workspace->getKey(),
'status' => AlertDelivery::STATUS_FAILED,
]);
$payload = app(GovernanceInboxSectionBuilder::class)->build(
user: $user,
workspace: $workspace,
authorizedTenants: [$alphaTenant, $bravoTenant],
visibleFindingTenants: [],
reviewTenants: [],
canViewAlerts: true,
selectedTenant: $alphaTenant,
selectedFamily: 'alert_delivery_failures',
);
expect($payload['sections'])->toHaveCount(1)
->and($payload['sections'][0]['key'])->toBe('alert_delivery_failures')
->and($payload['sections'][0]['count'])->toBe(0)
->and($payload['sections'][0]['empty_state'])->toContain('tenant filter');
});