'', 'workplaceId' => 'legacy-workspace', ]))->toBe('legacy-workspace') ->and(bird_control_plane_activator::canonicalAllowedChannelIds([ 'allowed_channel_ids_json' => '', 'channelId' => 'legacy-channel', ]))->toBe(['legacy-channel']) ->and(bird_control_plane_activator::canonicalAllowedChannelIds([ 'allowed_channel_ids_json' => '["channel-b","channel-a","channel-a"]', 'channelId' => 'legacy-channel', ]))->toBe(['channel-b', 'channel-a']); }); it('fails closed for malformed explicit channel configuration', function (): void { expect(bird_control_plane_activator::canonicalAllowedChannelIds([ 'allowed_channel_ids_json' => '{bad-json', 'channelId' => 'legacy-channel', ]))->toBe([]); }); it('requires an explicit participant when provider observations are ambiguous', function (): void { expect(bird_control_plane_activator::selectParticipantId('', ['participant-1'])) ->toBe('participant-1') ->and(bird_control_plane_activator::selectParticipantId( 'configured-participant', ['participant-1', 'participant-2'] ))->toBe('configured-participant') ->and(fn () => bird_control_plane_activator::selectParticipantId( '', ['participant-1', 'participant-2'] ))->toThrow(RuntimeException::class); }); it('discovers only access key participants from supported conversation shapes', function (): void { expect(bird_control_plane_activator::accessKeyParticipantIds([ 'participants' => [ ['type' => 'contact', 'id' => 'contact-1'], ['type' => 'accessKey', 'id' => 'access-key-1'], ], 'featuredParticipants' => [ ['type' => 'ACCESSKEY', 'id' => 'access-key-2'], ], 'lastMessage' => [ 'sender' => ['type' => 'accessKey', 'id' => 'access-key-1'], ], ]))->toBe(['access-key-1', 'access-key-2']); }); it('keeps automation disabled unless policy documents are explicitly valid', function (): void { $validFlow = json_encode([ 'version' => 'v1', 'rules' => [[ 'id' => 'route-support', 'enabled' => true, 'when' => ['platform' => 'whatsapp'], 'actions' => [['type' => 'tag', 'value' => 'support']], ]], ], JSON_THROW_ON_ERROR); $invalidFlow = json_encode([ 'version' => 'v1', 'rules' => [['enabled' => true]], ], JSON_THROW_ON_ERROR); $validTemplates = json_encode([ 'version' => 'v1', 'templates' => [[ 'enabled' => true, 'projectId' => 'project-1', 'version' => 'version-1', 'locale' => 'da-DK', 'parameterKeys' => ['name'], ]], ], JSON_THROW_ON_ERROR); expect(bird_control_plane_activator::policyActive($validFlow, 'rules'))->toBeTrue() ->and(bird_control_plane_activator::policyActive($invalidFlow, 'rules'))->toBeFalse() ->and(bird_control_plane_activator::safePolicy($invalidFlow, 'rules')) ->toBe('{"version":"v1","rules":[]}') ->and(bird_control_plane_activator::policyActive($validTemplates, 'templates'))->toBeTrue() ->and(bird_control_plane_activator::policyActive( '{"version":"v1","templates":[]}', 'templates' ))->toBeFalse(); }); it('keeps deployment activation provider-read-only and exposes participant status', function (): void { $activator = file_get_contents( app_path('modules/bird/classes/bird_control_plane_activator.php') ); $route = file_get_contents(app_path('routes/birdControlPlaneRoute.php')); expect($activator)->not->toBeFalse() ->and($activator)->toContain(bird_control_plane_activator::PUBLIC_WEBHOOK_URL) ->and($activator)->toContain('provider calls are limited to channels/conversations GETs') ->and($activator)->not->toContain('CURLOPT_POST') ->and($activator)->not->toContain('CURLOPT_CUSTOMREQUEST') ->and($activator)->not->toContain('/webhook-subscriptions') ->and($route)->toContain("'participantId' => trim("); }); it('requires strong activation secrets and sanitizes identifiers', function (): void { expect(bird_control_plane_activator::secretAcceptable(str_repeat('a', 31)))->toBeFalse() ->and(bird_control_plane_activator::secretAcceptable(str_repeat('a', 32)))->toBeTrue() ->and(bird_control_plane_activator::sanitizeId('participant-123456789')) ->toBe('part...6789') ->and(bird_control_plane_activator::sanitizeId('short'))->not->toBe('short'); }); it('requires Bird to advertise both conversation events with channel filtering', function (): void { $supported = [ 'results' => [[ 'service' => 'conversations', 'events' => [ ['name' => 'conversation.created', 'filterKeys' => ['channelId']], ['name' => 'conversation.updated', 'filterKeys' => ['channelId']], ], ]], ]; $missingFilter = [ 'results' => [[ 'service' => 'conversations', 'events' => [ ['name' => 'conversation.created', 'filterKeys' => ['channelId']], ['name' => 'conversation.updated', 'filterKeys' => []], ], ]], ]; expect(bird_webhook_subscription_reconciler::capabilitiesAdvertised($supported))->toBeTrue() ->and(bird_webhook_subscription_reconciler::capabilitiesAdvertised($missingFilter)) ->toBeFalse(); }); it('matches only exact owned webhook targets and leaves unrelated subscriptions alone', function (): void { $target = ['event' => 'conversation.created', 'channelId' => 'channel-1']; $subscriptions = [ [ 'id' => 'owned', 'service' => 'conversations', 'event' => 'conversation.created', 'url' => bird_control_plane_activator::PUBLIC_WEBHOOK_URL, 'eventFilters' => [['key' => 'channelId', 'value' => 'channel-1']], ], [ 'id' => 'other-url', 'service' => 'conversations', 'event' => 'conversation.created', 'url' => 'https://other.example.test/webhook', 'eventFilters' => [['key' => 'channelId', 'value' => 'channel-1']], ], [ 'id' => 'wider-filter', 'service' => 'conversations', 'event' => 'conversation.created', 'url' => bird_control_plane_activator::PUBLIC_WEBHOOK_URL, 'eventFilters' => [ ['key' => 'channelId', 'value' => 'channel-1'], ['key' => 'status', 'value' => 'active'], ], ], ]; expect(bird_webhook_subscription_reconciler::matchingSubscriptions( $subscriptions, $target ))->toHaveCount(1) ->and(bird_webhook_subscription_reconciler::matchingSubscriptions( $subscriptions, $target )[0]['id'])->toBe('owned') ->and(bird_webhook_subscription_reconciler::targets(['channel-1', 'channel-2'])) ->toHaveCount(4); }); it('discovers an organization only when UUID metadata is unique and workspace-consistent', function (): void { $workspaceId = 'a1405560-c8d3-4b1a-877d-3f449ad95352'; $organizationId = '823fbfaf-f14e-4693-b55a-8ec1c17d649e'; $discovered = bird_webhook_subscription_reconciler::discoverOrganizationContext([ ['results' => [[ 'id' => 'channel-1', 'workspaceId' => $workspaceId, 'organizationId' => $organizationId, ]]], ['results' => [[ 'id' => 'conversation-1', 'workspaceId' => $workspaceId, 'organizationId' => $organizationId, ]]], ], $workspaceId); $mismatch = bird_webhook_subscription_reconciler::discoverOrganizationContext([ ['results' => [[ 'workspaceId' => 'b4e02c85-c6d2-4b15-8885-e09671799c61', 'organizationId' => $organizationId, ]]], ], $workspaceId); expect($discovered['candidateIds'])->toBe([$organizationId]) ->and($discovered['workspaceConsistent'])->toBeTrue() ->and($mismatch['workspaceConsistent'])->toBeFalse(); }); it('prefers organization listing and accepts only explicit workspace fallback collections', function (): void { $workspaceId = 'a1405560-c8d3-4b1a-877d-3f449ad95352'; $organizationId = '823fbfaf-f14e-4693-b55a-8ec1c17d649e'; expect(bird_webhook_subscription_reconciler::subscriptionListPath( $organizationId, $workspaceId ))->toBe( '/organizations/' . $organizationId . '/workspaces/' . $workspaceId . '/webhook-subscriptions' )->and(bird_webhook_subscription_reconciler::subscriptionListPath( '', $workspaceId ))->toBe('/workspaces/' . $workspaceId . '/webhook-subscriptions') ->and(bird_webhook_subscription_reconciler::validSubscriptionPage([ 'results' => [], ]))->toBeTrue() ->and(bird_webhook_subscription_reconciler::validSubscriptionPage([]))->toBeFalse() ->and(bird_webhook_subscription_reconciler::validSubscriptionPage([ 'error' => 'not a collection', ]))->toBeFalse(); }); it('seals the bootstrap token with RSA OAEP SHA256 and the committed public key', function (): void { $sealer = new bird_control_plane_bootstrap_sealer(); $token = str_repeat('A', 64); $ciphertext = $sealer->seal($token); expect($sealer->fingerprint())->toBe(bird_control_plane_bootstrap_sealer::KEY_FINGERPRINT) ->and(bird_control_plane_bootstrap_sealer::ciphertextValid($ciphertext))->toBeTrue() ->and($ciphertext)->not->toContain($token); }); it('round trips bootstrap ciphertext with RSA OAEP SHA256', function (): void { $key = openssl_pkey_new([ 'private_key_bits' => 3072, 'private_key_type' => OPENSSL_KEYTYPE_RSA, ]); expect($key)->not->toBeFalse(); openssl_pkey_export($key, $privatePem); $details = openssl_pkey_get_details($key); expect($details)->toBeArray()->and($details['key'] ?? null)->toBeString(); $publicPath = tempnam(sys_get_temp_dir(), 'bird-bootstrap-public-'); $privatePath = tempnam(sys_get_temp_dir(), 'bird-bootstrap-private-'); file_put_contents($publicPath, $details['key']); file_put_contents($privatePath, $privatePem); chmod($privatePath, 0600); try { $probe = new bird_control_plane_bootstrap_sealer($publicPath, ''); $sealer = new bird_control_plane_bootstrap_sealer($publicPath, $probe->fingerprint()); $token = str_repeat('D', 64); $ciphertext = base64_decode($sealer->seal($token), true); $pipes = []; $process = proc_open([ 'openssl', 'pkeyutl', '-decrypt', '-inkey', $privatePath, '-pkeyopt', 'rsa_padding_mode:oaep', '-pkeyopt', 'rsa_oaep_md:sha256', '-pkeyopt', 'rsa_mgf1_md:sha256', ], [ 0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w'], ], $pipes, null, null, ['bypass_shell' => true]); expect($process)->toBeResource(); fwrite($pipes[0], $ciphertext); fclose($pipes[0]); $plaintext = stream_get_contents($pipes[1]); fclose($pipes[1]); stream_get_contents($pipes[2]); fclose($pipes[2]); expect(proc_close($process))->toBe(0) ->and($plaintext)->toBe($token); } finally { unlink($publicPath); unlink($privatePath); } }); it('recognizes stable bootstrap metadata without rotating a valid existing token', function (): void { $token = str_repeat('B', 64); $config = [ 'control_plane_bootstrap_token_hash' => hash('sha256', $token), 'control_plane_bootstrap_algorithm' => 'RSA-OAEP-256', 'control_plane_bootstrap_key_fingerprint' => bird_control_plane_bootstrap_sealer::KEY_FINGERPRINT, 'control_plane_bootstrap_token_version' => '7', 'control_plane_bootstrap_updated_at' => '2026-07-29T17:30:00Z', 'control_plane_bootstrap_ciphertext' => base64_encode(str_repeat("\0", 384)), ]; expect(bird_control_plane_auto_activation::existingBootstrapValid($config, $token)) ->toBeTrue() ->and(bird_control_plane_auto_activation::existingBootstrapValid( $config, str_repeat('C', 64) ))->toBeFalse(); $payload = json_decode(bird_control_plane_auto_activation::sealedPayload( $token, 7, '2026-07-29T17:30:00Z' ), true, 512, JSON_THROW_ON_ERROR); expect($payload)->toBe([ 'algorithm' => 'RSA-OAEP-256', 'keyFingerprint' => bird_control_plane_bootstrap_sealer::KEY_FINGERPRINT, 'tokenVersion' => 7, 'updatedAt' => '2026-07-29T17:30:00Z', 'token' => $token, ]); }); it('keeps capabilities and bootstrap unavailable when webhook reconciliation fails', function (): void { $state = [ 'controlPlaneEnabled' => true, 'outboundMessagesEnabled' => true, 'operationsActionsEnabled' => true, 'bootstrapReady' => true, ]; $disableCount = 0; $enableCount = 0; $finalCheckCount = 0; $disable = function () use (&$state, &$disableCount): void { $disableCount++; $state = [ 'controlPlaneEnabled' => false, 'outboundMessagesEnabled' => false, 'operationsActionsEnabled' => false, 'bootstrapReady' => false, ]; }; expect(fn () => bird_control_plane_auto_activation::guardedActivation( $disable, static function (): void { }, static function (): void { throw new RuntimeException('simulated_webhook_failure'); }, function () use (&$enableCount): void { $enableCount++; }, function () use (&$finalCheckCount): void { $finalCheckCount++; } ))->toThrow(RuntimeException::class, 'simulated_webhook_failure'); expect($state)->toBe([ 'controlPlaneEnabled' => false, 'outboundMessagesEnabled' => false, 'operationsActionsEnabled' => false, 'bootstrapReady' => false, ])->and($disableCount)->toBe(2) ->and($enableCount)->toBe(0) ->and($finalCheckCount)->toBe(0); }); it('accepts staged activation only while every write capability remains disabled', function (): void { $status = [ 'schema' => ['ready' => true], 'providerReadReady' => true, 'moduleEnabled' => true, 'providerCredentialConfigured' => true, 'providerUrlConfigured' => true, 'workspaceId' => 'wor...pace', 'allowedChannelCount' => 1, 'participantConfigured' => true, 'controlPlaneCredentialConfigured' => true, 'webhookSigningCredentialConfigured' => true, 'webhookPublicUrlExact' => true, 'controlPlaneEnabled' => false, 'outboundMessagesEnabled' => false, 'operationsActionsEnabled' => false, ]; expect(bird_control_plane_auto_activation::stagedActivationReady($status))->toBeTrue(); $status['outboundMessagesEnabled'] = true; expect(bird_control_plane_auto_activation::stagedActivationReady($status))->toBeFalse(); }); it('wires fail-closed startup and a pinned local bootstrap without plaintext exposure', function (): void { $repoRoot = getenv('PLENO_REPO_ROOT_FOR_TESTS'); expect($repoRoot)->toBeString()->not->toBe(''); $start = file_get_contents($repoRoot . '/services/coolify/api/start.sh'); $local = file_get_contents($repoRoot . '/scripts/bird-control-plane-bootstrap-local.sh'); $route = file_get_contents(app_path('routes/birdControlPlaneRoute.php')); expect($start)->toContain('bird-control-plane-auto-activate.php') ->and(strpos($start, 'bird-control-plane-auto-activate.php')) ->toBeLessThan(strpos($start, 'php-fpm -D')) ->and($local)->toContain( "bootstrap_url='https://api.truckwash.io:4433/bird/control-plane/v1/bootstrap'" ) ->and($local)->toContain( "status_url='https://api.truckwash.io:4433/bird/control-plane/v1/status'" ) ->and($local)->toContain(bird_control_plane_bootstrap_sealer::KEY_FINGERPRINT) ->and($local)->toContain("destination=\"\$credential_dir/bird.gateway-token\"") ->and($local)->toContain('chmod 600 "$destination"') ->and($local)->toContain('openssl pkeyutl -decrypt') ->and($route)->toContain('/bird/control-plane/v1/bootstrap') ->and($route)->toContain("header('Cache-Control: no-store, max-age=0')") ->and($route)->toContain("\$response->error('Not found', 404)"); });