forSnapshot($tenant, $snapshot); $run = spec365StaleReviewComposeRun($tenant, $user, $fingerprint, (int) $snapshot->getKey()); $review = spec365ReadyReviewTruth($tenant, $user, $snapshot, $fingerprint); Filament::setTenant(null, true); session([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]); Livewire::actingAs($user) ->test(TenantlessOperationRunViewer::class, ['run' => $run]) ->assertActionVisible('reconcileOperationRun') ->callAction('reconcileOperationRun') ->assertStatus(200); $run->refresh(); expect($run->status)->toBe(OperationRunStatus::Completed->value) ->and($run->outcome)->toBe(OperationRunOutcome::Succeeded->value) ->and($run->reconciliationAdapter())->toBe('environment_review_compose') ->and($run->reconciledRelatedReviewId())->toBe((int) $review->getKey()); $postDecision = app(OperationRunActionEligibility::class)->forRun($run->fresh(), $user); expect(data_get($postDecision, 'primary_action.key'))->toBe('view_review') ->and($postDecision['disabled_reasons']['reconcile'])->toBe(__('localization.operations.actions.disabled.already_reconciled')); $audit = AuditLog::query() ->where('action', 'operation.reconciled_by_operator') ->where('operation_run_id', (int) $run->getKey()) ->first(); expect($audit)->not->toBeNull(); $metadata = is_array($audit?->metadata) ? $audit->metadata : []; $encodedMetadata = json_encode($metadata, JSON_THROW_ON_ERROR); expect($metadata)->toMatchArray([ 'operator_action' => 'reconcile', 'operation_run_id' => (int) $run->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'managed_environment_id' => (int) $tenant->getKey(), 'actor_user_id' => (int) $user->getKey(), 'operation_type' => 'environment.review.compose', 'previous_status' => OperationRunStatus::Queued->value, 'previous_outcome' => OperationRunOutcome::Pending->value, 'resulting_status' => OperationRunStatus::Completed->value, 'resulting_outcome' => OperationRunOutcome::Succeeded->value, 'mutation_scope' => 'tenantpilot_operation_metadata_only', ])->and($encodedMetadata)->not->toContain('access_token', 'client_secret', 'refresh_token'); }); it('denies direct reconcile attempts for review viewers without manage capability and records a denied audit in Spec365', function (): void { [$user, $tenant] = createUserWithTenant(role: 'readonly', workspaceRole: 'readonly'); $snapshot = seedEnvironmentReviewEvidence($tenant, operationRunCount: 0); $fingerprint = app(EnvironmentReviewFingerprint::class)->forSnapshot($tenant, $snapshot); $run = spec365StaleReviewComposeRun($tenant, $user, $fingerprint, (int) $snapshot->getKey()); spec365ReadyReviewTruth($tenant, $user, $snapshot, $fingerprint); Filament::setTenant(null, true); session([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]); Livewire::actingAs($user) ->test(TenantlessOperationRunViewer::class, ['run' => $run]) ->assertDontSee(__('localization.operations.actions.reconcile')); try { app(OperationRunOperatorActionService::class)->reconcile($run, $user); $this->fail('Readonly users should not be able to reconcile review-compose operation runs.'); } catch (HttpException $exception) { expect($exception->getStatusCode())->toBe(403); } $run->refresh(); expect($run->status)->toBe(OperationRunStatus::Queued->value) ->and($run->outcome)->toBe(OperationRunOutcome::Pending->value) ->and($run->isLifecycleReconciled())->toBeFalse() ->and(AuditLog::query() ->where('action', 'operation.reconcile_denied') ->where('operation_run_id', (int) $run->getKey()) ->exists())->toBeTrue(); }); it('denies unsupported reconcile attempts without mutating run state in Spec365', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $run = OperationRun::factory()->forTenant($tenant)->create([ 'user_id' => (int) $user->getKey(), 'initiator_name' => $user->name, 'type' => 'unknown.operation', 'status' => OperationRunStatus::Queued->value, 'outcome' => OperationRunOutcome::Pending->value, 'created_at' => now()->subMinutes(10), ]); $decision = app(OperationRunActionEligibility::class)->forRun($run, $user); expect(data_get($decision, 'primary_action.key'))->toBe('view_details') ->and($decision['disabled_reasons']['reconcile'])->toBe(__('localization.operations.actions.disabled.unsupported_reconcile')); try { app(OperationRunOperatorActionService::class)->reconcile($run, $user); $this->fail('Unsupported operation runs should not reconcile.'); } catch (HttpException $exception) { expect($exception->getStatusCode())->toBe(403); } $run->refresh(); expect($run->status)->toBe(OperationRunStatus::Queued->value) ->and($run->outcome)->toBe(OperationRunOutcome::Pending->value) ->and($run->isLifecycleReconciled())->toBeFalse(); }); it('returns not found for direct reconcile attempts outside workspace scope in Spec365', function (): void { [$owner, $tenant] = createUserWithTenant(role: 'owner'); [$outsider] = createUserWithTenant(role: 'owner'); $snapshot = seedEnvironmentReviewEvidence($tenant, operationRunCount: 0); $fingerprint = app(EnvironmentReviewFingerprint::class)->forSnapshot($tenant, $snapshot); $run = spec365StaleReviewComposeRun($tenant, $owner, $fingerprint, (int) $snapshot->getKey()); try { app(OperationRunOperatorActionService::class)->reconcile($run, $outsider); $this->fail('Cross-workspace users should not be able to reconcile operation runs.'); } catch (HttpException $exception) { expect($exception->getStatusCode())->toBe(404); } $run->refresh(); expect($run->status)->toBe(OperationRunStatus::Queued->value) ->and($run->outcome)->toBe(OperationRunOutcome::Pending->value); }); it('returns not found for direct reconcile attempts outside managed environment scope in Spec365', function (): void { [$user, $allowedTenant] = createUserWithTenant(role: 'owner'); $deniedTenant = ManagedEnvironment::factory()->active()->create([ 'workspace_id' => (int) $allowedTenant->workspace_id, ]); expect(ManagedEnvironmentMembership::query() ->where('user_id', (int) $user->getKey()) ->where('managed_environment_id', (int) $allowedTenant->getKey()) ->exists())->toBeTrue(); $snapshot = seedEnvironmentReviewEvidence($deniedTenant, operationRunCount: 0); $fingerprint = app(EnvironmentReviewFingerprint::class)->forSnapshot($deniedTenant, $snapshot); $run = spec365StaleReviewComposeRun($deniedTenant, $user, $fingerprint, (int) $snapshot->getKey()); spec365ReadyReviewTruth($deniedTenant, $user, $snapshot, $fingerprint); $decision = app(OperationRunActionEligibility::class)->forRun($run, $user); expect($decision['primary_action'])->toBeNull() ->and($decision['disabled_reasons']['reconcile'])->toBe(__('localization.operations.actions.disabled.scope_unavailable')); try { app(OperationRunOperatorActionService::class)->reconcile($run, $user); $this->fail('Cross-environment users should not be able to reconcile operation runs.'); } catch (HttpException $exception) { expect($exception->getStatusCode())->toBe(404); } $run->refresh(); expect($run->status)->toBe(OperationRunStatus::Queued->value) ->and($run->outcome)->toBe(OperationRunOutcome::Pending->value); }); function spec365StaleReviewComposeRun(ManagedEnvironment $tenant, User $user, string $fingerprint, int $snapshotId): OperationRun { return OperationRun::factory()->forTenant($tenant)->create([ 'user_id' => (int) $user->getKey(), 'initiator_name' => $user->name, 'type' => 'environment.review.compose', 'status' => OperationRunStatus::Queued->value, 'outcome' => OperationRunOutcome::Pending->value, 'created_at' => now()->subMinutes(10), 'context' => [ 'workspace_id' => (int) $tenant->workspace_id, 'managed_environment_id' => (int) $tenant->getKey(), 'evidence_snapshot_id' => $snapshotId, 'review_fingerprint' => $fingerprint, ], ]); } function spec365ReadyReviewTruth(ManagedEnvironment $tenant, User $user, EvidenceSnapshot $snapshot, string $fingerprint): EnvironmentReview { $publishedRun = OperationRun::factory()->forTenant($tenant)->create([ 'user_id' => (int) $user->getKey(), 'initiator_name' => $user->name, 'type' => 'environment.review.compose', 'status' => OperationRunStatus::Completed->value, 'outcome' => OperationRunOutcome::Succeeded->value, 'completed_at' => now()->subMinutes(5), 'context' => [ 'workspace_id' => (int) $tenant->workspace_id, 'managed_environment_id' => (int) $tenant->getKey(), 'review_fingerprint' => $fingerprint, ], ]); return EnvironmentReview::factory()->ready()->create([ 'workspace_id' => (int) $tenant->workspace_id, 'managed_environment_id' => (int) $tenant->getKey(), 'evidence_snapshot_id' => (int) $snapshot->getKey(), 'initiated_by_user_id' => (int) $user->getKey(), 'operation_run_id' => (int) $publishedRun->getKey(), 'fingerprint' => $fingerprint, 'summary' => [ 'finding_count' => 4, 'report_count' => 2, 'operation_count' => 1, ], ]); }