active()->create([ 'name' => 'Spec389 Publishing Tenant', ]); [$user, $tenant] = createUserWithTenant($tenant, role: 'owner', workspaceRole: 'owner', ensureDefaultMicrosoftProviderConnection: true); spec389CreateResolutionCase($tenant, $user, [ 'status' => ReviewPublicationResolutionCaseStatus::InProgress->value, 'current_step_key' => ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot->value, ], [ 'step_key' => ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot->value, 'status' => ReviewPublicationResolutionStepStatus::Actionable->value, ]); spec389CreateResolutionCase($tenant, $user, [ 'status' => ReviewPublicationResolutionCaseStatus::Completed->value, 'current_step_key' => null, 'summary' => ['label' => 'Spec389 completed hidden case'], ], [ 'step_key' => ReviewPublicationResolutionStepKey::ReturnToPublication->value, 'status' => ReviewPublicationResolutionStepStatus::Completed->value, ]); spec389CreateResolutionCase($tenant, $user, [ 'status' => ReviewPublicationResolutionCaseStatus::Cancelled->value, 'current_step_key' => null, 'summary' => ['label' => 'Spec389 cancelled hidden case'], ], [ 'step_key' => ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot->value, 'status' => ReviewPublicationResolutionStepStatus::Superseded->value, ]); spec389CreateResolutionCase($tenant, $user, [ 'status' => ReviewPublicationResolutionCaseStatus::Superseded->value, 'current_step_key' => null, 'summary' => ['label' => 'Spec389 superseded hidden case'], ], [ 'step_key' => ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot->value, 'status' => ReviewPublicationResolutionStepStatus::Superseded->value, ]); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get(GovernanceInbox::getUrl(panel: 'admin', parameters: [ 'family' => 'review_publication_resolution', ])) ->assertOk() ->assertSee('Review publication work') ->assertSee('Review cannot be published yet') ->assertSee('A current evidence snapshot is required.') ->assertSee('Continue preparation') ->assertSee('Review publication status') ->assertSee('Updated: Any time') ->assertDontSee('Spec389 completed hidden case') ->assertDontSee('Spec389 cancelled hidden case') ->assertDontSee('Spec389 superseded hidden case') ->assertDontSee('Operation #'); }); it('Spec389 sorts publication work by severity before updated time', function (): void { $tenant = ManagedEnvironment::factory()->active()->create([ 'name' => 'Spec389 Sort Tenant', ]); [$user, $tenant] = createUserWithTenant($tenant, role: 'owner', workspaceRole: 'owner'); [$failedCase, $failedStep, $failedReview] = spec389CreateResolutionCase($tenant, $user, [ 'status' => ReviewPublicationResolutionCaseStatus::Blocked->value, 'current_step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value, ], [ 'step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value, 'status' => ReviewPublicationResolutionStepStatus::Failed->value, ]); $run = spec389ResolutionOperationRun($tenant, $failedCase, $failedReview, [ 'type' => OperationRunType::EntraAdminRolesScan->value, 'status' => OperationRunStatus::Completed->value, 'outcome' => OperationRunOutcome::Failed->value, ]); spec389AttachResolutionOperationProof($failedStep, $run, proofStatus: OperationRunOutcome::Failed->value); spec389CreateResolutionCase($tenant, $user, [ 'status' => ReviewPublicationResolutionCaseStatus::Blocked->value, 'current_step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value, 'updated_at' => now()->subMinute(), ], [ 'step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value, 'status' => ReviewPublicationResolutionStepStatus::Actionable->value, 'summary' => ['missing_report_dimensions' => ['unsupported_report']], ]); spec389CreateResolutionCase($tenant, $user, [ 'status' => ReviewPublicationResolutionCaseStatus::InProgress->value, 'current_step_key' => ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot->value, 'updated_at' => now()->subMinutes(2), ], [ 'step_key' => ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot->value, 'status' => ReviewPublicationResolutionStepStatus::Actionable->value, ]); spec389CreateResolutionCase($tenant, $user, [ 'status' => ReviewPublicationResolutionCaseStatus::Blocked->value, 'current_step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value, 'updated_at' => now()->subMinutes(3), ], [ 'step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value, 'status' => ReviewPublicationResolutionStepStatus::Failed->value, ]); [, $readyStep, $readyReview] = spec389CreateResolutionCase($tenant, $user, [ 'status' => ReviewPublicationResolutionCaseStatus::ReadyToContinue->value, 'current_step_key' => ReviewPublicationResolutionStepKey::ReturnToPublication->value, 'updated_at' => now()->subMinutes(4), ], [ 'step_key' => ReviewPublicationResolutionStepKey::ReturnToPublication->value, 'status' => ReviewPublicationResolutionStepStatus::Actionable->value, ]); spec389AttachReadyToContinueProof($readyStep, $readyReview); [$waitingCase, $waitingStep, $waitingReview] = spec389CreateResolutionCase($tenant, $user, [ 'status' => ReviewPublicationResolutionCaseStatus::InProgress->value, 'current_step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value, 'updated_at' => now(), ], [ 'step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value, 'status' => ReviewPublicationResolutionStepStatus::Running->value, ]); $waitingRun = spec389ResolutionOperationRun($tenant, $waitingCase, $waitingReview, [ 'type' => OperationRunType::EntraAdminRolesScan->value, 'status' => OperationRunStatus::Running->value, 'outcome' => OperationRunOutcome::Pending->value, ]); spec389AttachResolutionOperationProof($waitingStep, $waitingRun, proofStatus: OperationRunStatus::Running->value); $response = $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get(GovernanceInbox::getUrl(panel: 'admin', parameters: [ 'family' => 'review_publication_resolution', ])); $response ->assertOk() ->assertSee('Review publication work'); $section = spec389ReviewPublicationProviderSection($user, $tenant, previewLimit: 10); $statuses = collect($section['entries'] ?? [])->pluck('inbox_status')->all(); expect($statuses)->toBe([ 'failed', 'blocked', 'needs_attention', 'needs_recheck', 'ready_to_continue', 'waiting', ]); $waitingEntry = collect($section['entries'] ?? []) ->firstWhere('inbox_status', 'waiting'); expect($waitingEntry['primary_action_label'] ?? null)->toBe('Open operation') ->and(collect($waitingEntry['secondary_actions'] ?? [])->pluck('label')->all())->not->toContain('Open operation'); }); it('Spec389 applies derived status and updated-date filters only to review publication work', function (): void { $tenant = ManagedEnvironment::factory()->active()->create([ 'name' => 'Spec389 Filter Tenant', ]); [$user, $tenant] = createUserWithTenant($tenant, role: 'owner', workspaceRole: 'owner', ensureDefaultMicrosoftProviderConnection: true); spec389CreateResolutionCase($tenant, $user, [ 'status' => ReviewPublicationResolutionCaseStatus::InProgress->value, 'current_step_key' => ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot->value, 'updated_at' => now()->subHours(2), ], [ 'step_key' => ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot->value, 'status' => ReviewPublicationResolutionStepStatus::Actionable->value, 'updated_at' => now()->subHours(2), ]); [, $readyStep, $readyReview] = spec389CreateResolutionCase($tenant, $user, [ 'status' => ReviewPublicationResolutionCaseStatus::ReadyToContinue->value, 'current_step_key' => ReviewPublicationResolutionStepKey::ReturnToPublication->value, 'updated_at' => now()->subDays(10), ], [ 'step_key' => ReviewPublicationResolutionStepKey::ReturnToPublication->value, 'status' => ReviewPublicationResolutionStepStatus::Actionable->value, 'updated_at' => now()->subDays(10), ]); spec389AttachReadyToContinueProof($readyStep, $readyReview); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get(GovernanceInbox::getUrl(panel: 'admin', parameters: [ 'family' => 'review_publication_resolution', 'status' => 'needs_attention', 'updated' => 'last_24_hours', ])) ->assertOk() ->assertSee('Status: Needs attention') ->assertSee('Updated: Last 24 hours') ->assertSee('Review cannot be published yet') ->assertDontSee('Review preparation can continue'); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get(GovernanceInbox::getUrl(panel: 'admin', parameters: [ 'family' => 'review_publication_resolution', 'status' => 'ready_to_continue', 'updated' => 'last_24_hours', ])) ->assertOk() ->assertSee('These review publication filters are hiding active preparation work') ->assertSee('Clear review publication filters') ->assertDontSee('Review preparation can continue'); }); it('Spec389 falls back to needs re-check when ready-to-continue proof is stale', function (): void { $tenant = ManagedEnvironment::factory()->active()->create([ 'name' => 'Spec389 Stale Ready Tenant', ]); [$user, $tenant] = createUserWithTenant($tenant, role: 'owner', workspaceRole: 'owner'); [, $step, $review] = spec389CreateResolutionCase($tenant, $user, [ 'status' => ReviewPublicationResolutionCaseStatus::ReadyToContinue->value, 'current_step_key' => ReviewPublicationResolutionStepKey::ReturnToPublication->value, ], [ 'step_key' => ReviewPublicationResolutionStepKey::ReturnToPublication->value, 'status' => ReviewPublicationResolutionStepStatus::Actionable->value, ]); spec389AttachReadyToContinueProof($step, $review, [ 'proof_currentness' => ResolutionProofCurrentness::Stale->value, ]); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get(GovernanceInbox::getUrl(panel: 'admin', parameters: [ 'family' => 'review_publication_resolution', ])) ->assertOk() ->assertSee('Review preparation needs re-check') ->assertSee('Inspect preparation') ->assertDontSee('Review preparation can continue'); $section = spec389ReviewPublicationProviderSection($user, $tenant); $entry = collect($section['entries'] ?? [])->first(); expect($entry['inbox_status'] ?? null)->toBe('needs_recheck'); }); it('Spec389 discloses operation links only for safe current linked runs', function (): void { $tenant = ManagedEnvironment::factory()->active()->create([ 'name' => 'Spec389 Operation Tenant', ]); [$user, $tenant] = createUserWithTenant($tenant, role: 'owner', workspaceRole: 'owner'); [$case, $step, $review] = spec389CreateResolutionCase($tenant, $user, [ 'status' => ReviewPublicationResolutionCaseStatus::Blocked->value, 'current_step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value, ], [ 'step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value, 'status' => ReviewPublicationResolutionStepStatus::Failed->value, ]); $run = spec389ResolutionOperationRun($tenant, $case, $review, [ 'type' => OperationRunType::EntraAdminRolesScan->value, 'status' => OperationRunStatus::Completed->value, 'outcome' => OperationRunOutcome::Failed->value, ]); $step->forceFill([ 'operation_run_id' => (int) $run->getKey(), 'proof_type' => 'operation_run', 'proof_id' => (int) $run->getKey(), 'proof_status' => OperationRunOutcome::Failed->value, 'metadata' => spec389SafeOperationProofMetadata(), ])->save(); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get(GovernanceInbox::getUrl(panel: 'admin', parameters: [ 'family' => 'review_publication_resolution', 'status' => 'failed', ])) ->assertOk() ->assertSee('Review preparation action failed') ->assertSee('Open operation') ->assertDontSee('Operation #'); }); it('Spec389 hides operation links when proof currentness cannot be validated', function (): void { $tenant = ManagedEnvironment::factory()->active()->create([ 'name' => 'Spec389 Stale Proof Tenant', ]); [$user, $tenant] = createUserWithTenant($tenant, role: 'owner', workspaceRole: 'owner'); [$case, $step, $review] = spec389CreateResolutionCase($tenant, $user, [ 'status' => ReviewPublicationResolutionCaseStatus::Blocked->value, 'current_step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value, ], [ 'step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value, 'status' => ReviewPublicationResolutionStepStatus::Failed->value, ]); $run = spec389ResolutionOperationRun($tenant, $case, $review, [ 'type' => OperationRunType::EntraAdminRolesScan->value, 'status' => OperationRunStatus::Completed->value, 'outcome' => OperationRunOutcome::Failed->value, ]); $step->forceFill([ 'operation_run_id' => (int) $run->getKey(), 'proof_type' => 'operation_run', 'proof_id' => (int) $run->getKey(), 'proof_status' => OperationRunOutcome::Failed->value, 'metadata' => spec389SafeOperationProofMetadata([ 'proof_currentness' => ResolutionProofCurrentness::Stale->value, ]), ])->save(); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get(GovernanceInbox::getUrl(panel: 'admin', parameters: [ 'family' => 'review_publication_resolution', ])) ->assertOk() ->assertSee('Review preparation needs re-check') ->assertSee('Inspect preparation') ->assertDontSee('Open operation') ->assertDontSee('Operation #'); }); it('Spec389 hides operation links when operation context or proof binding is invalid', function (): void { $tenant = ManagedEnvironment::factory()->active()->create([ 'name' => 'Spec389 Invalid Operation Context Tenant', ]); [$user, $tenant] = createUserWithTenant($tenant, role: 'owner', workspaceRole: 'owner'); [$otherCase, , $otherReview] = spec389CreateResolutionCase($tenant, $user, [ 'status' => ReviewPublicationResolutionCaseStatus::Cancelled->value, 'current_step_key' => null, ], [ 'step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value, 'status' => ReviewPublicationResolutionStepStatus::Superseded->value, ]); $otherRun = spec389ResolutionOperationRun($tenant, $otherCase, $otherReview, [ 'type' => OperationRunType::EntraAdminRolesScan->value, 'status' => OperationRunStatus::Completed->value, 'outcome' => OperationRunOutcome::Failed->value, ]); $scenarios = [ 'missing case context' => function (ReviewPublicationResolutionCase $case, EnvironmentReview $review) use ($tenant): array { return [ 'context' => [ 'workspace_id' => (int) $tenant->workspace_id, 'managed_environment_id' => (int) $tenant->getKey(), 'environment_review_id' => (int) $review->getKey(), 'trigger' => 'review_publication_resolution', ], ]; }, 'missing trigger' => function (ReviewPublicationResolutionCase $case, EnvironmentReview $review) use ($tenant): array { return [ 'context' => [ 'workspace_id' => (int) $tenant->workspace_id, 'managed_environment_id' => (int) $tenant->getKey(), 'environment_review_id' => (int) $review->getKey(), 'review_publication_resolution_case_id' => (int) $case->getKey(), ], ]; }, 'cross case' => function (ReviewPublicationResolutionCase $case, EnvironmentReview $review) use ($tenant, $otherCase): array { return [ 'context' => [ 'workspace_id' => (int) $tenant->workspace_id, 'managed_environment_id' => (int) $tenant->getKey(), 'environment_review_id' => (int) $review->getKey(), 'review_publication_resolution_case_id' => (int) $otherCase->getKey(), 'trigger' => 'review_publication_resolution', ], ]; }, 'cross review' => function (ReviewPublicationResolutionCase $case) use ($tenant, $otherReview): array { return [ 'context' => [ 'workspace_id' => (int) $tenant->workspace_id, 'managed_environment_id' => (int) $tenant->getKey(), 'environment_review_id' => (int) $otherReview->getKey(), 'review_publication_resolution_case_id' => (int) $case->getKey(), 'trigger' => 'review_publication_resolution', ], ]; }, 'wrong type' => fn (): array => [ 'type' => OperationRunType::EvidenceSnapshotGenerate->value, ], 'wrong proof id' => fn (): array => [ 'proof_id' => (int) $otherRun->getKey(), ], ]; foreach ($scenarios as $label => $runOverrides) { [$case, $step, $review] = spec389CreateResolutionCase($tenant, $user, [ 'status' => ReviewPublicationResolutionCaseStatus::Blocked->value, 'current_step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value, 'updated_at' => now()->subMinutes(count($scenarios)), ], [ 'step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value, 'status' => ReviewPublicationResolutionStepStatus::Failed->value, 'summary' => ['scenario' => $label], ]); $overrides = $runOverrides($case, $review); $proofId = is_numeric($overrides['proof_id'] ?? null) ? (int) $overrides['proof_id'] : null; unset($overrides['proof_id']); $run = spec389ResolutionOperationRun($tenant, $case, $review, array_replace([ 'type' => OperationRunType::EntraAdminRolesScan->value, 'status' => OperationRunStatus::Completed->value, 'outcome' => OperationRunOutcome::Failed->value, ], $overrides)); spec389AttachResolutionOperationProof($step, $run, proofId: $proofId, proofStatus: OperationRunOutcome::Failed->value); } $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get(GovernanceInbox::getUrl(panel: 'admin', parameters: [ 'family' => 'review_publication_resolution', ])) ->assertOk() ->assertSee('Review preparation needs re-check') ->assertDontSee('Open operation') ->assertDontSee('Operation #'); $section = spec389ReviewPublicationProviderSection($user, $tenant, previewLimit: 10); $entries = collect($section['entries'] ?? []); expect($entries)->toHaveCount(count($scenarios)) ->and($entries->pluck('inbox_status')->unique()->values()->all())->toBe(['needs_recheck']) ->and(spec389EntryActionLabels($entries->all()))->not->toContain('Open operation'); }); it('Spec389 hides operation links when OperationRunPolicy denies the linked run', function (): void { $tenant = ManagedEnvironment::factory()->active()->create([ 'name' => 'Spec389 Policy Denied Operation Tenant', 'slug' => 'spec389-policy-denied-operation-tenant', ]); [$user, $tenant] = createUserWithTenant($tenant, role: 'owner', workspaceRole: 'owner'); [$case, $step, $review] = spec389CreateResolutionCase($tenant, $user, [ 'status' => ReviewPublicationResolutionCaseStatus::Blocked->value, 'current_step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value, ], [ 'step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value, 'status' => ReviewPublicationResolutionStepStatus::Failed->value, ]); $run = spec389ResolutionOperationRun($tenant, $case, $review, [ 'type' => 'provider.connection.check', 'status' => OperationRunStatus::Completed->value, 'outcome' => OperationRunOutcome::Failed->value, ]); spec389AttachResolutionOperationProof($step, $run, proofStatus: OperationRunOutcome::Failed->value); $originalFixture = config('tenantpilot.backup_health.browser_smoke_fixture'); config([ 'tenantpilot.backup_health.browser_smoke_fixture.user.email' => $user->email, 'tenantpilot.backup_health.browser_smoke_fixture.blocked_drillthrough.tenant_external_id' => (string) $tenant->external_id, 'tenantpilot.backup_health.browser_smoke_fixture.blocked_drillthrough.capability_denials' => [ Capabilities::PROVIDER_VIEW, ], ]); app(CapabilityResolver::class)->clearCache(); try { $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get(GovernanceInbox::getUrl(panel: 'admin', parameters: [ 'family' => 'review_publication_resolution', ])) ->assertOk() ->assertSee('Review preparation needs re-check') ->assertDontSee('Open operation') ->assertDontSee('Operation #'); $section = spec389ReviewPublicationProviderSection($user, $tenant); $entry = collect($section['entries'] ?? [])->first(); expect($entry['inbox_status'] ?? null)->toBe('needs_recheck') ->and(spec389EntryActionLabels([$entry]))->not->toContain('Open operation'); } finally { config(['tenantpilot.backup_health.browser_smoke_fixture' => $originalFixture]); app(CapabilityResolver::class)->clearCache(); } }); it('Spec389 hides resolution cases outside the viewer environment scope', function (): void { $tenant = ManagedEnvironment::factory()->active()->create([ 'name' => 'Spec389 Visible Tenant', ]); [$user, $tenant] = createUserWithTenant($tenant, role: 'owner', workspaceRole: 'owner'); $hiddenTenant = ManagedEnvironment::factory()->active()->create([ 'workspace_id' => (int) $tenant->workspace_id, 'name' => 'Spec389 Hidden Tenant', ]); spec389CreateResolutionCase($tenant, $user, [ 'status' => ReviewPublicationResolutionCaseStatus::InProgress->value, 'current_step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value, ], [ 'step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value, 'status' => ReviewPublicationResolutionStepStatus::Actionable->value, ]); spec389CreateResolutionCase($hiddenTenant, $user, [ 'status' => ReviewPublicationResolutionCaseStatus::InProgress->value, 'current_step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value, ], [ 'step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value, 'status' => ReviewPublicationResolutionStepStatus::Actionable->value, ]); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get(GovernanceInbox::getUrl(panel: 'admin', parameters: [ 'family' => 'review_publication_resolution', ])) ->assertOk() ->assertSee('Spec389 Visible Tenant') ->assertDontSee('Spec389 Hidden Tenant'); }); it('Spec389 hides resolution cases outside the active workspace', function (): void { $tenant = ManagedEnvironment::factory()->active()->create([ 'name' => 'Spec389 Workspace Visible Tenant', ]); [$user, $tenant] = createUserWithTenant($tenant, role: 'owner', workspaceRole: 'owner'); $foreignTenant = ManagedEnvironment::factory()->active()->create([ 'name' => 'Spec389 Foreign Workspace Tenant', ]); [$foreignUser, $foreignTenant] = createUserWithTenant($foreignTenant, role: 'owner', workspaceRole: 'owner'); spec389CreateResolutionCase($tenant, $user, [ 'status' => ReviewPublicationResolutionCaseStatus::InProgress->value, 'current_step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value, ], [ 'step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value, 'status' => ReviewPublicationResolutionStepStatus::Actionable->value, ]); spec389CreateResolutionCase($foreignTenant, $foreignUser, [ 'status' => ReviewPublicationResolutionCaseStatus::InProgress->value, 'current_step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value, ], [ 'step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value, 'status' => ReviewPublicationResolutionStepStatus::Actionable->value, ]); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get(GovernanceInbox::getUrl(panel: 'admin', parameters: [ 'family' => 'review_publication_resolution', ])) ->assertOk() ->assertSee('Spec389 Workspace Visible Tenant') ->assertDontSee('Spec389 Foreign Workspace Tenant'); }); it('Spec389 does not surface resolution intake work on the customer review workspace', function (): void { $tenant = ManagedEnvironment::factory()->active()->create([ 'name' => 'Spec389 Customer Surface Tenant', ]); [$owner, $tenant] = createUserWithTenant($tenant, role: 'owner', workspaceRole: 'owner'); [$customer] = createUserWithTenant($tenant, User::factory()->create(), role: 'readonly', workspaceRole: 'readonly'); spec389CreateResolutionCase($tenant, $owner, [ 'status' => ReviewPublicationResolutionCaseStatus::InProgress->value, 'current_step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value, ], [ 'step_key' => ReviewPublicationResolutionStepKey::CompleteRequiredReports->value, 'status' => ReviewPublicationResolutionStepStatus::Actionable->value, ]); $this->actingAs($customer) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get(CustomerReviewWorkspace::environmentFilterUrl($tenant)) ->assertOk() ->assertDontSee('Review publication work') ->assertDontSee('Review cannot be published yet') ->assertDontSee('Continue preparation') ->assertDontSee('Open operation'); }); it('Spec389 renders governance inbox publication work without creating audit events', function (): void { $tenant = ManagedEnvironment::factory()->active()->create([ 'name' => 'Spec389 Audit Neutral Tenant', ]); [$user, $tenant] = createUserWithTenant($tenant, role: 'owner', workspaceRole: 'owner'); spec389CreateResolutionCase($tenant, $user, [ 'status' => ReviewPublicationResolutionCaseStatus::InProgress->value, 'current_step_key' => ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot->value, ], [ 'step_key' => ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot->value, 'status' => ReviewPublicationResolutionStepStatus::Actionable->value, ]); $auditCount = AuditLog::query()->count(); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get(GovernanceInbox::getUrl(panel: 'admin', parameters: [ 'family' => 'review_publication_resolution', ])) ->assertOk() ->assertSee('Continue preparation') ->assertDontSee('Publish review') ->assertDontSee('Cancel resolution') ->assertDontSee('Prepare export'); expect(AuditLog::query()->count())->toBe($auditCount); }); /** * @return array{0: ReviewPublicationResolutionCase, 1: ReviewPublicationResolutionStep, 2: EnvironmentReview} */ function spec389CreateResolutionCase( ManagedEnvironment $tenant, User $actor, array $caseOverrides = [], array $stepOverrides = [], ): array { $now = now(); $snapshot = EvidenceSnapshot::query() ->where('workspace_id', (int) $tenant->workspace_id) ->where('managed_environment_id', (int) $tenant->getKey()) ->latest('id') ->first(); if (! $snapshot instanceof EvidenceSnapshot) { $snapshot = seedPartialEnvironmentReviewEvidence( tenant: $tenant, findingCount: 0, driftCount: 0, operationRunCount: 0, ); } $review = EnvironmentReview::factory()->create([ 'workspace_id' => (int) $tenant->workspace_id, 'managed_environment_id' => (int) $tenant->getKey(), 'evidence_snapshot_id' => (int) $snapshot->getKey(), 'initiated_by_user_id' => (int) $actor->getKey(), 'generated_at' => $caseOverrides['review_generated_at'] ?? $now, ]); $stepKey = (string) ($stepOverrides['step_key'] ?? ReviewPublicationResolutionStepKey::CompleteRequiredReports->value); $caseUpdatedAt = $caseOverrides['updated_at'] ?? $now; $stepUpdatedAt = $stepOverrides['updated_at'] ?? $caseUpdatedAt; $defaultStepSummary = $stepKey === ReviewPublicationResolutionStepKey::CompleteRequiredReports->value ? ['missing_report_dimensions' => ['permission_posture']] : []; unset($caseOverrides['review_generated_at']); $case = ReviewPublicationResolutionCase::query()->create(array_replace([ 'workspace_id' => (int) $tenant->workspace_id, 'managed_environment_id' => (int) $tenant->getKey(), 'environment_review_id' => (int) $review->getKey(), 'action_key' => ReviewPublicationResolutionCase::ACTION_KEY, 'status' => ReviewPublicationResolutionCaseStatus::InProgress->value, 'current_step_key' => $stepKey, 'readiness_fingerprint' => hash('sha256', 'spec389-'.$tenant->getKey().'-'.$review->getKey().'-'.str()->uuid()), 'created_by_user_id' => (int) $actor->getKey(), 'assigned_to_user_id' => (int) $actor->getKey(), 'started_at' => $now, 'last_evaluated_at' => $now, 'summary' => $defaultStepSummary, 'metadata' => [], 'created_at' => $caseUpdatedAt, 'updated_at' => $caseUpdatedAt, ], $caseOverrides)); $step = ReviewPublicationResolutionStep::query()->create(array_replace([ 'case_id' => (int) $case->getKey(), 'position' => 1, 'step_key' => $stepKey, 'status' => ReviewPublicationResolutionStepStatus::Actionable->value, 'primary_action_key' => ReviewPublicationResolutionStepKey::tryFrom($stepKey)?->primaryActionKey(), 'summary' => [], 'metadata' => [], 'created_at' => $stepUpdatedAt, 'updated_at' => $stepUpdatedAt, ], $stepOverrides)); return [$case->fresh(['tenant', 'environmentReview', 'steps.operationRun']), $step->fresh('operationRun'), $review->fresh()]; } function spec389ResolutionOperationRun( ManagedEnvironment $tenant, ReviewPublicationResolutionCase $case, EnvironmentReview $review, array $overrides = [], ): OperationRun { return OperationRun::factory()->forTenant($tenant)->create(array_replace([ 'type' => OperationRunType::EntraAdminRolesScan->value, 'status' => OperationRunStatus::Running->value, 'outcome' => OperationRunOutcome::Pending->value, 'context' => [ 'workspace_id' => (int) $tenant->workspace_id, 'managed_environment_id' => (int) $tenant->getKey(), 'environment_review_id' => (int) $review->getKey(), 'review_publication_resolution_case_id' => (int) $case->getKey(), 'trigger' => 'review_publication_resolution', ], ], $overrides)); } function spec389AttachResolutionOperationProof( ReviewPublicationResolutionStep $step, OperationRun $run, array $metadata = [], ?int $proofId = null, ?string $proofStatus = null, ): ReviewPublicationResolutionStep { $step->forceFill([ 'operation_run_id' => (int) $run->getKey(), 'proof_type' => 'operation_run', 'proof_id' => $proofId ?? (int) $run->getKey(), 'proof_status' => $proofStatus ?? (string) $run->outcome, 'metadata' => array_replace(spec389SafeOperationProofMetadata(), $metadata), ])->save(); return $step->fresh('operationRun'); } function spec389AttachReadyToContinueProof( ReviewPublicationResolutionStep $step, EnvironmentReview $review, array $metadata = [], ): ReviewPublicationResolutionStep { $step->forceFill([ 'proof_type' => 'environment_review', 'proof_id' => (int) $review->getKey(), 'proof_status' => 'ready', 'metadata' => array_replace(spec389SafeReadyToContinueProofMetadata(), $metadata), ])->save(); return $step->fresh(); } /** * @param array $overrides * @return array */ function spec389SafeOperationProofMetadata(array $overrides = []): array { return array_replace([ 'proof_currentness' => ResolutionProofCurrentness::Current->value, 'proof_usability' => ResolutionProofUsability::InspectionOnly->value, 'proof_visibility' => ResolutionProofVisibility::OperatorVisible->value, 'proof_summary' => [ 'message' => 'Safe current operation proof is available.', ], ], $overrides); } /** * @param array $overrides * @return array */ function spec389SafeReadyToContinueProofMetadata(array $overrides = []): array { return array_replace([ 'proof_currentness' => ResolutionProofCurrentness::Current->value, 'proof_usability' => ResolutionProofUsability::Usable->value, 'proof_visibility' => ResolutionProofVisibility::OperatorVisible->value, 'proof_summary' => [ 'message' => 'Current review proof is available.', ], ], $overrides); } function spec389ReviewPublicationProviderSection( User $user, ManagedEnvironment $tenant, ?string $selectedStatus = null, ?string $selectedUpdated = null, int $previewLimit = 10, ): array { $workspace = Workspace::query()->findOrFail((int) $tenant->workspace_id); return app(ReviewPublicationResolutionInboxProvider::class)->section( user: $user, workspace: $workspace, reviewTenants: [(int) $tenant->getKey() => $tenant->fresh()], selectedTenant: null, selectedStatus: $selectedStatus, selectedUpdated: $selectedUpdated, navigationContext: null, previewLimit: $previewLimit, ); } /** * @param list|null> $entries * @return list */ function spec389EntryActionLabels(array $entries): array { return collect($entries) ->filter(fn (mixed $entry): bool => is_array($entry)) ->flatMap(function (array $entry): array { return array_merge( collect($entry['secondary_actions'] ?? [])->pluck('label')->all(), collect($entry['linked_records'] ?? [])->pluck('label')->all(), ); }) ->filter(fn (mixed $label): bool => is_string($label)) ->values() ->all(); }