createDepartment([ 'name' => 'Edge Broker Department', ]); $session = api_fixtures()->createEdgeOperatorSession((int)$department['id']); $gateway = api_fixtures()->createClaimedEdgeGateway([ 'department_id' => (int)$department['id'], 'label' => 'Broker Gateway', ]); $validateGateway = api_client()->post( '/edge-agent/internal/gateways/' . (int)$gateway['id'] . '/validate', ['token' => (string)$gateway['agent_token']], edge_test_broker_headers() ); $validateGateway ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); expect($validateGateway->data()) ->toHaveKey('gateway_id', (int)$gateway['id']) ->toHaveKey('department_id', (int)$department['id']); $presence = api_client()->post( '/edge-agent/internal/gateways/' . (int)$gateway['id'] . '/presence', [ 'status' => 'connected', 'connection_id' => 'broker-presence-1', 'metadata' => [ 'transport' => 'ws', ], ], edge_test_broker_headers() ); $presence ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); expect($presence->data()) ->toHaveKey('connected', true) ->toHaveKey('connection_id', 'broker-presence-1'); $telemetry = api_client()->post( '/edge-agent/internal/gateways/' . (int)$gateway['id'] . '/telemetry', [ 'status' => 'ONLINE', 'metadata' => [ 'system_metrics' => [ 'cpu_load' => 0.42, 'memory_mb' => 512, ], ], 'inventory' => edge_broker_test_inventory('telemetry'), ], edge_test_broker_headers() ); $telemetry ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); expect($telemetry->data()['metadata']['system_metrics']['cpu_load'] ?? null) ->toBe(0.42); $logEntry = api_client()->post( '/edge-agent/internal/gateways/' . (int)$gateway['id'] . '/logs', [ 'level' => 'INFO', 'stream' => 'agent', 'source' => 'BROKER', 'message' => 'Broker forwarded a live gateway log.', 'context' => [ 'source' => 'broker-test', ], ], edge_test_broker_headers() ); $logEntry ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); expect($logEntry->data()) ->toHaveKey('message', 'Broker forwarded a live gateway log.'); $streamSession = api_client()->post( '/edge-gateways/' . (int)$gateway['id'] . '/stream-session', ['scopes' => ['logs', 'statistics', 'tasks']], $session['headers'] ); $streamSession ->assertStatus(201) ->assertEnvelope() ->assertSuccess(); $streamValidation = api_client()->post( '/edge-agent/internal/browser-streams/validate', ['token' => (string)$streamSession->data()['token']], edge_test_broker_headers() ); $streamValidation ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); expect($streamValidation->data()) ->toHaveKey('session_type', 'gateway-stream') ->toHaveKey('gateway_id', (int)$gateway['id']); $shellSession = api_client()->post( '/edge-gateways/' . (int)$gateway['id'] . '/shell-sessions', ['reason' => 'Broker shell validation'], $session['headers'] ); $shellSession ->assertStatus(201) ->assertEnvelope() ->assertSuccess(); $shellToken = (string)$shellSession->data()['token']; $validateShell = api_client()->post( '/edge-agent/internal/shell-sessions/validate', ['token' => $shellToken], edge_test_broker_headers() ); $validateShell ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); expect($validateShell->data()) ->toHaveKey('status', 'PENDING'); $openedShell = api_client()->post( '/edge-agent/internal/shell-sessions/opened', [ 'token' => $shellToken, 'connection_id' => 'shell-connection-1', ], edge_test_broker_headers() ); $openedShell ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); expect($openedShell->data()) ->toHaveKey('status', 'OPEN') ->toHaveKey('connection_id', 'shell-connection-1'); $closedShell = api_client()->post( '/edge-agent/internal/shell-sessions/close', [ 'token' => $shellToken, 'transcript' => "edge-broker-shell\n", 'reason' => 'agent_exit', ], edge_test_broker_headers() ); $closedShell ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); expect($closedShell->data()) ->toHaveKey('status', 'COMPLETED') ->and($closedShell->data()['transcript'] ?? null) ->toBe("edge-broker-shell\n"); $logsPage = api_client()->get('/edge-gateways/' . (int)$gateway['id'] . '/logs', $session['headers']); $logsPage ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); expect(collect_gateway_messages($logsPage->data()['log_entries'] ?? [])) ->toContain('Broker forwarded a live gateway log.'); expect(collect_gateway_messages($logsPage->data()['timeline'] ?? [])) ->toContain('GATEWAY_SHELL_SESSION_OPENED') ->toContain('GATEWAY_SHELL_SESSION_CLOSED'); expect($logsPage->data()['shell_sessions'][0]['transcript'] ?? null) ->toBe("edge-broker-shell\n"); $statisticsPage = api_client()->get('/edge-gateways/' . (int)$gateway['id'] . '/statistics', $session['headers']); $statisticsPage ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); expect($statisticsPage->data()) ->toHaveKey('system_metrics') ->and($statisticsPage->data()['system_metrics']['cpu_load'] ?? null) ->toBe(0.42); }); it('builds broker backlog and completes gateway operations through broker endpoints', function (): void { $department = api_fixtures()->createDepartment([ 'name' => 'Edge Broker Backlog Department', ]); $session = api_fixtures()->createEdgeOperatorSession((int)$department['id']); $gateway = api_fixtures()->createClaimedEdgeGateway([ 'department_id' => (int)$department['id'], 'label' => 'Broker Backlog Gateway', ]); $queuedOperation = api_client()->post('/edge-gateways/' . (int)$gateway['id'] . '/operations', [ 'type' => 'DISCOVERY', 'request' => [ 'inventory' => edge_broker_test_inventory('backlog'), ], ], $session['headers']); $queuedOperation ->assertStatus(201) ->assertEnvelope() ->assertSuccess(); $operationId = (int)($queuedOperation->data()['operation']['id'] ?? 0); expect($operationId)->toBeGreaterThan(0); $backlog = api_client()->post( '/edge-agent/internal/gateways/' . (int)$gateway['id'] . '/backlog', ['agent_instance_id' => 'broker-agent-1'], edge_test_broker_headers() ); $backlog ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); expect($backlog->data()['dispatch'] ?? []) ->toBeArray() ->not->toBeEmpty() ->and($backlog->data()['dispatch'][0]['type'] ?? null) ->toBe('TASK_DISPATCH') ->and((int)($backlog->data()['dispatch'][0]['operation']['id'] ?? 0)) ->toBe($operationId); $event = api_client()->post( '/edge-agent/internal/gateways/' . (int)$gateway['id'] . '/operations/' . $operationId . '/events', [ 'level' => 'INFO', 'code' => 'BROKER_EXECUTING', 'message' => 'Broker is executing the operation.', 'context' => [ 'progress' => 50, ], ], edge_test_broker_headers() ); $event ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); $complete = api_client()->post( '/edge-agent/internal/gateways/' . (int)$gateway['id'] . '/operations/' . $operationId . '/complete', [ 'ok' => true, 'result' => [ 'inventory' => edge_broker_test_inventory('completed'), ], ], edge_test_broker_headers() ); $complete ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); expect($complete->data()) ->toHaveKey('status', 'COMPLETED'); $operations = api_client()->get('/edge-gateways/' . (int)$gateway['id'] . '/operations', $session['headers']); $operations ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); expect(($operations->data()[0]['status'] ?? null)) ->toBe('COMPLETED'); $events = api_client()->get( '/edge-gateways/' . (int)$gateway['id'] . '/operations/' . $operationId . '/events', $session['headers'] ); $events ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); expect(collect_gateway_messages($events->data())) ->toContain('Broker is executing the operation.') ->toContain('Operation completed successfully'); $tasksPage = api_client()->get('/edge-gateways/' . (int)$gateway['id'] . '/tasks', $session['headers']); $tasksPage ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); expect($tasksPage->data()['recent_operations_summary']['completed'] ?? 0) ->toBeGreaterThanOrEqual(1); }); it('rejects invalid edge broker shared secrets', function (): void { $department = api_fixtures()->createDepartment([ 'name' => 'Edge Broker Forbidden Department', ]); $gateway = api_fixtures()->createClaimedEdgeGateway([ 'department_id' => (int)$department['id'], ]); $response = api_client()->post( '/edge-agent/internal/gateways/' . (int)$gateway['id'] . '/presence', ['status' => 'connected'], ['X-Edge-Broker-Secret' => 'wrong-secret'] ); $response ->assertStatus(403) ->assertEnvelope() ->assertSuccess(false) ->assertMessage('Invalid edge broker secret'); }); function edge_broker_test_inventory(string $suffix): array { return [[ 'device_id' => 'broker-' . $suffix, 'local_ip' => '10.40.50.60', 'model' => 'TruckWash Edge Broker', 'channel_count' => 1, 'online' => true, 'capabilities' => [ 'relay_commands' => true, ], 'metadata' => [ 'hostname' => 'broker-' . $suffix, ], ]]; } /** * @param mixed $items * @return array */ function collect_gateway_messages(mixed $items): array { if (!is_array($items)) { return []; } $messages = []; foreach ($items as $item) { if (is_array($item) && isset($item['message']) && is_string($item['message'])) { $messages[] = $item['message']; } } return $messages; }