create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'user_id' => (int) $user->getKey(), 'type' => 'inventory_sync', 'status' => OperationRunStatus::Queued->value, 'outcome' => OperationRunOutcome::Pending->value, 'context' => [ 'execution_authority_mode' => ExecutionAuthorityMode::ActorBound->value, ], ]); $decision = app(QueuedExecutionLegitimacyGate::class)->evaluate($run); expect($decision->allowed)->toBeTrue() ->and($decision->authorityMode)->toBe(ExecutionAuthorityMode::ActorBound) ->and($decision->reasonCode)->toBeNull() ->and($decision->checks)->toMatchArray([ 'workspace_scope' => 'passed', 'tenant_scope' => 'passed', 'capability' => 'passed', 'tenant_operability' => 'passed', 'execution_prerequisites' => 'not_applicable', ]) ->and($decision->toArray())->toMatchArray([ 'operation_type' => 'inventory_sync', 'authority_mode' => 'actor_bound', 'allowed' => true, 'retryable' => false, 'target_scope' => [ 'workspace_id' => (int) $tenant->workspace_id, 'tenant_id' => (int) $tenant->getKey(), 'provider_connection_id' => null, ], ]); }); it('denies actor-bound execution when the initiator loses capability', function (): void { [$user, $tenant] = createUserWithTenant(role: 'readonly'); $run = OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'user_id' => (int) $user->getKey(), 'type' => 'inventory_sync', 'status' => OperationRunStatus::Queued->value, 'outcome' => OperationRunOutcome::Pending->value, 'context' => [ 'execution_authority_mode' => ExecutionAuthorityMode::ActorBound->value, ], ]); $decision = app(QueuedExecutionLegitimacyGate::class)->evaluate($run); expect($decision->allowed)->toBeFalse() ->and($decision->denialClass)->toBe(ExecutionDenialClass::CapabilityDenied) ->and($decision->reasonCode)->toBe(ExecutionDenialReasonCode::MissingCapability) ->and($decision->retryable)->toBeFalse() ->and($decision->checks)->toMatchArray([ 'workspace_scope' => 'passed', 'tenant_scope' => 'passed', 'capability' => 'failed', ]); }); it('allows system-authority execution only for allowlisted operation types', function (): void { [, $tenant] = createUserWithTenant(role: 'owner'); $run = OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'user_id' => null, 'type' => 'backup_schedule_run', 'status' => OperationRunStatus::Queued->value, 'outcome' => OperationRunOutcome::Pending->value, 'context' => [ 'execution_authority_mode' => ExecutionAuthorityMode::SystemAuthority->value, ], ]); $decision = app(QueuedExecutionLegitimacyGate::class)->evaluate($run); expect($decision->allowed)->toBeTrue() ->and($decision->authorityMode)->toBe(ExecutionAuthorityMode::SystemAuthority) ->and($decision->initiator)->toMatchArray([ 'identity_type' => 'system', 'user_id' => null, ]); }); it('denies non-allowlisted system-authority execution with retryable prerequisite semantics', function (): void { [, $tenant] = createUserWithTenant(role: 'owner'); $run = OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'user_id' => null, 'type' => 'inventory_sync', 'status' => OperationRunStatus::Queued->value, 'outcome' => OperationRunOutcome::Pending->value, 'context' => [ 'execution_authority_mode' => ExecutionAuthorityMode::SystemAuthority->value, ], ]); $decision = app(QueuedExecutionLegitimacyGate::class)->evaluate($run); expect($decision->allowed)->toBeFalse() ->and($decision->denialClass)->toBe(ExecutionDenialClass::PrerequisiteInvalid) ->and($decision->reasonCode)->toBe(ExecutionDenialReasonCode::ExecutionPrerequisiteInvalid) ->and($decision->retryable)->toBeTrue() ->and($decision->checks['execution_prerequisites'])->toBe('failed'); }); it('maps write-gate blocks to retryable prerequisite denials', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $tenant->forceFill([ 'status' => 'archived', 'deleted_at' => now(), 'rbac_status' => 'not_configured', 'rbac_last_checked_at' => null, ])->save(); $run = OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'user_id' => (int) $user->getKey(), 'type' => 'restore.execute', 'status' => OperationRunStatus::Queued->value, 'outcome' => OperationRunOutcome::Pending->value, 'context' => [ 'execution_authority_mode' => ExecutionAuthorityMode::ActorBound->value, ], ]); $decision = app(QueuedExecutionLegitimacyGate::class)->evaluate($run); expect($decision->allowed)->toBeFalse() ->and($decision->reasonCode)->toBe(ExecutionDenialReasonCode::WriteGateBlocked) ->and($decision->denialClass)->toBe(ExecutionDenialClass::PrerequisiteInvalid) ->and($decision->retryable)->toBeTrue() ->and($decision->checks['execution_prerequisites'])->toBe('failed'); }); it('infers tenant sync capability for policy sync runs from the central resolver', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $run = OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'user_id' => (int) $user->getKey(), 'type' => 'policy.sync', 'status' => OperationRunStatus::Queued->value, 'outcome' => OperationRunOutcome::Pending->value, 'context' => [ 'execution_authority_mode' => ExecutionAuthorityMode::ActorBound->value, ], ]); $context = app(QueuedExecutionLegitimacyGate::class)->buildContext($run); expect($context->requiredCapability)->toBe('tenant.sync') ->and($context->authorityMode)->toBe(ExecutionAuthorityMode::ActorBound); }); it('infers system-authority schedule capability for backup schedule runs from the central resolver', function (): void { [, $tenant] = createUserWithTenant(role: 'owner'); $run = OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'user_id' => null, 'type' => 'backup_schedule_run', 'status' => OperationRunStatus::Queued->value, 'outcome' => OperationRunOutcome::Pending->value, 'context' => [], ]); $context = app(QueuedExecutionLegitimacyGate::class)->buildContext($run); expect($context->requiredCapability)->toBe('tenant_backup_schedules.run') ->and($context->authorityMode)->toBe(ExecutionAuthorityMode::SystemAuthority) ->and($context->initiatorSnapshot())->toMatchArray([ 'identity_type' => 'system', 'user_id' => null, ]); });