toBeFile() ->and($laneContract)->toBeFile(); $repoTruth = (string) file_get_contents($repoTruthMap); $laneClassification = (string) file_get_contents($laneContract); expect($repoTruth) ->toContain('environment_id') ->toContain('GovernanceDecisionRegisterBuilder') ->toContain('Review-ready') ->toContain('Not added.') ->and($laneClassification) ->toContain('Needs triage') ->toContain('Requires decision') ->toContain('Risk / exception review') ->toContain('Evidence required') ->toContain('Blocked'); }); it('renders the Spec 346 operator summary, active lanes, and linked actions before source detail', function (): void { [$user, $environmentA, $environmentB] = spec346GovernanceInboxFixture(); $response = $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $environmentA->workspace_id]) ->get(GovernanceInbox::getUrl(panel: 'admin')); $response ->assertOk() ->assertSee('Governance Inbox') ->assertSee('Daily operator queue for governance follow-up, accepted risk, evidence gaps, and review handoff.') ->assertSee('Open governance work') ->assertSee('Primary inbox lanes') ->assertSee('Needs triage') ->assertSee('Requires decision') ->assertSee('Risk / exception review') ->assertSee('Evidence required') ->assertSee('Blocked') ->assertSee('Next recommended action') ->assertSee('Triage Finding #') ->assertSee('Linked records') ->assertSee('Secondary actions') ->assertSee('More context') ->assertSee('Open Customer Review Workspace') ->assertSee('The finding is unassigned and still needs first triage.') ->assertSee('The finding remains assigned and needs follow-up before it can be cleared.') ->assertSee('Spec346 accepted-risk support review') ->assertSee('The operation reached a terminal outcome that still needs monitoring follow-up.') ->assertSee('The latest review state asks for follow-up before this governance concern is closed.') ->assertSee('Recently resolved') ->assertSee('Source detail') ->assertSee('Diagnostics / source detail') ->assertDontSee('Decision workbench') ->assertDontSee('Queue context') ->assertDontSee('raw payload should stay hidden'); $content = (string) $response->getContent(); expect(strpos($content, 'Open governance work'))->toBeLessThan(strpos($content, 'Primary inbox lanes')) ->and(strpos($content, 'Primary inbox lanes'))->toBeLessThan(strpos($content, 'Source detail')) ->and(strpos($content, 'Primary inbox lanes'))->toBeLessThan(strpos($content, 'Recently resolved')); expect($content)->not->toContain('No governance items need attention.'); expect($environmentB->workspace_id)->toBe($environmentA->workspace_id); }); it('normalizes rendered lane, action, badge, and link contracts before Blade consumes them', function (): void { [$user, $environmentA] = spec346GovernanceInboxFixture(); $this->actingAs($user); session()->put(WorkspaceContext::SESSION_KEY, (int) $environmentA->workspace_id); $instance = Livewire::test(GovernanceInbox::class) ->instance(); $summary = $instance->operatorSummary(); $lanes = $instance->laneGroups(); expect($summary['next_recommended_item']) ->toBeArray() ->and($summary['next_recommended_item']) ->toHaveKeys([ 'headline', 'lane_label', 'status_label', 'environment_label', 'reason_label', 'impact_label', 'primary_action', ]) ->and($summary['next_recommended_item']['primary_action']) ->toHaveKeys(['label', 'url']); foreach ($summary['counts'] as $count) { expect($count)->toHaveKeys(['key', 'label', 'count', 'description', 'state', 'chip_label']); } foreach ($lanes as $lane) { expect($lane)->toHaveKeys(['key', 'label', 'anchor_id', 'count', 'items']); foreach ($lane['items'] as $item) { expect($item)->toHaveKeys([ 'lane_label', 'status_label', 'title', 'environment_label', 'reason_heading', 'reason_label', 'impact_label', 'source_label', 'owner_label', 'due_label', 'evidence_label', 'exception_label', 'primary_action', 'secondary_actions', 'linked_records', ]); expect($item['primary_action'])->toHaveKeys(['label', 'url']); foreach ([...$item['secondary_actions'], ...$item['linked_records']] as $link) { expect($link)->toHaveKeys(['label', 'url']); } } } }); it('renders the Spec 346 productized empty state without looking like missing data', function (): void { $environment = ManagedEnvironment::factory()->active()->create([ 'name' => 'Spec346 Empty Environment', ]); [$user, $environment] = createUserWithTenant($environment, role: 'owner', workspaceRole: 'owner'); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $environment->workspace_id]) ->get(GovernanceInbox::getUrl(panel: 'admin')) ->assertOk() ->assertSee('Open governance work') ->assertSee('No governance items need attention.') ->assertSee('Findings, decisions, accepted-risk reviews, evidence gaps, and review follow-ups will appear here when they need operator attention.') ->assertSee('Diagnostics / source detail') ->assertDontSee('Decision workbench') ->assertDontSee('No records found') ->assertDontSee('Missing data'); }); it('renders blocked governance follow-up with explicit blocker reason and next action', function (): void { $environment = ManagedEnvironment::factory()->active()->create([ 'name' => 'Spec346 Blocked Environment', ]); [$user, $environment] = createUserWithTenant($environment, role: 'owner', workspaceRole: 'owner'); OperationRun::factory() ->forTenant($environment) ->create([ 'status' => OperationRunStatus::Completed->value, 'outcome' => OperationRunOutcome::Failed->value, 'completed_at' => now()->subMinutes(5), ]); $response = $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $environment->workspace_id]) ->get(GovernanceInbox::getUrl(panel: 'admin')); $response ->assertOk() ->assertSee('Blocked') ->assertSee('Blocker') ->assertSee('The operation reached a terminal outcome that still needs monitoring follow-up.') ->assertSee('Open operation proof') ->assertDontSee('No governance items need attention.'); }); it('keeps recently resolved items secondary to active operator work', function (): void { [$user, $environment] = spec346RecentlyResolvedOnlyFixture(); $response = $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $environment->workspace_id]) ->get(GovernanceInbox::getUrl(panel: 'admin')); $response ->assertOk() ->assertSee('Recently resolved') ->assertSee('Open recently closed decisions') ->assertSee('Recently rejected closure reason'); $content = (string) $response->getContent(); expect(strpos($content, 'Primary inbox lanes'))->toBeLessThan(strpos($content, 'Recently resolved')); }); /** * @return array{0: User, 1: ManagedEnvironment, 2: ManagedEnvironment} */ function spec346GovernanceInboxFixture(): array { $environmentA = ManagedEnvironment::factory()->active()->create([ 'name' => 'Spec346 Environment Alpha', 'external_id' => 'spec346-environment-alpha', ]); [$user, $environmentA] = createUserWithTenant($environmentA, role: 'owner', workspaceRole: 'owner'); $environmentB = ManagedEnvironment::factory()->active()->create([ 'workspace_id' => (int) $environmentA->workspace_id, 'name' => 'Spec346 Environment Beta', 'external_id' => 'spec346-environment-beta', ]); createUserWithTenant($environmentB, user: $user, role: 'owner', workspaceRole: 'owner'); Finding::factory() ->for($environmentA) ->create([ 'workspace_id' => (int) $environmentA->workspace_id, 'status' => Finding::STATUS_NEW, 'owner_user_id' => null, 'assignee_user_id' => null, 'evidence_jsonb' => [], ]); Finding::factory() ->for($environmentA) ->assignedTo((int) $user->getKey()) ->ownedBy((int) $user->getKey()) ->create([ 'workspace_id' => (int) $environmentA->workspace_id, 'status' => Finding::STATUS_IN_PROGRESS, 'evidence_jsonb' => [], ]); Finding::factory() ->for($environmentB) ->assignedTo((int) $user->getKey()) ->ownedBy((int) $user->getKey()) ->create([ 'workspace_id' => (int) $environmentB->workspace_id, 'status' => Finding::STATUS_IN_PROGRESS, 'evidence_jsonb' => [ 'summary' => [ 'kind' => 'policy_snapshot', 'raw_payload' => 'raw payload should stay hidden', ], ], ]); $exceptionFinding = Finding::factory() ->for($environmentA) ->riskAccepted() ->create([ 'workspace_id' => (int) $environmentA->workspace_id, ]); FindingException::query()->create([ 'workspace_id' => (int) $environmentA->workspace_id, 'managed_environment_id' => (int) $environmentA->getKey(), 'finding_id' => (int) $exceptionFinding->getKey(), 'requested_by_user_id' => (int) $user->getKey(), 'owner_user_id' => (int) $user->getKey(), 'status' => FindingException::STATUS_PENDING, 'current_validity_state' => FindingException::VALIDITY_MISSING_SUPPORT, 'request_reason' => 'Spec346 accepted-risk support review', 'requested_at' => now()->subDay(), 'review_due_at' => now()->addDays(3), 'evidence_summary' => ['reference_count' => 0], ]); OperationRun::factory() ->forTenant($environmentA) ->create([ 'status' => OperationRunStatus::Completed->value, 'outcome' => OperationRunOutcome::Failed->value, 'completed_at' => now()->subMinutes(10), ]); spec346SeedReviewFollowUp($environmentA, $user); spec346SeedRecentlyClosedDecision($environmentA, $user, 'Recently rejected closure reason'); return [$user, $environmentA, $environmentB]; } /** * @return array{0: User, 1: ManagedEnvironment} */ function spec346RecentlyResolvedOnlyFixture(): array { $environment = ManagedEnvironment::factory()->active()->create([ 'name' => 'Spec346 Resolved Environment', 'external_id' => 'spec346-resolved-environment', ]); [$user, $environment] = createUserWithTenant($environment, role: 'owner', workspaceRole: 'owner'); Finding::factory() ->for($environment) ->create([ 'workspace_id' => (int) $environment->workspace_id, 'status' => Finding::STATUS_NEW, 'owner_user_id' => null, 'assignee_user_id' => null, 'evidence_jsonb' => [], ]); spec346SeedRecentlyClosedDecision($environment, $user, 'Recently rejected closure reason'); return [$user, $environment]; } function spec346SeedReviewFollowUp(ManagedEnvironment $environment, User $user): void { $backupHealthResolver = app(TenantBackupHealthResolver::class); $fingerprints = app(ManagedEnvironmentTriageReviewFingerprint::class); $fingerprint = $fingerprints->forBackupHealth($backupHealthResolver->assess($environment)); ManagedEnvironmentTriageReview::factory() ->for($environment) ->followUpNeeded() ->create([ 'workspace_id' => (int) $environment->workspace_id, 'reviewed_by_user_id' => (int) $user->getKey(), 'concern_family' => PortfolioArrivalContextToken::FAMILY_BACKUP_HEALTH, 'review_fingerprint' => $fingerprint['fingerprint'], 'review_snapshot' => $fingerprint['snapshot'], ]); } function spec346SeedRecentlyClosedDecision(ManagedEnvironment $environment, User $user, string $reason): void { $finding = Finding::factory() ->for($environment) ->create([ 'workspace_id' => (int) $environment->workspace_id, 'status' => Finding::STATUS_IN_PROGRESS, ]); $exception = FindingException::query()->create([ 'workspace_id' => (int) $environment->workspace_id, 'managed_environment_id' => (int) $environment->getKey(), 'finding_id' => (int) $finding->getKey(), 'requested_by_user_id' => (int) $user->getKey(), 'owner_user_id' => (int) $user->getKey(), 'status' => FindingException::STATUS_REJECTED, 'current_validity_state' => FindingException::VALIDITY_REJECTED, 'request_reason' => 'Closed exception for Spec346 register follow-up', 'requested_at' => now()->subDays(5), 'review_due_at' => now()->subDays(2), 'rejected_at' => now()->subDays(1), 'evidence_summary' => ['reference_count' => 0], ]); $decision = FindingExceptionDecision::query()->create([ 'workspace_id' => (int) $environment->workspace_id, 'managed_environment_id' => (int) $environment->getKey(), 'finding_exception_id' => (int) $exception->getKey(), 'actor_user_id' => (int) $user->getKey(), 'decision_type' => FindingExceptionDecision::TYPE_REJECTED, 'reason' => $reason, 'decided_at' => now()->subDay(), 'metadata' => [], ]); $exception->forceFill([ 'current_decision_id' => (int) $decision->getKey(), ])->save(); }