create(['name' => 'Spec386 Case']); [$owner, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner'); $snapshot = seedPartialEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0); $review = composeEnvironmentReviewForTest($tenant, $owner, $snapshot); $case = app(ReviewPublicationResolutionService::class)->openOrResume($review, $owner); expect($case)->toBeInstanceOf(ReviewPublicationResolutionCase::class) ->and($case->status)->toBe(ReviewPublicationResolutionCaseStatus::InProgress->value) ->and($case->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::ValidateReviewReadiness->value)?->status) ->toBe(ReviewPublicationResolutionStepStatus::Completed->value) ->and($case->current_step_key)->not->toBeNull(); expect(AuditLog::query() ->where('action', AuditActionId::ReviewPublicationResolutionCreated->value) ->where('resource_id', (string) $case->getKey()) ->exists())->toBeTrue(); }); it('does not create a resolution case when a mutable review is already publishable', function (): void { [$owner, $tenant] = createUserWithTenant(role: 'owner'); $review = composeEnvironmentReviewForTest($tenant, $owner); $case = app(ReviewPublicationResolutionService::class)->openOrResume($review, $owner); expect($case)->toBeNull() ->and(ReviewPublicationResolutionCase::query()->count())->toBe(0); }); it('does not let readonly actors create resolution cases', function (): void { $tenant = ManagedEnvironment::factory()->create(['name' => 'Spec386 Readonly No Create']); [$owner, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner'); [$readonly] = createUserWithTenant(tenant: $tenant, user: User::factory()->create(), role: 'readonly', workspaceRole: 'readonly'); $snapshot = seedPartialEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0); $review = composeEnvironmentReviewForTest($tenant, $owner, $snapshot); expect(fn () => app(ReviewPublicationResolutionService::class)->openOrResume($review, $readonly)) ->toThrow(AuthorizationException::class) ->and(ReviewPublicationResolutionCase::query()->forReview($review)->count())->toBe(0); setAdminEnvironmentContext($tenant); Livewire::actingAs($readonly) ->test(ResolveReviewPublication::class, ['record' => $review->getKey()]) ->assertForbidden(); expect(ReviewPublicationResolutionCase::query()->forReview($review)->count())->toBe(0); }); it('supersedes an active resolution case when the readiness fingerprint changes', function (): void { $tenant = ManagedEnvironment::factory()->create(['name' => 'Spec386 Supersede']); [$owner, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner'); $snapshot = seedPartialEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0); $review = composeEnvironmentReviewForTest($tenant, $owner, $snapshot); $firstCase = app(ReviewPublicationResolutionService::class)->openOrResume($review, $owner); $section = $review->sections()->where('required', true)->firstOrFail(); $section->forceFill([ 'summary_payload' => array_replace_recursive(is_array($section->summary_payload) ? $section->summary_payload : [], [ 'publication_blockers' => ['Spec386 changed blocker.'], ]), ])->save(); $secondCase = app(ReviewPublicationResolutionService::class)->openOrResume($review->fresh(['sections', 'evidenceSnapshot.items']), $owner); expect($secondCase)->not->toBeNull() ->and($secondCase->getKey())->not->toBe($firstCase?->getKey()) ->and($firstCase?->fresh()->status)->toBe(ReviewPublicationResolutionCaseStatus::Superseded->value) ->and(AuditLog::query() ->where('action', AuditActionId::ReviewPublicationResolutionSuperseded->value) ->where('resource_id', (string) $firstCase?->getKey()) ->exists())->toBeTrue(); }); it('keeps readonly actors able to inspect but unable to execute resolution steps', function (): void { $tenant = ManagedEnvironment::factory()->create(['name' => 'Spec386 Readonly']); [$owner, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner'); [$readonly] = createUserWithTenant(tenant: $tenant, user: User::factory()->create(), role: 'readonly', workspaceRole: 'readonly'); $snapshot = seedPartialEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0); $review = composeEnvironmentReviewForTest($tenant, $owner, $snapshot); $case = app(ReviewPublicationResolutionService::class)->openOrResume($review, $owner); expect($readonly->can('view', $case))->toBeTrue() ->and($readonly->can('executeStep', $case))->toBeFalse(); setAdminEnvironmentContext($tenant); Livewire::actingAs($readonly) ->test(ResolveReviewPublication::class, ['record' => $review->getKey()]) ->assertSee('Review can\'t be published yet') ->assertSee('Publication preparation') ->assertDontSee('Report-backed evidence') ->assertActionVisible('execute_current_step') ->assertActionDisabled('execute_current_step'); }); it('requires confirmation before executing the current resolution step', function (): void { $tenant = ManagedEnvironment::factory()->create(['name' => 'Spec386 Confirm Execute']); [$owner, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner'); $snapshot = seedPartialEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0); $review = composeEnvironmentReviewForTest($tenant, $owner, $snapshot); app(ReviewPublicationResolutionService::class)->openOrResume($review, $owner); setAdminEnvironmentContext($tenant); Livewire::actingAs($owner) ->test(ResolveReviewPublication::class, ['record' => $review->getKey()]) ->assertActionExists('execute_current_step', fn (Action $action): bool => $action->isConfirmationRequired()); }); it('authorizes provider report resolution from the provider capability instead of review manage', function (): void { $tenant = ManagedEnvironment::factory()->create(['name' => 'Spec386 Provider Capability']); [$owner, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner'); [$operator] = createUserWithTenant(tenant: $tenant, user: User::factory()->create(), role: 'operator', workspaceRole: 'operator'); $snapshot = seedPartialEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0); spec386DeleteStoredReport($tenant, StoredReport::REPORT_TYPE_PERMISSION_POSTURE); $review = composeEnvironmentReviewForTest($tenant, $owner, $snapshot); $case = app(ReviewPublicationResolutionService::class)->openOrResume($review, $owner); expect($case?->current_step_key)->toBe(ReviewPublicationResolutionStepKey::CompleteRequiredReports->value) ->and($operator->can(Capabilities::PROVIDER_RUN, $tenant))->toBeTrue() ->and($operator->can(Capabilities::ENVIRONMENT_REVIEW_MANAGE, $tenant))->toBeFalse() ->and($operator->can('executeStep', $case))->toBeTrue(); setAdminEnvironmentContext($tenant); Livewire::actingAs($operator) ->test(ResolveReviewPublication::class, ['record' => $review->getKey()]) ->assertActionVisible('execute_current_step') ->assertActionEnabled('execute_current_step'); }); it('plans only relevant required steps for the current blocker set', function (): void { $tenant = ManagedEnvironment::factory()->create(['name' => 'Spec386 Relevant Steps']); [$owner, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner'); $snapshot = seedPartialEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0); $snapshot = restateEnvironmentReviewEvidenceSnapshot($snapshot, EvidenceCompletenessState::Complete); $snapshot->items()->whereIn('dimension_key', ['permission_posture', 'entra_admin_roles'])->update([ 'state' => EvidenceCompletenessState::Complete->value, ]); $snapshot->items()->where('dimension_key', 'permission_posture')->update([ 'state' => EvidenceCompletenessState::Missing->value, 'source_record_id' => null, 'source_fingerprint' => null, ]); spec386DeleteStoredReport($tenant, StoredReport::REPORT_TYPE_PERMISSION_POSTURE); $review = composeEnvironmentReviewForTest($tenant, $owner, $snapshot->fresh('items')); $case = app(ReviewPublicationResolutionService::class)->openOrResume($review, $owner); $stepKeys = $case?->steps->pluck('step_key')->all(); expect($stepKeys)->toContain(ReviewPublicationResolutionStepKey::CompleteRequiredReports->value) ->and($stepKeys)->not->toContain(ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot->value); }); it('lets current readiness truth supersede stale failed operation proof on an existing step', function (): void { $tenant = ManagedEnvironment::factory()->create(['name' => 'Spec386 Stale Proof']); [$owner, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner'); $snapshot = seedPartialEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0); spec386DeleteStoredReport($tenant, StoredReport::REPORT_TYPE_PERMISSION_POSTURE); $review = composeEnvironmentReviewForTest($tenant, $owner, $snapshot); $case = app(ReviewPublicationResolutionService::class)->openOrResume($review, $owner); $step = $case?->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::CompleteRequiredReports->value); $failedRun = OperationRun::factory()->forTenant($tenant)->create([ 'type' => 'provider.connection.check', 'status' => OperationRunStatus::Completed->value, 'outcome' => OperationRunOutcome::Failed->value, 'completed_at' => now()->subMinute(), 'context' => [ 'environment_review_id' => (int) $review->getKey(), 'review_publication_resolution_case_id' => (int) $case?->getKey(), ], ]); $step?->forceFill([ 'status' => ReviewPublicationResolutionStepStatus::Failed->value, 'operation_run_id' => (int) $failedRun->getKey(), 'proof_type' => 'operation_run', 'proof_id' => (int) $failedRun->getKey(), 'proof_status' => OperationRunOutcome::Failed->value, ])->save(); $snapshot = restateEnvironmentReviewEvidenceSnapshot($snapshot, EvidenceCompletenessState::Complete); $permissionReport = spec386CreateReadyStoredReport($tenant, StoredReport::REPORT_TYPE_PERMISSION_POSTURE); $adminRolesReport = StoredReport::query() ->where('managed_environment_id', (int) $tenant->getKey()) ->where('report_type', StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES) ->latest('id') ->firstOrFail(); $snapshot->items()->where('dimension_key', 'permission_posture')->update([ 'state' => EvidenceCompletenessState::Complete->value, 'source_record_id' => (int) $permissionReport->getKey(), 'source_fingerprint' => (string) $permissionReport->fingerprint, ]); $snapshot->items()->where('dimension_key', 'entra_admin_roles')->update([ 'state' => EvidenceCompletenessState::Complete->value, 'source_record_id' => (int) $adminRolesReport->getKey(), 'source_fingerprint' => (string) $adminRolesReport->fingerprint, ]); app(ReviewPublicationResolutionService::class)->refreshCase($case->fresh(['steps.operationRun', 'environmentReview.evidenceSnapshot.items']), $owner); expect($step?->fresh()->status)->toBe(ReviewPublicationResolutionStepStatus::Completed->value); }); it('queues the existing report operation when executing the required reports step', function (): void { Queue::fake(); $tenant = ManagedEnvironment::factory()->create(['name' => 'Spec386 Report Step']); [$owner, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner'); $snapshot = seedPartialEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0); $snapshot->items()->whereIn('dimension_key', ['permission_posture', 'entra_admin_roles'])->update([ 'state' => EvidenceCompletenessState::Complete->value, ]); $snapshot->items()->where('dimension_key', 'entra_admin_roles')->update([ 'state' => EvidenceCompletenessState::Missing->value, 'source_record_id' => null, 'source_fingerprint' => null, ]); spec386DeleteStoredReport($tenant, StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES); $review = composeEnvironmentReviewForTest($tenant, $owner, $snapshot); $case = app(ReviewPublicationResolutionService::class)->openOrResume($review, $owner); $result = app(ReviewPublicationResolutionActionService::class)->executeCurrentStep($case, $owner); $updatedCase = $result['case']->fresh('steps.operationRun'); $runningStep = $updatedCase->steps->firstWhere('status', ReviewPublicationResolutionStepStatus::Running->value); expect($result['operation_type'])->toBe(OperationRunType::EntraAdminRolesScan->value) ->and($updatedCase->status)->toBe(ReviewPublicationResolutionCaseStatus::WaitingForRun->value) ->and($runningStep)->not->toBeNull() ->and($runningStep?->step_key)->toBe(ReviewPublicationResolutionStepKey::CompleteRequiredReports->value) ->and($runningStep?->operationRun?->type)->toBe(OperationRunType::EntraAdminRolesScan->value) ->and(AuditLog::query() ->where('action', AuditActionId::ReviewPublicationResolutionOperationLinked->value) ->where('resource_id', (string) $updatedCase->getKey()) ->exists())->toBeTrue(); Queue::assertPushed(ScanEntraAdminRolesJob::class); }); it('does not persist raw source-service exception text in failed step audit payloads', function (): void { $tenant = ManagedEnvironment::factory()->create(['name' => 'Spec386 Redacted Failure']); [$owner, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner'); $snapshot = seedPartialEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0); $review = composeEnvironmentReviewForTest($tenant, $owner, $snapshot); $case = app(ReviewPublicationResolutionService::class)->openOrResume($review, $owner); $review = markEnvironmentReviewCustomerSafeReady($review); $review->forceFill([ 'status' => EnvironmentReviewStatus::Ready->value, 'current_export_review_pack_id' => null, ])->save(); $review->sections()->get()->each(function ($section): void { $summary = is_array($section->summary_payload) ? $section->summary_payload : []; $baselineReadiness = is_array($summary['baseline_readiness'] ?? null) ? $summary['baseline_readiness'] : []; $baselineReadiness['publication_blockers'] = []; $section->forceFill([ 'completeness_state' => \App\Support\EnvironmentReviewCompletenessState::Complete->value, 'summary_payload' => array_replace($summary, [ 'publication_blockers' => [], 'baseline_readiness' => $baselineReadiness, ]), ])->save(); }); $case = app(ReviewPublicationResolutionService::class)->refreshCase($case->fresh(['steps.operationRun', 'environmentReview']), $owner); expect($case->current_step_key)->toBe(ReviewPublicationResolutionStepKey::GenerateReviewPack->value); app()->bind(ReviewPackService::class, fn (): ReviewPackService => new class extends ReviewPackService { public function __construct() {} public function generateFromReview(\App\Models\EnvironmentReview $review, User $user, array $options = []): \App\Models\ReviewPack { throw new RuntimeException('secret-token=abc123 rawGraphPayload {"access_token":"xyz"}'); } }); expect(fn () => app(ReviewPublicationResolutionActionService::class)->executeCurrentStep($case, $owner)) ->toThrow(RuntimeException::class); $failedStep = $case?->fresh('steps')->steps->firstWhere('step_key', ReviewPublicationResolutionStepKey::GenerateReviewPack->value); $failureAudit = AuditLog::query() ->where('action', AuditActionId::ReviewPublicationResolutionStepFailed->value) ->where('resource_id', (string) $case?->getKey()) ->latest('id') ->firstOrFail(); $auditPayload = json_encode($failureAudit->metadata, JSON_THROW_ON_ERROR); $stepPayload = json_encode($failedStep?->summary ?? [], JSON_THROW_ON_ERROR); expect($failedStep?->summary)->toHaveKey('failure_code') ->and($failedStep?->summary)->not->toHaveKey('failure') ->and($auditPayload)->toContain('review_publication_resolution.step_failed_before_queue') ->and($auditPayload)->not->toContain('secret-token', 'rawGraphPayload', 'access_token') ->and($stepPayload)->not->toContain('secret-token', 'rawGraphPayload', 'access_token'); }); it('queues the existing evidence operation when executing the evidence collection step', function (): void { $tenant = ManagedEnvironment::factory()->create(['name' => 'Spec386 Evidence Step']); [$owner, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner'); $snapshot = seedPartialEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0); $snapshot->items()->whereIn('dimension_key', ['permission_posture', 'entra_admin_roles'])->update([ 'state' => EvidenceCompletenessState::Complete->value, ]); $review = composeEnvironmentReviewForTest($tenant, $owner, $snapshot->fresh('items')); $case = app(ReviewPublicationResolutionService::class)->openOrResume($review, $owner); $result = app(ReviewPublicationResolutionActionService::class)->executeCurrentStep($case, $owner); $updatedCase = $result['case']->fresh('steps.operationRun'); $runningStep = $updatedCase->steps->firstWhere('status', ReviewPublicationResolutionStepStatus::Running->value); expect($result['operation_type'])->toBe(OperationRunType::EvidenceSnapshotGenerate->value) ->and($updatedCase->status)->toBe(ReviewPublicationResolutionCaseStatus::WaitingForRun->value) ->and($runningStep)->not->toBeNull() ->and($runningStep?->step_key)->toBe(ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot->value) ->and($runningStep?->operationRun?->type)->toBe(OperationRunType::EvidenceSnapshotGenerate->value) ->and(AuditLog::query() ->where('action', AuditActionId::ReviewPublicationResolutionOperationLinked->value) ->where('resource_id', (string) $updatedCase->getKey()) ->exists())->toBeTrue(); }); it('promotes blocked mutable reviews to the publication resolution page action', function (): void { $tenant = ManagedEnvironment::factory()->create(['name' => 'Spec386 Header']); [$owner, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner'); $snapshot = seedPartialEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0); $review = composeEnvironmentReviewForTest($tenant, $owner, $snapshot); setAdminEnvironmentContext($tenant); $expectedUrl = EnvironmentReviewResource::environmentScopedUrl('resolve-publication', ['record' => $review], $tenant); Livewire::actingAs($owner) ->test(ViewEnvironmentReview::class, ['record' => $review->getKey()]) ->assertActionExists('resolve_publication_blockers', fn (Action $action): bool => ! $action->isConfirmationRequired()) ->assertActionVisible('resolve_publication_blockers') ->callAction('resolve_publication_blockers') ->assertRedirect($expectedUrl); expect(ReviewPublicationResolutionCase::query()->forReview($review)->active()->count())->toBe(1); }); function spec386DeleteStoredReport(ManagedEnvironment $tenant, string $reportType): void { StoredReport::query() ->where('workspace_id', (int) $tenant->workspace_id) ->where('managed_environment_id', (int) $tenant->getKey()) ->where('report_type', $reportType) ->delete(); } function spec386CreateReadyStoredReport(ManagedEnvironment $tenant, string $reportType): StoredReport { $factory = $reportType === StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES ? StoredReport::factory()->entraAdminRoles(['roles' => []]) : StoredReport::factory()->permissionPosture([ 'required_count' => 0, 'granted_count' => 0, 'permissions' => [], ]); return $factory->create([ 'workspace_id' => (int) $tenant->workspace_id, 'managed_environment_id' => (int) $tenant->getKey(), 'report_type' => $reportType, 'status' => StoredReport::STATUS_READY, 'generated_at' => now()->addMinute(), 'created_at' => now()->addMinute(), 'updated_at' => now()->addMinute(), ]); }