forTenant($tenant)->create([ 'user_id' => (int) $user->getKey(), 'type' => 'environment.review.compose', 'status' => OperationRunStatus::Queued->value, 'outcome' => OperationRunOutcome::Pending->value, 'created_at' => now()->subMinute(), ]); $decision = spec365OperationRunDecision($run, $user); expect(data_get($decision, 'primary_action.key'))->toBe('view_details') ->and($decision['freshness_state'])->toBe('fresh_active') ->and($decision['disabled_reasons']['reconcile'])->toBe(__('localization.operations.actions.disabled.lifecycle_fresh')) ->and($decision['disabled_reasons']['retry'])->toBe(__('localization.operations.actions.disabled.retry_deferred')); }); it('fails closed for stale supported review-compose runs without canonical proof in Spec365', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $snapshot = seedEnvironmentReviewEvidence($tenant, operationRunCount: 0); $fingerprint = app(EnvironmentReviewFingerprint::class)->forSnapshot($tenant, $snapshot); $run = OperationRun::factory()->forTenant($tenant)->create([ 'user_id' => (int) $user->getKey(), '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' => (int) $snapshot->getKey(), 'review_fingerprint' => $fingerprint, ], ]); $decision = spec365OperationRunDecision($run, $user); expect(data_get($decision, 'primary_action.key'))->toBe('view_details') ->and(spec365OperationRunActionKeys($decision['secondary_actions']))->not->toContain('reconcile') ->and($decision['disabled_reasons']['reconcile'])->toBe(__('localization.operations.actions.disabled.insufficient_proof')) ->and($decision['disabled_reasons']['retry'])->toBe(__('localization.operations.actions.disabled.retry_deferred')); }); it('offers a confirmed reconcile action for stale supported review-compose runs when canonical proof exists in Spec365', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $snapshot = seedEnvironmentReviewEvidence($tenant, operationRunCount: 0); $fingerprint = app(EnvironmentReviewFingerprint::class)->forSnapshot($tenant, $snapshot); $run = OperationRun::factory()->forTenant($tenant)->create([ 'user_id' => (int) $user->getKey(), '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' => (int) $snapshot->getKey(), 'review_fingerprint' => $fingerprint, ], ]); spec365UnitReadyReviewTruth($tenant, $user, $fingerprint, (int) $snapshot->getKey()); $decision = spec365OperationRunDecision($run, $user); expect(data_get($decision, 'primary_action.key'))->toBe('reconcile') ->and(data_get($decision, 'primary_action.requires_confirmation'))->toBeTrue() ->and($decision['mutation_scope'])->toBe(__('localization.operations.actions.mutation_scope_reconcile')) ->and($decision['attention_reason'])->toBe(__('localization.operations.actions.attention.reconcile_available')) ->and($decision['disabled_reasons']['retry'])->toBe(__('localization.operations.actions.disabled.retry_deferred')); }); it('offers reconcile for stale running supported review-compose runs in Spec365', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $snapshot = seedEnvironmentReviewEvidence($tenant, operationRunCount: 0); $fingerprint = app(EnvironmentReviewFingerprint::class)->forSnapshot($tenant, $snapshot); $run = OperationRun::factory()->forTenant($tenant)->create([ 'user_id' => (int) $user->getKey(), 'type' => 'environment.review.compose', 'status' => OperationRunStatus::Running->value, 'outcome' => OperationRunOutcome::Pending->value, 'started_at' => now()->subMinutes(20), 'created_at' => now()->subMinutes(20), 'context' => [ 'workspace_id' => (int) $tenant->workspace_id, 'managed_environment_id' => (int) $tenant->getKey(), 'evidence_snapshot_id' => (int) $snapshot->getKey(), 'review_fingerprint' => $fingerprint, ], ]); spec365UnitReadyReviewTruth($tenant, $user, $fingerprint, (int) $snapshot->getKey()); $decision = spec365OperationRunDecision($run, $user); expect(data_get($decision, 'primary_action.key'))->toBe('reconcile') ->and($decision['freshness_state'])->toBe('likely_stale'); }); it('fails closed for unsupported operation types in Spec365', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $run = OperationRun::factory()->forTenant($tenant)->create([ 'user_id' => (int) $user->getKey(), 'type' => 'unknown.operation', 'status' => OperationRunStatus::Queued->value, 'outcome' => OperationRunOutcome::Pending->value, 'created_at' => now()->subMinutes(10), ]); $decision = spec365OperationRunDecision($run, $user); expect($decision['high_risk'])->toBeTrue() ->and(data_get($decision, 'primary_action.key'))->toBe('view_details') ->and($decision['disabled_reasons']['reconcile'])->toBe(__('localization.operations.actions.disabled.unsupported_reconcile')) ->and($decision['disabled_reasons']['retry'])->toBe(__('localization.operations.actions.disabled.high_risk_retry')); }); it('fails closed for review viewers who lack the execution capability in Spec365', function (): void { [$user, $tenant] = createUserWithTenant(role: 'readonly', workspaceRole: 'readonly'); $run = OperationRun::factory()->forTenant($tenant)->create([ 'user_id' => (int) $user->getKey(), 'type' => 'environment.review.compose', 'status' => OperationRunStatus::Queued->value, 'outcome' => OperationRunOutcome::Pending->value, 'created_at' => now()->subMinutes(10), ]); $decision = spec365OperationRunDecision($run, $user); expect(data_get($decision, 'primary_action.key'))->toBe('view_details') ->and(spec365OperationRunActionKeys($decision['secondary_actions']))->not->toContain('reconcile') ->and($decision['disabled_reasons']['reconcile'])->toBe(__('localization.operations.actions.disabled.missing_capability')); }); it('returns no enabled actions outside the actor workspace scope in Spec365', function (): void { [$owner, $tenant] = createUserWithTenant(role: 'owner'); $outsider = User::factory()->create(); $run = OperationRun::factory()->forTenant($tenant)->create([ 'user_id' => (int) $owner->getKey(), 'type' => 'environment.review.compose', 'status' => OperationRunStatus::Queued->value, 'outcome' => OperationRunOutcome::Pending->value, 'created_at' => now()->subMinutes(10), ]); $decision = spec365OperationRunDecision($run, $outsider); expect($decision['primary_action'])->toBeNull() ->and($decision['disabled_reasons']['view_details'])->toBe(__('localization.operations.actions.disabled.scope_unavailable')) ->and($decision['disabled_reasons']['reconcile'])->toBe(__('localization.operations.actions.disabled.scope_unavailable')) ->and($decision['attention_reason'])->toBe(__('localization.operations.actions.attention.scope_unavailable')); }); it('keeps high-risk restore runs off primary mutation actions and forbids success-forcing controls in Spec365', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $run = OperationRun::factory()->forTenant($tenant)->create([ 'user_id' => (int) $user->getKey(), 'type' => 'restore.execute', 'status' => OperationRunStatus::Queued->value, 'outcome' => OperationRunOutcome::Pending->value, 'created_at' => now()->subMinutes(10), ]); $decision = spec365OperationRunDecision($run, $user); $enabledKeys = array_merge( [data_get($decision, 'primary_action.key')], spec365OperationRunActionKeys($decision['secondary_actions']), ); expect($decision['high_risk'])->toBeTrue() ->and(data_get($decision, 'primary_action.key'))->toBe('view_restore_details') ->and($decision['disabled_reasons']['retry'])->toBe(__('localization.operations.actions.disabled.high_risk_retry')) ->and($enabledKeys)->not->toContain( 'retry_restore', 'restore_reexecute', 'force_complete', 'mark_succeeded', 'delete_run', 'purge_run', ); foreach ([ 'retry_restore', 'restore_reexecute', 'force_complete', 'mark_succeeded', 'delete_run', 'purge_run', ] as $action) { expect($decision['disabled_reasons'])->not->toHaveKey($action) ->and(spec365OperationRunActionKeys($decision['disabled_actions']))->not->toContain($action); } }); function spec365UnitReadyReviewTruth( ManagedEnvironment $tenant, User $user, string $fingerprint, int $snapshotId, ): 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' => $snapshotId, 'initiated_by_user_id' => (int) $user->getKey(), 'operation_run_id' => (int) $publishedRun->getKey(), 'fingerprint' => $fingerprint, ]); } /** * @return array */ function spec365OperationRunDecision(OperationRun $run, ?User $user): array { return app(OperationRunActionEligibility::class)->forRun($run, $user); } /** * @param list> $actions * @return list */ function spec365OperationRunActionKeys(array $actions): array { return array_values(array_filter(array_map( static fn (array $action): ?string => is_string($action['key'] ?? null) ? (string) $action['key'] : null, $actions, ))); }