TenantAtlas/apps/platform/tests/Feature/Governance/Spec389GovernanceInboxResolutionIntakeTest.php
ahmido 9912d94563 feat: add governance inbox resolution intake (#460)
Automated PR created by Codex via Gitea API.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #460
2026-06-20 07:46:12 +00:00

847 lines
38 KiB
PHP

<?php
declare(strict_types=1);
use App\Filament\Pages\Governance\GovernanceInbox;
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
use App\Models\AuditLog;
use App\Models\EnvironmentReview;
use App\Models\EvidenceSnapshot;
use App\Models\ManagedEnvironment;
use App\Models\OperationRun;
use App\Models\ReviewPublicationResolutionCase;
use App\Models\ReviewPublicationResolutionStep;
use App\Models\User;
use App\Models\Workspace;
use App\Services\Auth\CapabilityResolver;
use App\Support\Auth\Capabilities;
use App\Support\GovernanceInbox\ReviewPublicationResolutionInboxProvider;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\OperationRunType;
use App\Support\ReviewPublicationResolution\ResolutionProofCurrentness;
use App\Support\ReviewPublicationResolution\ResolutionProofUsability;
use App\Support\ReviewPublicationResolution\ResolutionProofVisibility;
use App\Support\ReviewPublicationResolution\ReviewPublicationResolutionCaseStatus;
use App\Support\ReviewPublicationResolution\ReviewPublicationResolutionStepKey;
use App\Support\ReviewPublicationResolution\ReviewPublicationResolutionStepStatus;
use App\Support\Workspaces\WorkspaceContext;
it('Spec389 renders active review publication resolution cases in the governance inbox', function (): void {
$tenant = ManagedEnvironment::factory()->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<string, mixed> $overrides
* @return array<string, mixed>
*/
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<string, mixed> $overrides
* @return array<string, mixed>
*/
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<array<string, mixed>|null> $entries
* @return list<string>
*/
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();
}