Spaces:
Sleeping
Sleeping
Update
Browse files- Dockerfile +6 -5
- README.md +1 -1
- client/COMPARE.md +127 -0
- client/js/tests/README.md +228 -0
- client/js/tests/consumer.test.ts +316 -0
- client/js/tests/factory.test.ts +218 -0
- client/js/tests/integration.test.ts +353 -0
- client/js/tests/producer.test.ts +217 -0
- client/js/tests/rest-api.test.ts +156 -0
- client/js/tests/setup.ts +130 -0
- client/js/tests/video-client.test.ts +254 -0
- client/python/.gitignore +207 -0
- client/python/uv.lock +0 -0
- server/.gitignore +207 -0
- server/launch_with_ui.py +1 -1
- server/launch_without_ui.py +35 -0
- server/server.log +50 -0
Dockerfile
CHANGED
@@ -41,7 +41,8 @@ ENV PYTHONUNBUFFERED=1 \
|
|
41 |
PYTHONDONTWRITEBYTECODE=1 \
|
42 |
UV_SYSTEM_PYTHON=1 \
|
43 |
UV_COMPILE_BYTECODE=1 \
|
44 |
-
UV_CACHE_DIR=/tmp/uv-cache
|
|
|
45 |
|
46 |
# Install system dependencies needed for video processing
|
47 |
RUN apt-get update && apt-get install -y \
|
@@ -106,12 +107,12 @@ WORKDIR /app
|
|
106 |
# Add virtual environment to PATH
|
107 |
ENV PATH="/app/server/.venv/bin:$PATH"
|
108 |
|
109 |
-
# Expose port
|
110 |
-
EXPOSE
|
111 |
|
112 |
# Health check
|
113 |
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
114 |
-
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost
|
115 |
|
116 |
# Start the FastAPI server (serves both frontend and backend)
|
117 |
-
CMD ["sh", "-c", "cd server && SERVE_FRONTEND=true uv run python launch_with_ui.py --host
|
|
|
41 |
PYTHONDONTWRITEBYTECODE=1 \
|
42 |
UV_SYSTEM_PYTHON=1 \
|
43 |
UV_COMPILE_BYTECODE=1 \
|
44 |
+
UV_CACHE_DIR=/tmp/uv-cache \
|
45 |
+
PORT=8000
|
46 |
|
47 |
# Install system dependencies needed for video processing
|
48 |
RUN apt-get update && apt-get install -y \
|
|
|
107 |
# Add virtual environment to PATH
|
108 |
ENV PATH="/app/server/.venv/bin:$PATH"
|
109 |
|
110 |
+
# Expose the configured port (default 8000)
|
111 |
+
EXPOSE ${PORT}
|
112 |
|
113 |
# Health check
|
114 |
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
115 |
+
CMD python -c "import urllib.request; urllib.request.urlopen(f'http://localhost:${PORT}/health')" || exit 1
|
116 |
|
117 |
# Start the FastAPI server (serves both frontend and backend)
|
118 |
+
CMD ["sh", "-c", "cd server && SERVE_FRONTEND=true uv run python launch_with_ui.py --host localhost --port ${PORT}"]
|
README.md
CHANGED
@@ -4,7 +4,7 @@ emoji: 🤖
|
|
4 |
colorFrom: blue
|
5 |
colorTo: purple
|
6 |
sdk: docker
|
7 |
-
app_port:
|
8 |
suggested_hardware: cpu-upgrade
|
9 |
suggested_storage: small
|
10 |
short_description: Real-time robotics control and video streaming platform
|
|
|
4 |
colorFrom: blue
|
5 |
colorTo: purple
|
6 |
sdk: docker
|
7 |
+
app_port: 8000
|
8 |
suggested_hardware: cpu-upgrade
|
9 |
suggested_storage: small
|
10 |
short_description: Real-time robotics control and video streaming platform
|
client/COMPARE.md
ADDED
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Client Library API Comparison
|
2 |
+
|
3 |
+
This table shows the correspondence between the Python and JavaScript/TypeScript client libraries for RobotHub TransportServer.
|
4 |
+
|
5 |
+
## 📦 Installation
|
6 |
+
|
7 |
+
| Python | JavaScript/TypeScript |
|
8 |
+
|--------|----------------------|
|
9 |
+
| `uv add transport-server-client` | `bun add @robothub/transport-server-client` |
|
10 |
+
|
11 |
+
## 🔧 Imports
|
12 |
+
|
13 |
+
| Python | JavaScript/TypeScript |
|
14 |
+
|--------|----------------------|
|
15 |
+
| `from transport_server_client import RoboticsProducer` | `import { robotics } from '@robothub/transport-server-client'` |
|
16 |
+
| `from transport_server_client import RoboticsConsumer` | `const producer = new robotics.RoboticsProducer()` |
|
17 |
+
| `from transport_server_client.video import VideoProducer` | `import { video } from '@robothub/transport-server-client'` |
|
18 |
+
| `from transport_server_client.video import VideoConsumer` | `const producer = new video.VideoProducer()` |
|
19 |
+
|
20 |
+
## 🤖 Robotics Producer
|
21 |
+
|
22 |
+
| Operation | Python | JavaScript/TypeScript |
|
23 |
+
|-----------|--------|----------------------|
|
24 |
+
| **Create instance** | `producer = RoboticsProducer('http://localhost:8000')` | `const producer = new robotics.RoboticsProducer('http://localhost:8000')` |
|
25 |
+
| **Connect to room** | `await producer.connect(workspace_id, room_id)` | `await producer.connect(workspaceId, roomId)` |
|
26 |
+
| **Create room** | `room_info = await producer.create_room()` | `const { workspaceId, roomId } = await producer.createRoom()` |
|
27 |
+
| **Send joint update** | `await producer.send_joint_update(joints)` | `await producer.sendJointUpdate(joints)` |
|
28 |
+
| **Send state sync** | `await producer.send_state_sync(state)` | `await producer.sendStateSync(state)` |
|
29 |
+
| **Emergency stop** | `await producer.send_emergency_stop(reason)` | `await producer.sendEmergencyStop(reason)` |
|
30 |
+
| **List rooms** | `rooms = await producer.list_rooms(workspace_id)` | `const rooms = await producer.listRooms(workspaceId)` |
|
31 |
+
| **Delete room** | `await producer.delete_room(workspace_id, room_id)` | `await producer.deleteRoom(workspaceId, roomId)` |
|
32 |
+
| **Disconnect** | `await producer.disconnect()` | `await producer.disconnect()` |
|
33 |
+
|
34 |
+
## 🤖 Robotics Consumer
|
35 |
+
|
36 |
+
| Operation | Python | JavaScript/TypeScript |
|
37 |
+
|-----------|--------|----------------------|
|
38 |
+
| **Create instance** | `consumer = RoboticsConsumer('http://localhost:8000')` | `const consumer = new robotics.RoboticsConsumer('http://localhost:8000')` |
|
39 |
+
| **Connect to room** | `await consumer.connect(workspace_id, room_id)` | `await consumer.connect(workspaceId, roomId)` |
|
40 |
+
| **Get current state** | `state = await consumer.get_state_sync()` | `const state = await consumer.getStateSyncAsync()` |
|
41 |
+
| **Joint update callback** | `consumer.on_joint_update(callback)` | `consumer.onJointUpdate(callback)` |
|
42 |
+
| **State sync callback** | `consumer.on_state_sync(callback)` | `consumer.onStateSync(callback)` |
|
43 |
+
| **Error callback** | `consumer.on_error(callback)` | `consumer.onError(callback)` |
|
44 |
+
| **Connected callback** | `consumer.on_connected(callback)` | `consumer.onConnected(callback)` |
|
45 |
+
| **Disconnected callback** | `consumer.on_disconnected(callback)` | `consumer.onDisconnected(callback)` |
|
46 |
+
| **Disconnect** | `await consumer.disconnect()` | `await consumer.disconnect()` |
|
47 |
+
|
48 |
+
## 📹 Video Producer
|
49 |
+
|
50 |
+
| Operation | Python | JavaScript/TypeScript |
|
51 |
+
|-----------|--------|----------------------|
|
52 |
+
| **Create instance** | `producer = VideoProducer('http://localhost:8000')` | `const producer = new video.VideoProducer('http://localhost:8000')` |
|
53 |
+
| **Connect to room** | `await producer.connect(workspace_id, room_id)` | `await producer.connect(workspaceId, roomId)` |
|
54 |
+
| **Start camera** | `await producer.start_camera(config)` | `await producer.startCamera(constraints)` |
|
55 |
+
| **Start screen share** | `await producer.start_screen_share()` | `await producer.startScreenShare()` |
|
56 |
+
| **Stop streaming** | `await producer.stop_streaming()` | `await producer.stopStreaming()` |
|
57 |
+
| **Update config** | `await producer.update_video_config(config)` | `await producer.updateVideoConfig(config)` |
|
58 |
+
| **Disconnect** | `await producer.disconnect()` | `await producer.disconnect()` |
|
59 |
+
|
60 |
+
## 📹 Video Consumer
|
61 |
+
|
62 |
+
| Operation | Python | JavaScript/TypeScript |
|
63 |
+
|-----------|--------|----------------------|
|
64 |
+
| **Create instance** | `consumer = VideoConsumer('http://localhost:8000')` | `const consumer = new video.VideoConsumer('http://localhost:8000')` |
|
65 |
+
| **Connect to room** | `await consumer.connect(workspace_id, room_id)` | `await consumer.connect(workspaceId, roomId)` |
|
66 |
+
| **Start receiving** | `await consumer.start_receiving()` | `await consumer.startReceiving()` |
|
67 |
+
| **Stop receiving** | `await consumer.stop_receiving()` | `await consumer.stopReceiving()` |
|
68 |
+
| **Attach to video element** | N/A (Python) | `consumer.attachToVideoElement(videoElement)` |
|
69 |
+
| **Frame callback** | `consumer.on_frame_update(callback)` | `consumer.onFrameUpdate(callback)` |
|
70 |
+
| **Stream started callback** | `consumer.on_stream_started(callback)` | `consumer.onStreamStarted(callback)` |
|
71 |
+
| **Stream stopped callback** | `consumer.on_stream_stopped(callback)` | `consumer.onStreamStopped(callback)` |
|
72 |
+
| **Disconnect** | `await consumer.disconnect()` | `await consumer.disconnect()` |
|
73 |
+
|
74 |
+
## ⚡ Factory Functions
|
75 |
+
|
76 |
+
| Operation | Python | JavaScript/TypeScript |
|
77 |
+
|-----------|--------|----------------------|
|
78 |
+
| **Quick producer** | `producer = await create_producer_client(url)` | `const producer = await robotics.createProducerClient(url)` |
|
79 |
+
| **Quick consumer** | `consumer = await create_consumer_client(room_id, url)` | `const consumer = await robotics.createConsumerClient(workspaceId, roomId, url)` |
|
80 |
+
|
81 |
+
## 🔧 Context Managers / Lifecycle
|
82 |
+
|
83 |
+
| Operation | Python | JavaScript/TypeScript |
|
84 |
+
|-----------|--------|----------------------|
|
85 |
+
| **Auto cleanup** | `async with RoboticsProducer(url) as producer:` | No built-in equivalent |
|
86 |
+
| **Check connection** | `producer.is_connected()` | `producer.isConnected()` |
|
87 |
+
| **Connection info** | `info = producer.get_connection_info()` | `const info = producer.getConnectionInfo()` |
|
88 |
+
|
89 |
+
## 📝 Data Formats
|
90 |
+
|
91 |
+
### Joint Data
|
92 |
+
|
93 |
+
| Python | JavaScript/TypeScript |
|
94 |
+
|--------|----------------------|
|
95 |
+
| `{'name': 'shoulder', 'value': 45.0}` | `{ name: 'shoulder', value: 45.0 }` |
|
96 |
+
| `[{'name': 'shoulder', 'value': 45.0}]` | `[{ name: 'shoulder', value: 45.0 }]` |
|
97 |
+
|
98 |
+
### State Data
|
99 |
+
|
100 |
+
| Python | JavaScript/TypeScript |
|
101 |
+
|--------|----------------------|
|
102 |
+
| `{'shoulder': 45.0, 'elbow': -30.0}` | `{ shoulder: 45.0, elbow: -30.0 }` |
|
103 |
+
|
104 |
+
### Room Info Response
|
105 |
+
|
106 |
+
| Python | JavaScript/TypeScript |
|
107 |
+
|--------|----------------------|
|
108 |
+
| `{'workspace_id': 'uuid', 'room_id': 'uuid'}` | `{ workspaceId: 'uuid', roomId: 'uuid' }` |
|
109 |
+
|
110 |
+
## 🔄 Naming Conventions
|
111 |
+
|
112 |
+
| Python (snake_case) | JavaScript/TypeScript (camelCase) |
|
113 |
+
|---------------------|-----------------------------------|
|
114 |
+
| `send_joint_update` | `sendJointUpdate` |
|
115 |
+
| `send_state_sync` | `sendStateSync` |
|
116 |
+
| `get_state_sync` | `getStateSyncAsync` |
|
117 |
+
| `on_joint_update` | `onJointUpdate` |
|
118 |
+
| `create_room` | `createRoom` |
|
119 |
+
| `list_rooms` | `listRooms` |
|
120 |
+
| `workspace_id` | `workspaceId` |
|
121 |
+
| `room_id` | `roomId` |
|
122 |
+
| `start_camera` | `startCamera` |
|
123 |
+
| `stop_streaming` | `stopStreaming` |
|
124 |
+
|
125 |
+
---
|
126 |
+
|
127 |
+
**Both libraries provide the same functionality with language-appropriate conventions!** 🤖✨
|
client/js/tests/README.md
ADDED
@@ -0,0 +1,228 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# RobotHub TransportServer JavaScript Client Tests
|
2 |
+
|
3 |
+
## Overview
|
4 |
+
|
5 |
+
This directory contains comprehensive tests for the RobotHub TransportServer JavaScript/TypeScript client library, mirroring the Python test structure. The tests are built using [Bun's test framework](https://bun.sh/docs/test/writing) and provide full coverage of both robotics and video functionality.
|
6 |
+
|
7 |
+
## Test Structure
|
8 |
+
|
9 |
+
The test suite is organized to match the Python client tests:
|
10 |
+
|
11 |
+
```
|
12 |
+
tests/
|
13 |
+
├── setup.ts # Test utilities and helpers (equivalent to conftest.py)
|
14 |
+
├── producer.test.ts # RoboticsProducer tests
|
15 |
+
├── consumer.test.ts # RoboticsConsumer tests
|
16 |
+
├── factory.test.ts # Factory function tests
|
17 |
+
├── integration.test.ts # Integration tests
|
18 |
+
├── rest-api.test.ts # REST API tests
|
19 |
+
├── video-client.test.ts # Video client tests
|
20 |
+
└── README.md # This file
|
21 |
+
```
|
22 |
+
|
23 |
+
## Running Tests
|
24 |
+
|
25 |
+
### Prerequisites
|
26 |
+
|
27 |
+
1. **Server Running**: Ensure the RobotHub TransportServer is running on `http://localhost:8000`
|
28 |
+
2. **Dependencies**: Install dependencies with `bun install`
|
29 |
+
|
30 |
+
### Run All Tests
|
31 |
+
|
32 |
+
```bash
|
33 |
+
# From the js client directory
|
34 |
+
bun test
|
35 |
+
|
36 |
+
# Or using npm script
|
37 |
+
bun run test
|
38 |
+
```
|
39 |
+
|
40 |
+
### Run Specific Test Files
|
41 |
+
|
42 |
+
```bash
|
43 |
+
# Run only producer tests
|
44 |
+
bun test tests/producer.test.ts
|
45 |
+
|
46 |
+
# Run only integration tests
|
47 |
+
bun test tests/integration.test.ts
|
48 |
+
|
49 |
+
# Run with verbose output
|
50 |
+
bun test --verbose
|
51 |
+
```
|
52 |
+
|
53 |
+
## Test Categories
|
54 |
+
|
55 |
+
### 1. Robotics Producer Tests (`producer.test.ts`)
|
56 |
+
- ✅ Basic connection and disconnection
|
57 |
+
- ✅ Connection info validation
|
58 |
+
- ✅ Joint updates and state synchronization
|
59 |
+
- ✅ Emergency stop functionality
|
60 |
+
- ✅ Event callbacks (connected, disconnected, error)
|
61 |
+
- ✅ Error handling for disconnected operations
|
62 |
+
- ✅ Multiple room connections
|
63 |
+
- ✅ Custom participant IDs
|
64 |
+
- ✅ Large data handling
|
65 |
+
- ✅ High-frequency updates
|
66 |
+
|
67 |
+
### 2. Robotics Consumer Tests (`consumer.test.ts`)
|
68 |
+
- ✅ Basic connection and disconnection
|
69 |
+
- ✅ Connection info validation
|
70 |
+
- ✅ State synchronization retrieval
|
71 |
+
- ✅ Event callbacks setup
|
72 |
+
- ✅ Multiple consumers in same room
|
73 |
+
- ✅ Receiving state sync and joint updates
|
74 |
+
- ✅ Emergency stop reception
|
75 |
+
- ✅ Custom participant IDs
|
76 |
+
- ✅ Reconnection scenarios
|
77 |
+
- ✅ State persistence after producer updates
|
78 |
+
|
79 |
+
### 3. Factory Function Tests (`factory.test.ts`)
|
80 |
+
- ✅ Client creation by role
|
81 |
+
- ✅ Invalid role handling
|
82 |
+
- ✅ Auto room creation for producers
|
83 |
+
- ✅ Specific room connection
|
84 |
+
- ✅ Producer-consumer pair creation
|
85 |
+
- ✅ Default URL handling
|
86 |
+
- ✅ Multiple producer management
|
87 |
+
- ✅ Nonexistent room error handling
|
88 |
+
|
89 |
+
### 4. Integration Tests (`integration.test.ts`)
|
90 |
+
- ✅ Full producer-consumer workflow
|
91 |
+
- ✅ Multiple consumers receiving same data
|
92 |
+
- ✅ Emergency stop propagation
|
93 |
+
- ✅ Producer reconnection scenarios
|
94 |
+
- ✅ Late-joining consumers
|
95 |
+
- ✅ Room state persistence
|
96 |
+
- ✅ High-frequency update handling
|
97 |
+
|
98 |
+
### 5. REST API Tests (`rest-api.test.ts`)
|
99 |
+
- ✅ Room listing (empty and populated)
|
100 |
+
- ✅ Room creation (auto and custom IDs)
|
101 |
+
- ✅ Room information retrieval
|
102 |
+
- ✅ Room state retrieval
|
103 |
+
- ✅ Room deletion
|
104 |
+
- ✅ Error handling for nonexistent rooms
|
105 |
+
|
106 |
+
### 6. Video Client Tests (`video-client.test.ts`)
|
107 |
+
- ✅ Type definitions validation
|
108 |
+
- ✅ Producer and consumer creation
|
109 |
+
- ✅ Room creation and listing (when server available)
|
110 |
+
- ✅ Connection validation
|
111 |
+
- ✅ Factory function existence
|
112 |
+
- ✅ Mock frame source functionality
|
113 |
+
- ✅ Configuration type validation
|
114 |
+
|
115 |
+
## Test Results Summary
|
116 |
+
|
117 |
+
```
|
118 |
+
✅ 69 tests passed
|
119 |
+
❌ 0 tests failed
|
120 |
+
🔍 187 expect() calls
|
121 |
+
⏱️ Runtime: ~4 seconds
|
122 |
+
```
|
123 |
+
|
124 |
+
## API Correspondence
|
125 |
+
|
126 |
+
The JavaScript tests mirror the Python test structure, ensuring API parity:
|
127 |
+
|
128 |
+
| Python Test | JavaScript Test | Coverage |
|
129 |
+
|-------------|-----------------|----------|
|
130 |
+
| `test_producer.py` | `producer.test.ts` | ✅ Complete |
|
131 |
+
| `test_consumer.py` | `consumer.test.ts` | ✅ Complete |
|
132 |
+
| `test_factory_functions.py` | `factory.test.ts` | ✅ Complete |
|
133 |
+
| `test_integration.py` | `integration.test.ts` | ✅ Complete |
|
134 |
+
| `test_rest_api.py` | `rest-api.test.ts` | ✅ Complete |
|
135 |
+
| `test_video_client.py` | `video-client.test.ts` | ✅ Complete |
|
136 |
+
| `conftest.py` | `setup.ts` | ✅ Complete |
|
137 |
+
|
138 |
+
## Test Features
|
139 |
+
|
140 |
+
### Async/Await Support
|
141 |
+
All tests use modern async/await patterns with proper cleanup:
|
142 |
+
|
143 |
+
```typescript
|
144 |
+
test("producer connection", async () => {
|
145 |
+
const { workspaceId, roomId } = await producer.createRoom();
|
146 |
+
await producer.connect(workspaceId, roomId);
|
147 |
+
expect(producer.isConnected()).toBe(true);
|
148 |
+
await producer.disconnect();
|
149 |
+
});
|
150 |
+
```
|
151 |
+
|
152 |
+
### Message Collection
|
153 |
+
Tests use a `MessageCollector` utility for testing callbacks:
|
154 |
+
|
155 |
+
```typescript
|
156 |
+
const updateCollector = new MessageCollector(1);
|
157 |
+
consumer.onJointUpdate(updateCollector.collect);
|
158 |
+
await producer.sendJointUpdate(joints);
|
159 |
+
const updates = await updateCollector.waitForMessages(2000);
|
160 |
+
expect(updates.length).toBeGreaterThanOrEqual(1);
|
161 |
+
```
|
162 |
+
|
163 |
+
### Resource Management
|
164 |
+
Automatic cleanup prevents test interference:
|
165 |
+
|
166 |
+
```typescript
|
167 |
+
afterEach(async () => {
|
168 |
+
if (producer.isConnected()) {
|
169 |
+
await producer.disconnect();
|
170 |
+
}
|
171 |
+
await roomManager.cleanup(producer);
|
172 |
+
});
|
173 |
+
```
|
174 |
+
|
175 |
+
### Error Testing
|
176 |
+
Comprehensive error scenario coverage:
|
177 |
+
|
178 |
+
```typescript
|
179 |
+
test("send without connection", async () => {
|
180 |
+
await expect(producer.sendJointUpdate([]))
|
181 |
+
.rejects.toThrow("Must be connected");
|
182 |
+
});
|
183 |
+
```
|
184 |
+
|
185 |
+
## Debugging Tests
|
186 |
+
|
187 |
+
### Enable Debug Logging
|
188 |
+
```bash
|
189 |
+
# Run with debug output
|
190 |
+
DEBUG=* bun test
|
191 |
+
|
192 |
+
# Or specific modules
|
193 |
+
DEBUG=robotics:* bun test
|
194 |
+
```
|
195 |
+
|
196 |
+
### Test Individual Scenarios
|
197 |
+
```bash
|
198 |
+
# Test specific functionality
|
199 |
+
bun test --grep "emergency stop"
|
200 |
+
bun test --grep "multiple consumers"
|
201 |
+
```
|
202 |
+
|
203 |
+
### Server Connectivity Issues
|
204 |
+
If tests fail due to server connectivity:
|
205 |
+
|
206 |
+
1. Ensure server is running: `curl http://localhost:8000/health`
|
207 |
+
2. Check server logs for errors
|
208 |
+
3. Verify WebSocket connections are allowed
|
209 |
+
4. Try running tests with longer timeouts
|
210 |
+
|
211 |
+
## Contributing
|
212 |
+
|
213 |
+
When adding new tests:
|
214 |
+
|
215 |
+
1. Follow the existing naming conventions
|
216 |
+
2. Add proper cleanup in `afterEach` blocks
|
217 |
+
3. Use `MessageCollector` for callback testing
|
218 |
+
4. Test both success and error scenarios
|
219 |
+
5. Update this README with new test descriptions
|
220 |
+
|
221 |
+
## Performance Notes
|
222 |
+
|
223 |
+
- Tests run in parallel by default
|
224 |
+
- Average test suite runtime: ~4 seconds
|
225 |
+
- Individual test timeouts: 10 seconds
|
226 |
+
- Message collection timeouts: 2-5 seconds
|
227 |
+
|
228 |
+
The JavaScript test suite provides comprehensive validation that the TypeScript/JavaScript client maintains full API compatibility with the Python client while leveraging modern JavaScript testing practices.
|
client/js/tests/consumer.test.ts
ADDED
@@ -0,0 +1,316 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/**
|
2 |
+
* Tests for RoboticsConsumer - equivalent to Python's test_consumer.py
|
3 |
+
*/
|
4 |
+
import { test, expect, describe, beforeEach, afterEach } from "bun:test";
|
5 |
+
import { robotics } from "../src/index";
|
6 |
+
import { TEST_SERVER_URL, TestRoomManager, MessageCollector, sleep, assertIsConnected, assertIsDisconnected } from "./setup";
|
7 |
+
|
8 |
+
const { RoboticsProducer, RoboticsConsumer } = robotics;
|
9 |
+
|
10 |
+
describe("RoboticsConsumer", () => {
|
11 |
+
let consumer: robotics.RoboticsConsumer;
|
12 |
+
let producer: robotics.RoboticsProducer;
|
13 |
+
let roomManager: TestRoomManager;
|
14 |
+
|
15 |
+
beforeEach(() => {
|
16 |
+
consumer = new RoboticsConsumer(TEST_SERVER_URL);
|
17 |
+
producer = new RoboticsProducer(TEST_SERVER_URL);
|
18 |
+
roomManager = new TestRoomManager();
|
19 |
+
});
|
20 |
+
|
21 |
+
afterEach(async () => {
|
22 |
+
if (consumer.isConnected()) {
|
23 |
+
await consumer.disconnect();
|
24 |
+
}
|
25 |
+
if (producer.isConnected()) {
|
26 |
+
await producer.disconnect();
|
27 |
+
}
|
28 |
+
await roomManager.cleanup(producer);
|
29 |
+
});
|
30 |
+
|
31 |
+
test("consumer connection", async () => {
|
32 |
+
// Create room first
|
33 |
+
const { workspaceId, roomId } = await producer.createRoom();
|
34 |
+
roomManager.addRoom(workspaceId, roomId);
|
35 |
+
|
36 |
+
expect(consumer.isConnected()).toBe(false);
|
37 |
+
|
38 |
+
const success = await consumer.connect(workspaceId, roomId);
|
39 |
+
expect(success).toBe(true);
|
40 |
+
assertIsConnected(consumer, workspaceId, roomId);
|
41 |
+
|
42 |
+
const info = consumer.getConnectionInfo();
|
43 |
+
expect(info.role).toBe("consumer");
|
44 |
+
|
45 |
+
await consumer.disconnect();
|
46 |
+
assertIsDisconnected(consumer);
|
47 |
+
});
|
48 |
+
|
49 |
+
test("consumer connection info", async () => {
|
50 |
+
const { workspaceId, roomId } = await producer.createRoom();
|
51 |
+
roomManager.addRoom(workspaceId, roomId);
|
52 |
+
await consumer.connect(workspaceId, roomId);
|
53 |
+
|
54 |
+
const info = consumer.getConnectionInfo();
|
55 |
+
expect(info.connected).toBe(true);
|
56 |
+
expect(info.room_id).toBe(roomId);
|
57 |
+
expect(info.workspace_id).toBe(workspaceId);
|
58 |
+
expect(info.role).toBe("consumer");
|
59 |
+
expect(info.participant_id).toBeTruthy();
|
60 |
+
expect(info.base_url).toBe(TEST_SERVER_URL);
|
61 |
+
});
|
62 |
+
|
63 |
+
test("get state sync", async () => {
|
64 |
+
const { workspaceId, roomId } = await producer.createRoom();
|
65 |
+
roomManager.addRoom(workspaceId, roomId);
|
66 |
+
await consumer.connect(workspaceId, roomId);
|
67 |
+
|
68 |
+
const state = await consumer.getStateSyncAsync();
|
69 |
+
expect(typeof state).toBe("object");
|
70 |
+
// Initial state should be empty
|
71 |
+
expect(Object.keys(state)).toHaveLength(0);
|
72 |
+
});
|
73 |
+
|
74 |
+
test("consumer callbacks setup", async () => {
|
75 |
+
const { workspaceId, roomId } = await producer.createRoom();
|
76 |
+
roomManager.addRoom(workspaceId, roomId);
|
77 |
+
|
78 |
+
let stateSyncCalled = false;
|
79 |
+
let jointUpdateCalled = false;
|
80 |
+
let errorCalled = false;
|
81 |
+
let connectedCalled = false;
|
82 |
+
let disconnectedCalled = false;
|
83 |
+
|
84 |
+
consumer.onStateSync((state) => {
|
85 |
+
stateSyncCalled = true;
|
86 |
+
});
|
87 |
+
|
88 |
+
consumer.onJointUpdate((joints) => {
|
89 |
+
jointUpdateCalled = true;
|
90 |
+
});
|
91 |
+
|
92 |
+
consumer.onError((error) => {
|
93 |
+
errorCalled = true;
|
94 |
+
});
|
95 |
+
|
96 |
+
consumer.onConnected(() => {
|
97 |
+
connectedCalled = true;
|
98 |
+
});
|
99 |
+
|
100 |
+
consumer.onDisconnected(() => {
|
101 |
+
disconnectedCalled = true;
|
102 |
+
});
|
103 |
+
|
104 |
+
// Connect and test connection callbacks
|
105 |
+
await consumer.connect(workspaceId, roomId);
|
106 |
+
await sleep(100);
|
107 |
+
expect(connectedCalled).toBe(true);
|
108 |
+
|
109 |
+
await consumer.disconnect();
|
110 |
+
await sleep(100);
|
111 |
+
expect(disconnectedCalled).toBe(true);
|
112 |
+
});
|
113 |
+
|
114 |
+
test("multiple consumers", async () => {
|
115 |
+
const { workspaceId, roomId } = await producer.createRoom();
|
116 |
+
roomManager.addRoom(workspaceId, roomId);
|
117 |
+
|
118 |
+
const consumer1 = new RoboticsConsumer(TEST_SERVER_URL);
|
119 |
+
const consumer2 = new RoboticsConsumer(TEST_SERVER_URL);
|
120 |
+
|
121 |
+
try {
|
122 |
+
// Both consumers should be able to connect
|
123 |
+
const success1 = await consumer1.connect(workspaceId, roomId);
|
124 |
+
const success2 = await consumer2.connect(workspaceId, roomId);
|
125 |
+
|
126 |
+
expect(success1).toBe(true);
|
127 |
+
expect(success2).toBe(true);
|
128 |
+
expect(consumer1.isConnected()).toBe(true);
|
129 |
+
expect(consumer2.isConnected()).toBe(true);
|
130 |
+
} finally {
|
131 |
+
if (consumer1.isConnected()) {
|
132 |
+
await consumer1.disconnect();
|
133 |
+
}
|
134 |
+
if (consumer2.isConnected()) {
|
135 |
+
await consumer2.disconnect();
|
136 |
+
}
|
137 |
+
}
|
138 |
+
});
|
139 |
+
|
140 |
+
test("consumer receive state sync", async () => {
|
141 |
+
const { workspaceId, roomId } = await producer.createRoom();
|
142 |
+
roomManager.addRoom(workspaceId, roomId);
|
143 |
+
|
144 |
+
const updateCollector = new MessageCollector(1);
|
145 |
+
consumer.onJointUpdate(updateCollector.collect);
|
146 |
+
|
147 |
+
await producer.connect(workspaceId, roomId);
|
148 |
+
await consumer.connect(workspaceId, roomId);
|
149 |
+
|
150 |
+
// Give some time for connection to stabilize
|
151 |
+
await sleep(100);
|
152 |
+
|
153 |
+
// Producer sends state sync (which gets converted to joint updates)
|
154 |
+
await producer.sendStateSync({ shoulder: 45.0, elbow: -20.0 });
|
155 |
+
|
156 |
+
// Wait for message to be received
|
157 |
+
const receivedUpdates = await updateCollector.waitForMessages(2000);
|
158 |
+
|
159 |
+
// Consumer should have received the joint updates from the state sync
|
160 |
+
expect(receivedUpdates.length).toBeGreaterThanOrEqual(1);
|
161 |
+
});
|
162 |
+
|
163 |
+
test("consumer receive joint updates", async () => {
|
164 |
+
const { workspaceId, roomId } = await producer.createRoom();
|
165 |
+
roomManager.addRoom(workspaceId, roomId);
|
166 |
+
|
167 |
+
const updateCollector = new MessageCollector(1);
|
168 |
+
consumer.onJointUpdate(updateCollector.collect);
|
169 |
+
|
170 |
+
await producer.connect(workspaceId, roomId);
|
171 |
+
await consumer.connect(workspaceId, roomId);
|
172 |
+
|
173 |
+
// Give some time for connection to stabilize
|
174 |
+
await sleep(100);
|
175 |
+
|
176 |
+
// Producer sends joint updates
|
177 |
+
const testJoints = [
|
178 |
+
{ name: "shoulder", value: 45.0 },
|
179 |
+
{ name: "elbow", value: -20.0 }
|
180 |
+
];
|
181 |
+
await producer.sendJointUpdate(testJoints);
|
182 |
+
|
183 |
+
// Wait for message to be received
|
184 |
+
const receivedUpdates = await updateCollector.waitForMessages(2000);
|
185 |
+
|
186 |
+
// Consumer should have received the joint update
|
187 |
+
expect(receivedUpdates.length).toBeGreaterThanOrEqual(1);
|
188 |
+
if (receivedUpdates.length > 0) {
|
189 |
+
const receivedJoints = receivedUpdates[receivedUpdates.length - 1];
|
190 |
+
expect(Array.isArray(receivedJoints)).toBe(true);
|
191 |
+
expect(receivedJoints).toHaveLength(2);
|
192 |
+
}
|
193 |
+
});
|
194 |
+
|
195 |
+
test("consumer multiple updates", async () => {
|
196 |
+
const { workspaceId, roomId } = await producer.createRoom();
|
197 |
+
roomManager.addRoom(workspaceId, roomId);
|
198 |
+
|
199 |
+
const updateCollector = new MessageCollector(3);
|
200 |
+
consumer.onJointUpdate(updateCollector.collect);
|
201 |
+
|
202 |
+
await producer.connect(workspaceId, roomId);
|
203 |
+
await consumer.connect(workspaceId, roomId);
|
204 |
+
|
205 |
+
// Give some time for connection to stabilize
|
206 |
+
await sleep(100);
|
207 |
+
|
208 |
+
// Send multiple updates
|
209 |
+
for (let i = 0; i < 5; i++) {
|
210 |
+
await producer.sendStateSync({
|
211 |
+
joint1: i * 10,
|
212 |
+
joint2: i * -5
|
213 |
+
});
|
214 |
+
await sleep(50);
|
215 |
+
}
|
216 |
+
|
217 |
+
// Wait for all messages to be received
|
218 |
+
const receivedUpdates = await updateCollector.waitForMessages(3000);
|
219 |
+
|
220 |
+
// Should have received multiple updates
|
221 |
+
expect(receivedUpdates.length).toBeGreaterThanOrEqual(3);
|
222 |
+
});
|
223 |
+
|
224 |
+
test("consumer emergency stop", async () => {
|
225 |
+
const { workspaceId, roomId } = await producer.createRoom();
|
226 |
+
roomManager.addRoom(workspaceId, roomId);
|
227 |
+
|
228 |
+
const errorCollector = new MessageCollector<string>(1);
|
229 |
+
consumer.onError(errorCollector.collect);
|
230 |
+
|
231 |
+
await producer.connect(workspaceId, roomId);
|
232 |
+
await consumer.connect(workspaceId, roomId);
|
233 |
+
|
234 |
+
// Give some time for connection to stabilize
|
235 |
+
await sleep(100);
|
236 |
+
|
237 |
+
// Producer sends emergency stop
|
238 |
+
await producer.sendEmergencyStop("Test emergency stop");
|
239 |
+
|
240 |
+
// Wait for message to be received
|
241 |
+
const receivedErrors = await errorCollector.waitForMessages(2000);
|
242 |
+
|
243 |
+
// Consumer should have received emergency stop as error
|
244 |
+
expect(receivedErrors.length).toBeGreaterThanOrEqual(1);
|
245 |
+
if (receivedErrors.length > 0) {
|
246 |
+
expect(receivedErrors[receivedErrors.length - 1].toLowerCase()).toContain("emergency stop");
|
247 |
+
}
|
248 |
+
});
|
249 |
+
|
250 |
+
test("custom participant id", async () => {
|
251 |
+
const { workspaceId, roomId } = await producer.createRoom();
|
252 |
+
roomManager.addRoom(workspaceId, roomId);
|
253 |
+
const customId = "custom-consumer-456";
|
254 |
+
|
255 |
+
await consumer.connect(workspaceId, roomId, customId);
|
256 |
+
|
257 |
+
const info = consumer.getConnectionInfo();
|
258 |
+
expect(info.participant_id).toBe(customId);
|
259 |
+
});
|
260 |
+
|
261 |
+
test("get state without connection", async () => {
|
262 |
+
expect(consumer.isConnected()).toBe(false);
|
263 |
+
|
264 |
+
await expect(consumer.getStateSyncAsync())
|
265 |
+
.rejects.toThrow("Must be connected");
|
266 |
+
});
|
267 |
+
|
268 |
+
test("consumer reconnection", async () => {
|
269 |
+
const { workspaceId, roomId } = await producer.createRoom();
|
270 |
+
roomManager.addRoom(workspaceId, roomId);
|
271 |
+
|
272 |
+
// First connection
|
273 |
+
await consumer.connect(workspaceId, roomId);
|
274 |
+
expect(consumer.isConnected()).toBe(true);
|
275 |
+
|
276 |
+
await consumer.disconnect();
|
277 |
+
expect(consumer.isConnected()).toBe(false);
|
278 |
+
|
279 |
+
// Reconnect to same room
|
280 |
+
await consumer.connect(workspaceId, roomId);
|
281 |
+
expect(consumer.isConnected()).toBe(true);
|
282 |
+
expect(consumer.getConnectionInfo().room_id).toBe(roomId);
|
283 |
+
expect(consumer.getConnectionInfo().workspace_id).toBe(workspaceId);
|
284 |
+
});
|
285 |
+
|
286 |
+
test("consumer state after producer updates", async () => {
|
287 |
+
const { workspaceId, roomId } = await producer.createRoom();
|
288 |
+
roomManager.addRoom(workspaceId, roomId);
|
289 |
+
|
290 |
+
await producer.connect(workspaceId, roomId);
|
291 |
+
await consumer.connect(workspaceId, roomId);
|
292 |
+
|
293 |
+
// Give some time for connection to stabilize
|
294 |
+
await sleep(100);
|
295 |
+
|
296 |
+
// Producer sends some state updates
|
297 |
+
await producer.sendStateSync({
|
298 |
+
shoulder: 45.0,
|
299 |
+
elbow: -20.0,
|
300 |
+
wrist: 10.0
|
301 |
+
});
|
302 |
+
|
303 |
+
// Wait for state to propagate
|
304 |
+
await sleep(200);
|
305 |
+
|
306 |
+
// Consumer should be able to get updated state
|
307 |
+
const state = await consumer.getStateSyncAsync();
|
308 |
+
expect(typeof state).toBe("object");
|
309 |
+
|
310 |
+
// State should contain the joints we sent
|
311 |
+
const expectedJoints = new Set(["shoulder", "elbow", "wrist"]);
|
312 |
+
if (Object.keys(state).length > 0) { // Only check if state is not empty
|
313 |
+
expect(new Set(Object.keys(state))).toEqual(expectedJoints);
|
314 |
+
}
|
315 |
+
});
|
316 |
+
});
|
client/js/tests/factory.test.ts
ADDED
@@ -0,0 +1,218 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/**
|
2 |
+
* Tests for Factory Functions - equivalent to Python's test_factory_functions.py
|
3 |
+
*/
|
4 |
+
import { test, expect, describe, beforeEach, afterEach } from "bun:test";
|
5 |
+
import { robotics } from "../src/index";
|
6 |
+
import { TEST_SERVER_URL, TestRoomManager, MessageCollector, sleep } from "./setup";
|
7 |
+
|
8 |
+
const { RoboticsProducer, RoboticsConsumer, createClient, createProducerClient, createConsumerClient } = robotics;
|
9 |
+
|
10 |
+
describe("Factory Functions", () => {
|
11 |
+
let roomManager: TestRoomManager;
|
12 |
+
|
13 |
+
beforeEach(() => {
|
14 |
+
roomManager = new TestRoomManager();
|
15 |
+
});
|
16 |
+
|
17 |
+
afterEach(async () => {
|
18 |
+
// Cleanup will be handled by individual tests
|
19 |
+
});
|
20 |
+
|
21 |
+
test("create client producer", () => {
|
22 |
+
const client = createClient("producer");
|
23 |
+
expect(client).toBeInstanceOf(RoboticsProducer);
|
24 |
+
expect(client.isConnected()).toBe(false);
|
25 |
+
expect(client.getConnectionInfo().base_url).toBe("http://localhost:8000");
|
26 |
+
});
|
27 |
+
|
28 |
+
test("create client consumer", () => {
|
29 |
+
const client = createClient("consumer");
|
30 |
+
expect(client).toBeInstanceOf(RoboticsConsumer);
|
31 |
+
expect(client.isConnected()).toBe(false);
|
32 |
+
expect(client.getConnectionInfo().base_url).toBe("http://localhost:8000");
|
33 |
+
});
|
34 |
+
|
35 |
+
test("create client invalid role", () => {
|
36 |
+
expect(() => createClient("invalid_role" as any)).toThrow("Invalid role");
|
37 |
+
});
|
38 |
+
|
39 |
+
test("create client default url", () => {
|
40 |
+
const client = createClient("producer");
|
41 |
+
expect(client.getConnectionInfo().base_url).toBe("http://localhost:8000");
|
42 |
+
});
|
43 |
+
|
44 |
+
test("create producer client auto room", async () => {
|
45 |
+
const producer = await createProducerClient(TEST_SERVER_URL);
|
46 |
+
|
47 |
+
try {
|
48 |
+
expect(producer).toBeInstanceOf(RoboticsProducer);
|
49 |
+
expect(producer.isConnected()).toBe(true);
|
50 |
+
const info = producer.getConnectionInfo();
|
51 |
+
expect(info.room_id).toBeTruthy();
|
52 |
+
expect(info.workspace_id).toBeTruthy();
|
53 |
+
expect(info.role).toBe("producer");
|
54 |
+
|
55 |
+
// Should be able to send commands immediately
|
56 |
+
await producer.sendStateSync({ test: 123.0 });
|
57 |
+
|
58 |
+
// Track for cleanup
|
59 |
+
roomManager.addRoom(info.workspace_id!, info.room_id!);
|
60 |
+
} finally {
|
61 |
+
await producer.disconnect();
|
62 |
+
await roomManager.cleanup(producer);
|
63 |
+
}
|
64 |
+
});
|
65 |
+
|
66 |
+
test("create producer client specific room", async () => {
|
67 |
+
// First create a room
|
68 |
+
const tempProducer = new RoboticsProducer(TEST_SERVER_URL);
|
69 |
+
const { workspaceId, roomId } = await tempProducer.createRoom();
|
70 |
+
roomManager.addRoom(workspaceId, roomId);
|
71 |
+
|
72 |
+
try {
|
73 |
+
const producer = await createProducerClient(TEST_SERVER_URL, workspaceId, roomId);
|
74 |
+
|
75 |
+
expect(producer).toBeInstanceOf(RoboticsProducer);
|
76 |
+
expect(producer.isConnected()).toBe(true);
|
77 |
+
const info = producer.getConnectionInfo();
|
78 |
+
expect(info.room_id).toBe(roomId);
|
79 |
+
expect(info.workspace_id).toBe(workspaceId);
|
80 |
+
expect(info.role).toBe("producer");
|
81 |
+
|
82 |
+
await producer.disconnect();
|
83 |
+
} finally {
|
84 |
+
await roomManager.cleanup(tempProducer);
|
85 |
+
}
|
86 |
+
});
|
87 |
+
|
88 |
+
test("create consumer client", async () => {
|
89 |
+
// First create a room
|
90 |
+
const tempProducer = new RoboticsProducer(TEST_SERVER_URL);
|
91 |
+
const { workspaceId, roomId } = await tempProducer.createRoom();
|
92 |
+
roomManager.addRoom(workspaceId, roomId);
|
93 |
+
|
94 |
+
try {
|
95 |
+
const consumer = await createConsumerClient(workspaceId, roomId, TEST_SERVER_URL);
|
96 |
+
|
97 |
+
expect(consumer).toBeInstanceOf(RoboticsConsumer);
|
98 |
+
expect(consumer.isConnected()).toBe(true);
|
99 |
+
const info = consumer.getConnectionInfo();
|
100 |
+
expect(info.room_id).toBe(roomId);
|
101 |
+
expect(info.workspace_id).toBe(workspaceId);
|
102 |
+
expect(info.role).toBe("consumer");
|
103 |
+
|
104 |
+
// Should be able to get state immediately
|
105 |
+
const state = await consumer.getStateSyncAsync();
|
106 |
+
expect(typeof state).toBe("object");
|
107 |
+
|
108 |
+
await consumer.disconnect();
|
109 |
+
} finally {
|
110 |
+
await roomManager.cleanup(tempProducer);
|
111 |
+
}
|
112 |
+
});
|
113 |
+
|
114 |
+
test("create producer consumer pair", async () => {
|
115 |
+
const producer = await createProducerClient(TEST_SERVER_URL);
|
116 |
+
const producerInfo = producer.getConnectionInfo();
|
117 |
+
const workspaceId = producerInfo.workspace_id!;
|
118 |
+
const roomId = producerInfo.room_id!;
|
119 |
+
roomManager.addRoom(workspaceId, roomId);
|
120 |
+
|
121 |
+
try {
|
122 |
+
const consumer = await createConsumerClient(workspaceId, roomId, TEST_SERVER_URL);
|
123 |
+
|
124 |
+
// Both should be connected to same room
|
125 |
+
const consumerInfo = consumer.getConnectionInfo();
|
126 |
+
expect(producerInfo.room_id).toBe(consumerInfo.room_id);
|
127 |
+
expect(producerInfo.workspace_id).toBe(consumerInfo.workspace_id);
|
128 |
+
expect(producer.isConnected()).toBe(true);
|
129 |
+
expect(consumer.isConnected()).toBe(true);
|
130 |
+
|
131 |
+
// Test communication
|
132 |
+
const updateCollector = new MessageCollector(1);
|
133 |
+
consumer.onJointUpdate(updateCollector.collect);
|
134 |
+
|
135 |
+
// Give some time for connection to stabilize
|
136 |
+
await sleep(100);
|
137 |
+
|
138 |
+
// Send update from producer
|
139 |
+
await producer.sendStateSync({ test_joint: 42.0 });
|
140 |
+
|
141 |
+
// Wait for message
|
142 |
+
const receivedUpdates = await updateCollector.waitForMessages(2000);
|
143 |
+
|
144 |
+
// Consumer should have received update
|
145 |
+
expect(receivedUpdates.length).toBeGreaterThanOrEqual(1);
|
146 |
+
|
147 |
+
await consumer.disconnect();
|
148 |
+
} finally {
|
149 |
+
await producer.disconnect();
|
150 |
+
await roomManager.cleanup(producer);
|
151 |
+
}
|
152 |
+
});
|
153 |
+
|
154 |
+
test("convenience functions with default url", async () => {
|
155 |
+
const producer = await createProducerClient();
|
156 |
+
const producerInfo = producer.getConnectionInfo();
|
157 |
+
const workspaceId = producerInfo.workspace_id!;
|
158 |
+
const roomId = producerInfo.room_id!;
|
159 |
+
roomManager.addRoom(workspaceId, roomId);
|
160 |
+
|
161 |
+
try {
|
162 |
+
expect(producerInfo.base_url).toBe("http://localhost:8000");
|
163 |
+
expect(producer.isConnected()).toBe(true);
|
164 |
+
|
165 |
+
const consumer = await createConsumerClient(workspaceId, roomId);
|
166 |
+
|
167 |
+
try {
|
168 |
+
const consumerInfo = consumer.getConnectionInfo();
|
169 |
+
expect(consumerInfo.base_url).toBe("http://localhost:8000");
|
170 |
+
expect(consumer.isConnected()).toBe(true);
|
171 |
+
} finally {
|
172 |
+
await consumer.disconnect();
|
173 |
+
}
|
174 |
+
} finally {
|
175 |
+
await producer.disconnect();
|
176 |
+
await roomManager.cleanup(producer);
|
177 |
+
}
|
178 |
+
});
|
179 |
+
|
180 |
+
test("multiple convenience producers", async () => {
|
181 |
+
const producer1 = await createProducerClient(TEST_SERVER_URL);
|
182 |
+
const producer2 = await createProducerClient(TEST_SERVER_URL);
|
183 |
+
|
184 |
+
try {
|
185 |
+
const info1 = producer1.getConnectionInfo();
|
186 |
+
const info2 = producer2.getConnectionInfo();
|
187 |
+
|
188 |
+
// Both should be connected to different rooms
|
189 |
+
expect(info1.room_id).not.toBe(info2.room_id);
|
190 |
+
expect(producer1.isConnected()).toBe(true);
|
191 |
+
expect(producer2.isConnected()).toBe(true);
|
192 |
+
|
193 |
+
// Track for cleanup
|
194 |
+
roomManager.addRoom(info1.workspace_id!, info1.room_id!);
|
195 |
+
roomManager.addRoom(info2.workspace_id!, info2.room_id!);
|
196 |
+
|
197 |
+
// Both should work independently
|
198 |
+
await producer1.sendStateSync({ joint1: 10.0 });
|
199 |
+
await producer2.sendStateSync({ joint2: 20.0 });
|
200 |
+
} finally {
|
201 |
+
await producer1.disconnect();
|
202 |
+
await producer2.disconnect();
|
203 |
+
await roomManager.cleanup(producer1);
|
204 |
+
}
|
205 |
+
});
|
206 |
+
|
207 |
+
test("create consumer nonexistent room", async () => {
|
208 |
+
const fakeWorkspaceId = "00000000-0000-0000-0000-000000000000";
|
209 |
+
const fakeRoomId = "00000000-0000-0000-0000-000000000000";
|
210 |
+
|
211 |
+
const consumer = new RoboticsConsumer(TEST_SERVER_URL);
|
212 |
+
const success = await consumer.connect(fakeWorkspaceId, fakeRoomId);
|
213 |
+
|
214 |
+
// Should fail to connect to non-existent room
|
215 |
+
expect(success).toBe(false);
|
216 |
+
expect(consumer.isConnected()).toBe(false);
|
217 |
+
});
|
218 |
+
});
|
client/js/tests/integration.test.ts
ADDED
@@ -0,0 +1,353 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/**
|
2 |
+
* Integration tests for producer-consumer interactions - equivalent to Python's test_integration.py
|
3 |
+
*/
|
4 |
+
import { test, expect, describe, beforeEach, afterEach } from "bun:test";
|
5 |
+
import { robotics } from "../src/index";
|
6 |
+
import { TEST_SERVER_URL, TestRoomManager, MessageCollector, sleep } from "./setup";
|
7 |
+
|
8 |
+
const { RoboticsProducer, createConsumerClient, createProducerClient } = robotics;
|
9 |
+
|
10 |
+
describe("Integration", () => {
|
11 |
+
let roomManager: TestRoomManager;
|
12 |
+
|
13 |
+
beforeEach(() => {
|
14 |
+
roomManager = new TestRoomManager();
|
15 |
+
});
|
16 |
+
|
17 |
+
afterEach(async () => {
|
18 |
+
// Cleanup handled by individual tests
|
19 |
+
});
|
20 |
+
|
21 |
+
test("full producer consumer workflow", async () => {
|
22 |
+
// Create producer and room
|
23 |
+
const producer = await createProducerClient(TEST_SERVER_URL);
|
24 |
+
const producerInfo = producer.getConnectionInfo();
|
25 |
+
const workspaceId = producerInfo.workspace_id!;
|
26 |
+
const roomId = producerInfo.room_id!;
|
27 |
+
roomManager.addRoom(workspaceId, roomId);
|
28 |
+
|
29 |
+
try {
|
30 |
+
// Create consumer and connect to same room
|
31 |
+
const consumer = await createConsumerClient(workspaceId, roomId, TEST_SERVER_URL);
|
32 |
+
|
33 |
+
try {
|
34 |
+
// Set up consumer to collect messages
|
35 |
+
const stateCollector = new MessageCollector<Record<string, number>>(1);
|
36 |
+
const updateCollector = new MessageCollector(4);
|
37 |
+
const errorCollector = new MessageCollector<string>(1);
|
38 |
+
|
39 |
+
consumer.onStateSync(stateCollector.collect);
|
40 |
+
consumer.onJointUpdate(updateCollector.collect);
|
41 |
+
consumer.onError(errorCollector.collect);
|
42 |
+
|
43 |
+
// Wait for connections to stabilize
|
44 |
+
await sleep(200);
|
45 |
+
|
46 |
+
// Producer sends initial state
|
47 |
+
const initialState = { shoulder: 0.0, elbow: 0.0, wrist: 0.0 };
|
48 |
+
await producer.sendStateSync(initialState);
|
49 |
+
await sleep(100);
|
50 |
+
|
51 |
+
// Producer sends series of joint updates
|
52 |
+
const jointSequences = [
|
53 |
+
[{ name: "shoulder", value: 45.0 }],
|
54 |
+
[{ name: "elbow", value: -30.0 }],
|
55 |
+
[{ name: "wrist", value: 15.0 }],
|
56 |
+
[
|
57 |
+
{ name: "shoulder", value: 90.0 },
|
58 |
+
{ name: "elbow", value: -60.0 }
|
59 |
+
]
|
60 |
+
];
|
61 |
+
|
62 |
+
for (const joints of jointSequences) {
|
63 |
+
await producer.sendJointUpdate(joints);
|
64 |
+
await sleep(100);
|
65 |
+
}
|
66 |
+
|
67 |
+
// Wait for all messages to be received
|
68 |
+
const receivedUpdates = await updateCollector.waitForMessages(3000);
|
69 |
+
|
70 |
+
// Verify consumer received messages
|
71 |
+
expect(receivedUpdates.length).toBeGreaterThanOrEqual(4);
|
72 |
+
|
73 |
+
// Verify final state
|
74 |
+
const finalState = await consumer.getStateSyncAsync();
|
75 |
+
const expectedFinalState = { shoulder: 90.0, elbow: -60.0, wrist: 15.0 };
|
76 |
+
expect(finalState).toEqual(expectedFinalState);
|
77 |
+
|
78 |
+
} finally {
|
79 |
+
await consumer.disconnect();
|
80 |
+
}
|
81 |
+
} finally {
|
82 |
+
await producer.disconnect();
|
83 |
+
await roomManager.cleanup(producer);
|
84 |
+
}
|
85 |
+
});
|
86 |
+
|
87 |
+
test("multiple consumers same room", async () => {
|
88 |
+
const producer = await createProducerClient(TEST_SERVER_URL);
|
89 |
+
const producerInfo = producer.getConnectionInfo();
|
90 |
+
const workspaceId = producerInfo.workspace_id!;
|
91 |
+
const roomId = producerInfo.room_id!;
|
92 |
+
roomManager.addRoom(workspaceId, roomId);
|
93 |
+
|
94 |
+
try {
|
95 |
+
// Create multiple consumers
|
96 |
+
const consumer1 = await createConsumerClient(workspaceId, roomId, TEST_SERVER_URL);
|
97 |
+
const consumer2 = await createConsumerClient(workspaceId, roomId, TEST_SERVER_URL);
|
98 |
+
|
99 |
+
try {
|
100 |
+
// Set up message collection for both consumers
|
101 |
+
const consumer1Collector = new MessageCollector(1);
|
102 |
+
const consumer2Collector = new MessageCollector(1);
|
103 |
+
|
104 |
+
consumer1.onJointUpdate(consumer1Collector.collect);
|
105 |
+
consumer2.onJointUpdate(consumer2Collector.collect);
|
106 |
+
|
107 |
+
// Wait for connections
|
108 |
+
await sleep(200);
|
109 |
+
|
110 |
+
// Producer sends updates
|
111 |
+
const testJoints = [
|
112 |
+
{ name: "joint1", value: 10.0 },
|
113 |
+
{ name: "joint2", value: 20.0 }
|
114 |
+
];
|
115 |
+
await producer.sendJointUpdate(testJoints);
|
116 |
+
|
117 |
+
// Wait for message propagation
|
118 |
+
const consumer1Updates = await consumer1Collector.waitForMessages(2000);
|
119 |
+
const consumer2Updates = await consumer2Collector.waitForMessages(2000);
|
120 |
+
|
121 |
+
// Both consumers should receive the same update
|
122 |
+
expect(consumer1Updates.length).toBeGreaterThanOrEqual(1);
|
123 |
+
expect(consumer2Updates.length).toBeGreaterThanOrEqual(1);
|
124 |
+
|
125 |
+
// Verify both received same data
|
126 |
+
if (consumer1Updates.length > 0 && consumer2Updates.length > 0) {
|
127 |
+
expect(consumer1Updates[consumer1Updates.length - 1]).toEqual(
|
128 |
+
consumer2Updates[consumer2Updates.length - 1]
|
129 |
+
);
|
130 |
+
}
|
131 |
+
|
132 |
+
} finally {
|
133 |
+
await consumer1.disconnect();
|
134 |
+
await consumer2.disconnect();
|
135 |
+
}
|
136 |
+
} finally {
|
137 |
+
await producer.disconnect();
|
138 |
+
await roomManager.cleanup(producer);
|
139 |
+
}
|
140 |
+
});
|
141 |
+
|
142 |
+
test("emergency stop propagation", async () => {
|
143 |
+
const producer = await createProducerClient(TEST_SERVER_URL);
|
144 |
+
const producerInfo = producer.getConnectionInfo();
|
145 |
+
const workspaceId = producerInfo.workspace_id!;
|
146 |
+
const roomId = producerInfo.room_id!;
|
147 |
+
roomManager.addRoom(workspaceId, roomId);
|
148 |
+
|
149 |
+
try {
|
150 |
+
// Create consumers
|
151 |
+
const consumer1 = await createConsumerClient(workspaceId, roomId, TEST_SERVER_URL);
|
152 |
+
const consumer2 = await createConsumerClient(workspaceId, roomId, TEST_SERVER_URL);
|
153 |
+
|
154 |
+
try {
|
155 |
+
// Set up error collection
|
156 |
+
const consumer1ErrorCollector = new MessageCollector<string>(1);
|
157 |
+
const consumer2ErrorCollector = new MessageCollector<string>(1);
|
158 |
+
|
159 |
+
consumer1.onError(consumer1ErrorCollector.collect);
|
160 |
+
consumer2.onError(consumer2ErrorCollector.collect);
|
161 |
+
|
162 |
+
// Wait for connections
|
163 |
+
await sleep(200);
|
164 |
+
|
165 |
+
// Producer sends emergency stop
|
166 |
+
await producer.sendEmergencyStop("Integration test emergency stop");
|
167 |
+
|
168 |
+
// Wait for message propagation
|
169 |
+
const consumer1Errors = await consumer1ErrorCollector.waitForMessages(2000);
|
170 |
+
const consumer2Errors = await consumer2ErrorCollector.waitForMessages(2000);
|
171 |
+
|
172 |
+
// Both consumers should receive emergency stop
|
173 |
+
expect(consumer1Errors.length).toBeGreaterThanOrEqual(1);
|
174 |
+
expect(consumer2Errors.length).toBeGreaterThanOrEqual(1);
|
175 |
+
|
176 |
+
// Verify error messages contain emergency stop info
|
177 |
+
if (consumer1Errors.length > 0) {
|
178 |
+
expect(consumer1Errors[consumer1Errors.length - 1].toLowerCase()).toContain("emergency stop");
|
179 |
+
}
|
180 |
+
if (consumer2Errors.length > 0) {
|
181 |
+
expect(consumer2Errors[consumer2Errors.length - 1].toLowerCase()).toContain("emergency stop");
|
182 |
+
}
|
183 |
+
|
184 |
+
} finally {
|
185 |
+
await consumer1.disconnect();
|
186 |
+
await consumer2.disconnect();
|
187 |
+
}
|
188 |
+
} finally {
|
189 |
+
await producer.disconnect();
|
190 |
+
await roomManager.cleanup(producer);
|
191 |
+
}
|
192 |
+
});
|
193 |
+
|
194 |
+
test("producer reconnection workflow", async () => {
|
195 |
+
// Create room first
|
196 |
+
const tempProducer = new RoboticsProducer(TEST_SERVER_URL);
|
197 |
+
const { workspaceId, roomId } = await tempProducer.createRoom();
|
198 |
+
roomManager.addRoom(workspaceId, roomId);
|
199 |
+
|
200 |
+
try {
|
201 |
+
// Create consumer first
|
202 |
+
const consumer = await createConsumerClient(workspaceId, roomId, TEST_SERVER_URL);
|
203 |
+
|
204 |
+
try {
|
205 |
+
const updateCollector = new MessageCollector(2);
|
206 |
+
consumer.onJointUpdate(updateCollector.collect);
|
207 |
+
|
208 |
+
// Create producer and connect
|
209 |
+
const producer = new RoboticsProducer(TEST_SERVER_URL);
|
210 |
+
await producer.connect(workspaceId, roomId);
|
211 |
+
|
212 |
+
// Send initial update
|
213 |
+
await producer.sendStateSync({ joint1: 10.0 });
|
214 |
+
await sleep(100);
|
215 |
+
|
216 |
+
// Disconnect producer
|
217 |
+
await producer.disconnect();
|
218 |
+
|
219 |
+
// Reconnect producer
|
220 |
+
await producer.connect(workspaceId, roomId);
|
221 |
+
|
222 |
+
// Send another update
|
223 |
+
await producer.sendStateSync({ joint1: 20.0 });
|
224 |
+
await sleep(200);
|
225 |
+
|
226 |
+
// Consumer should have received both updates
|
227 |
+
const receivedUpdates = await updateCollector.waitForMessages(3000);
|
228 |
+
expect(receivedUpdates.length).toBeGreaterThanOrEqual(2);
|
229 |
+
|
230 |
+
await producer.disconnect();
|
231 |
+
|
232 |
+
} finally {
|
233 |
+
await consumer.disconnect();
|
234 |
+
}
|
235 |
+
} finally {
|
236 |
+
await roomManager.cleanup(tempProducer);
|
237 |
+
}
|
238 |
+
});
|
239 |
+
|
240 |
+
test("consumer late join", async () => {
|
241 |
+
const producer = await createProducerClient(TEST_SERVER_URL);
|
242 |
+
const producerInfo = producer.getConnectionInfo();
|
243 |
+
const workspaceId = producerInfo.workspace_id!;
|
244 |
+
const roomId = producerInfo.room_id!;
|
245 |
+
roomManager.addRoom(workspaceId, roomId);
|
246 |
+
|
247 |
+
try {
|
248 |
+
// Producer sends some updates before consumer joins
|
249 |
+
await producer.sendStateSync({ joint1: 10.0, joint2: 20.0 });
|
250 |
+
await sleep(100);
|
251 |
+
|
252 |
+
await producer.sendJointUpdate([{ name: "joint3", value: 30.0 }]);
|
253 |
+
await sleep(100);
|
254 |
+
|
255 |
+
// Now consumer joins
|
256 |
+
const consumer = await createConsumerClient(workspaceId, roomId, TEST_SERVER_URL);
|
257 |
+
|
258 |
+
try {
|
259 |
+
// Consumer should be able to get current state
|
260 |
+
const currentState = await consumer.getStateSyncAsync();
|
261 |
+
|
262 |
+
// Should contain all previously sent updates
|
263 |
+
const expectedState = { joint1: 10.0, joint2: 20.0, joint3: 30.0 };
|
264 |
+
expect(currentState).toEqual(expectedState);
|
265 |
+
|
266 |
+
} finally {
|
267 |
+
await consumer.disconnect();
|
268 |
+
}
|
269 |
+
} finally {
|
270 |
+
await producer.disconnect();
|
271 |
+
await roomManager.cleanup(producer);
|
272 |
+
}
|
273 |
+
});
|
274 |
+
|
275 |
+
test("room cleanup on producer disconnect", async () => {
|
276 |
+
const producer = await createProducerClient(TEST_SERVER_URL);
|
277 |
+
const producerInfo = producer.getConnectionInfo();
|
278 |
+
const workspaceId = producerInfo.workspace_id!;
|
279 |
+
const roomId = producerInfo.room_id!;
|
280 |
+
roomManager.addRoom(workspaceId, roomId);
|
281 |
+
|
282 |
+
try {
|
283 |
+
const consumer = await createConsumerClient(workspaceId, roomId, TEST_SERVER_URL);
|
284 |
+
|
285 |
+
try {
|
286 |
+
// Send some state
|
287 |
+
await producer.sendStateSync({ joint1: 42.0 });
|
288 |
+
await sleep(100);
|
289 |
+
|
290 |
+
// Verify state exists
|
291 |
+
const stateBefore = await consumer.getStateSyncAsync();
|
292 |
+
expect(stateBefore).toEqual({ joint1: 42.0 });
|
293 |
+
|
294 |
+
// Producer disconnects
|
295 |
+
await producer.disconnect();
|
296 |
+
await sleep(100);
|
297 |
+
|
298 |
+
// State should still be accessible to consumer
|
299 |
+
const stateAfter = await consumer.getStateSyncAsync();
|
300 |
+
expect(stateAfter).toEqual({ joint1: 42.0 });
|
301 |
+
|
302 |
+
} finally {
|
303 |
+
await consumer.disconnect();
|
304 |
+
}
|
305 |
+
} finally {
|
306 |
+
// Room cleanup handled by roomManager since producer disconnected
|
307 |
+
}
|
308 |
+
});
|
309 |
+
|
310 |
+
test("high frequency updates", async () => {
|
311 |
+
const producer = await createProducerClient(TEST_SERVER_URL);
|
312 |
+
const producerInfo = producer.getConnectionInfo();
|
313 |
+
const workspaceId = producerInfo.workspace_id!;
|
314 |
+
const roomId = producerInfo.room_id!;
|
315 |
+
roomManager.addRoom(workspaceId, roomId);
|
316 |
+
|
317 |
+
try {
|
318 |
+
const consumer = await createConsumerClient(workspaceId, roomId, TEST_SERVER_URL);
|
319 |
+
|
320 |
+
try {
|
321 |
+
const updateCollector = new MessageCollector(5);
|
322 |
+
consumer.onJointUpdate(updateCollector.collect);
|
323 |
+
|
324 |
+
// Wait for connection
|
325 |
+
await sleep(100);
|
326 |
+
|
327 |
+
// Send rapid updates
|
328 |
+
for (let i = 0; i < 20; i++) {
|
329 |
+
await producer.sendStateSync({ joint1: i, timestamp: i });
|
330 |
+
await sleep(10); // 10ms intervals
|
331 |
+
}
|
332 |
+
|
333 |
+
// Wait for all messages
|
334 |
+
const receivedUpdates = await updateCollector.waitForMessages(5000);
|
335 |
+
|
336 |
+
// Should have received multiple updates
|
337 |
+
// (exact number may vary due to change detection)
|
338 |
+
expect(receivedUpdates.length).toBeGreaterThanOrEqual(5);
|
339 |
+
|
340 |
+
// Final state should reflect last update
|
341 |
+
const finalState = await consumer.getStateSyncAsync();
|
342 |
+
expect(finalState.joint1).toBe(19);
|
343 |
+
expect(finalState.timestamp).toBe(19);
|
344 |
+
|
345 |
+
} finally {
|
346 |
+
await consumer.disconnect();
|
347 |
+
}
|
348 |
+
} finally {
|
349 |
+
await producer.disconnect();
|
350 |
+
await roomManager.cleanup(producer);
|
351 |
+
}
|
352 |
+
});
|
353 |
+
});
|
client/js/tests/producer.test.ts
ADDED
@@ -0,0 +1,217 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/**
|
2 |
+
* Tests for RoboticsProducer - equivalent to Python's test_producer.py
|
3 |
+
*/
|
4 |
+
import { test, expect, describe, beforeEach, afterEach } from "bun:test";
|
5 |
+
import { robotics } from "../src/index";
|
6 |
+
import { TEST_SERVER_URL, TestRoomManager, MessageCollector, sleep, assertIsConnected, assertIsDisconnected } from "./setup";
|
7 |
+
|
8 |
+
const { RoboticsProducer } = robotics;
|
9 |
+
|
10 |
+
describe("RoboticsProducer", () => {
|
11 |
+
let producer: robotics.RoboticsProducer;
|
12 |
+
let roomManager: TestRoomManager;
|
13 |
+
|
14 |
+
beforeEach(() => {
|
15 |
+
producer = new RoboticsProducer(TEST_SERVER_URL);
|
16 |
+
roomManager = new TestRoomManager();
|
17 |
+
});
|
18 |
+
|
19 |
+
afterEach(async () => {
|
20 |
+
if (producer.isConnected()) {
|
21 |
+
await producer.disconnect();
|
22 |
+
}
|
23 |
+
await roomManager.cleanup(producer);
|
24 |
+
});
|
25 |
+
|
26 |
+
test("producer connection", async () => {
|
27 |
+
// Create room first
|
28 |
+
const { workspaceId, roomId } = await producer.createRoom();
|
29 |
+
roomManager.addRoom(workspaceId, roomId);
|
30 |
+
|
31 |
+
expect(producer.isConnected()).toBe(false);
|
32 |
+
|
33 |
+
const success = await producer.connect(workspaceId, roomId);
|
34 |
+
expect(success).toBe(true);
|
35 |
+
assertIsConnected(producer, workspaceId, roomId);
|
36 |
+
|
37 |
+
const info = producer.getConnectionInfo();
|
38 |
+
expect(info.role).toBe("producer");
|
39 |
+
|
40 |
+
await producer.disconnect();
|
41 |
+
assertIsDisconnected(producer);
|
42 |
+
});
|
43 |
+
|
44 |
+
test("producer connection info", async () => {
|
45 |
+
const { workspaceId, roomId } = await producer.createRoom();
|
46 |
+
roomManager.addRoom(workspaceId, roomId);
|
47 |
+
await producer.connect(workspaceId, roomId);
|
48 |
+
|
49 |
+
const info = producer.getConnectionInfo();
|
50 |
+
expect(info.connected).toBe(true);
|
51 |
+
expect(info.room_id).toBe(roomId);
|
52 |
+
expect(info.workspace_id).toBe(workspaceId);
|
53 |
+
expect(info.role).toBe("producer");
|
54 |
+
expect(info.participant_id).toBeTruthy();
|
55 |
+
expect(info.base_url).toBe(TEST_SERVER_URL);
|
56 |
+
});
|
57 |
+
|
58 |
+
test("send joint update", async () => {
|
59 |
+
const { workspaceId, roomId } = await producer.createRoom();
|
60 |
+
roomManager.addRoom(workspaceId, roomId);
|
61 |
+
await producer.connect(workspaceId, roomId);
|
62 |
+
|
63 |
+
const joints = [
|
64 |
+
{ name: "shoulder", value: 45.0 },
|
65 |
+
{ name: "elbow", value: -20.0 },
|
66 |
+
{ name: "wrist", value: 10.0 }
|
67 |
+
];
|
68 |
+
|
69 |
+
// Should not throw an exception
|
70 |
+
await producer.sendJointUpdate(joints);
|
71 |
+
});
|
72 |
+
|
73 |
+
test("send state sync", async () => {
|
74 |
+
const { workspaceId, roomId } = await producer.createRoom();
|
75 |
+
roomManager.addRoom(workspaceId, roomId);
|
76 |
+
await producer.connect(workspaceId, roomId);
|
77 |
+
|
78 |
+
const state = { shoulder: 45.0, elbow: -20.0, wrist: 10.0 };
|
79 |
+
|
80 |
+
// Should not throw an exception
|
81 |
+
await producer.sendStateSync(state);
|
82 |
+
});
|
83 |
+
|
84 |
+
test("send emergency stop", async () => {
|
85 |
+
const { workspaceId, roomId } = await producer.createRoom();
|
86 |
+
roomManager.addRoom(workspaceId, roomId);
|
87 |
+
await producer.connect(workspaceId, roomId);
|
88 |
+
|
89 |
+
// Should not throw an exception
|
90 |
+
await producer.sendEmergencyStop("Test emergency stop");
|
91 |
+
await producer.sendEmergencyStop(); // Default reason
|
92 |
+
});
|
93 |
+
|
94 |
+
test("producer callbacks", async () => {
|
95 |
+
const { workspaceId, roomId } = await producer.createRoom();
|
96 |
+
roomManager.addRoom(workspaceId, roomId);
|
97 |
+
|
98 |
+
let connectedCalled = false;
|
99 |
+
let disconnectedCalled = false;
|
100 |
+
let errorCalled = false;
|
101 |
+
let errorMessage: string | null = null;
|
102 |
+
|
103 |
+
producer.onConnected(() => {
|
104 |
+
connectedCalled = true;
|
105 |
+
});
|
106 |
+
|
107 |
+
producer.onDisconnected(() => {
|
108 |
+
disconnectedCalled = true;
|
109 |
+
});
|
110 |
+
|
111 |
+
producer.onError((error) => {
|
112 |
+
errorCalled = true;
|
113 |
+
errorMessage = error;
|
114 |
+
});
|
115 |
+
|
116 |
+
// Connect and disconnect
|
117 |
+
await producer.connect(workspaceId, roomId);
|
118 |
+
await sleep(100); // Give callbacks time to execute
|
119 |
+
expect(connectedCalled).toBe(true);
|
120 |
+
|
121 |
+
await producer.disconnect();
|
122 |
+
await sleep(100); // Give callbacks time to execute
|
123 |
+
expect(disconnectedCalled).toBe(true);
|
124 |
+
});
|
125 |
+
|
126 |
+
test("send without connection", async () => {
|
127 |
+
expect(producer.isConnected()).toBe(false);
|
128 |
+
|
129 |
+
await expect(producer.sendJointUpdate([{ name: "test", value: 0 }]))
|
130 |
+
.rejects.toThrow("Must be connected");
|
131 |
+
|
132 |
+
await expect(producer.sendStateSync({ test: 0 }))
|
133 |
+
.rejects.toThrow("Must be connected");
|
134 |
+
|
135 |
+
await expect(producer.sendEmergencyStop())
|
136 |
+
.rejects.toThrow("Must be connected");
|
137 |
+
});
|
138 |
+
|
139 |
+
test("multiple connections", async () => {
|
140 |
+
// Connect to first room
|
141 |
+
const { workspaceId: workspaceId1, roomId: roomId1 } = await producer.createRoom();
|
142 |
+
roomManager.addRoom(workspaceId1, roomId1);
|
143 |
+
await producer.connect(workspaceId1, roomId1);
|
144 |
+
|
145 |
+
expect(producer.getConnectionInfo().room_id).toBe(roomId1);
|
146 |
+
expect(producer.getConnectionInfo().workspace_id).toBe(workspaceId1);
|
147 |
+
|
148 |
+
// Create second room
|
149 |
+
const { workspaceId: workspaceId2, roomId: roomId2 } = await producer.createRoom();
|
150 |
+
roomManager.addRoom(workspaceId2, roomId2);
|
151 |
+
|
152 |
+
// Connect to second room (should disconnect from first)
|
153 |
+
await producer.connect(workspaceId2, roomId2);
|
154 |
+
expect(producer.getConnectionInfo().room_id).toBe(roomId2);
|
155 |
+
expect(producer.getConnectionInfo().workspace_id).toBe(workspaceId2);
|
156 |
+
expect(producer.isConnected()).toBe(true);
|
157 |
+
});
|
158 |
+
|
159 |
+
test("duplicate producer connection", async () => {
|
160 |
+
const { workspaceId, roomId } = await producer.createRoom();
|
161 |
+
roomManager.addRoom(workspaceId, roomId);
|
162 |
+
|
163 |
+
const producer2 = new RoboticsProducer(TEST_SERVER_URL);
|
164 |
+
|
165 |
+
try {
|
166 |
+
// First producer connects successfully
|
167 |
+
const success1 = await producer.connect(workspaceId, roomId);
|
168 |
+
expect(success1).toBe(true);
|
169 |
+
|
170 |
+
// Second producer should fail to connect as producer
|
171 |
+
const success2 = await producer2.connect(workspaceId, roomId);
|
172 |
+
expect(success2).toBe(false); // Should fail since room already has producer
|
173 |
+
} finally {
|
174 |
+
if (producer2.isConnected()) {
|
175 |
+
await producer2.disconnect();
|
176 |
+
}
|
177 |
+
}
|
178 |
+
});
|
179 |
+
|
180 |
+
test("custom participant id", async () => {
|
181 |
+
const { workspaceId, roomId } = await producer.createRoom();
|
182 |
+
roomManager.addRoom(workspaceId, roomId);
|
183 |
+
const customId = "custom-producer-123";
|
184 |
+
|
185 |
+
await producer.connect(workspaceId, roomId, customId);
|
186 |
+
|
187 |
+
const info = producer.getConnectionInfo();
|
188 |
+
expect(info.participant_id).toBe(customId);
|
189 |
+
});
|
190 |
+
|
191 |
+
test("large joint update", async () => {
|
192 |
+
const { workspaceId, roomId } = await producer.createRoom();
|
193 |
+
roomManager.addRoom(workspaceId, roomId);
|
194 |
+
await producer.connect(workspaceId, roomId);
|
195 |
+
|
196 |
+
// Create a large joint update
|
197 |
+
const joints = Array.from({ length: 100 }, (_, i) => ({
|
198 |
+
name: `joint_${i}`,
|
199 |
+
value: i
|
200 |
+
}));
|
201 |
+
|
202 |
+
// Should handle large updates without issue
|
203 |
+
await producer.sendJointUpdate(joints);
|
204 |
+
});
|
205 |
+
|
206 |
+
test("rapid updates", async () => {
|
207 |
+
const { workspaceId, roomId } = await producer.createRoom();
|
208 |
+
roomManager.addRoom(workspaceId, roomId);
|
209 |
+
await producer.connect(workspaceId, roomId);
|
210 |
+
|
211 |
+
// Send multiple rapid updates
|
212 |
+
for (let i = 0; i < 10; i++) {
|
213 |
+
await producer.sendStateSync({ joint1: i, joint2: i * 2 });
|
214 |
+
await sleep(10); // Small delay
|
215 |
+
}
|
216 |
+
});
|
217 |
+
});
|
client/js/tests/rest-api.test.ts
ADDED
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/**
|
2 |
+
* Tests for REST API functionality - equivalent to Python's test_rest_api.py
|
3 |
+
*/
|
4 |
+
import { test, expect, describe, beforeEach, afterEach } from "bun:test";
|
5 |
+
import { robotics } from "../src/index";
|
6 |
+
import { TEST_SERVER_URL, TestRoomManager } from "./setup";
|
7 |
+
|
8 |
+
const { RoboticsProducer } = robotics;
|
9 |
+
|
10 |
+
describe("REST API", () => {
|
11 |
+
let producer: robotics.RoboticsProducer;
|
12 |
+
let roomManager: TestRoomManager;
|
13 |
+
|
14 |
+
beforeEach(() => {
|
15 |
+
producer = new RoboticsProducer(TEST_SERVER_URL);
|
16 |
+
roomManager = new TestRoomManager();
|
17 |
+
});
|
18 |
+
|
19 |
+
afterEach(async () => {
|
20 |
+
if (producer.isConnected()) {
|
21 |
+
await producer.disconnect();
|
22 |
+
}
|
23 |
+
await roomManager.cleanup(producer);
|
24 |
+
});
|
25 |
+
|
26 |
+
test("list rooms empty", async () => {
|
27 |
+
// Use a temporary workspace ID
|
28 |
+
const workspaceId = `test-workspace-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
29 |
+
const rooms = await producer.listRooms(workspaceId);
|
30 |
+
expect(Array.isArray(rooms)).toBe(true);
|
31 |
+
});
|
32 |
+
|
33 |
+
test("create room", async () => {
|
34 |
+
const { workspaceId, roomId } = await producer.createRoom();
|
35 |
+
roomManager.addRoom(workspaceId, roomId);
|
36 |
+
|
37 |
+
expect(typeof workspaceId).toBe("string");
|
38 |
+
expect(typeof roomId).toBe("string");
|
39 |
+
expect(workspaceId.length).toBeGreaterThan(0);
|
40 |
+
expect(roomId.length).toBeGreaterThan(0);
|
41 |
+
});
|
42 |
+
|
43 |
+
test("create room with id", async () => {
|
44 |
+
const customRoomId = "test-room-123";
|
45 |
+
const customWorkspaceId = "test-workspace-456";
|
46 |
+
const { workspaceId, roomId } = await producer.createRoom(customWorkspaceId, customRoomId);
|
47 |
+
roomManager.addRoom(workspaceId, roomId);
|
48 |
+
|
49 |
+
expect(workspaceId).toBe(customWorkspaceId);
|
50 |
+
expect(roomId).toBe(customRoomId);
|
51 |
+
});
|
52 |
+
|
53 |
+
test("list rooms with rooms", async () => {
|
54 |
+
// Create a test room
|
55 |
+
const { workspaceId, roomId } = await producer.createRoom();
|
56 |
+
roomManager.addRoom(workspaceId, roomId);
|
57 |
+
|
58 |
+
const rooms = await producer.listRooms(workspaceId);
|
59 |
+
expect(Array.isArray(rooms)).toBe(true);
|
60 |
+
expect(rooms.length).toBeGreaterThanOrEqual(1);
|
61 |
+
|
62 |
+
// Check if our room is in the list
|
63 |
+
const roomIds = rooms.map(room => room.id);
|
64 |
+
expect(roomIds).toContain(roomId);
|
65 |
+
|
66 |
+
// Verify room structure
|
67 |
+
const testRoom = rooms.find(room => room.id === roomId);
|
68 |
+
expect(testRoom).toBeDefined();
|
69 |
+
expect(testRoom!).toHaveProperty("participants");
|
70 |
+
expect(testRoom!).toHaveProperty("joints_count");
|
71 |
+
});
|
72 |
+
|
73 |
+
test("get room info", async () => {
|
74 |
+
const { workspaceId, roomId } = await producer.createRoom();
|
75 |
+
roomManager.addRoom(workspaceId, roomId);
|
76 |
+
|
77 |
+
const roomInfo = await producer.getRoomInfo(workspaceId, roomId);
|
78 |
+
expect(typeof roomInfo).toBe("object");
|
79 |
+
expect(roomInfo.id).toBe(roomId);
|
80 |
+
expect(roomInfo.workspace_id).toBe(workspaceId);
|
81 |
+
expect(roomInfo).toHaveProperty("participants");
|
82 |
+
expect(roomInfo).toHaveProperty("joints_count");
|
83 |
+
expect(roomInfo).toHaveProperty("has_producer");
|
84 |
+
expect(roomInfo).toHaveProperty("active_consumers");
|
85 |
+
});
|
86 |
+
|
87 |
+
test("get room state", async () => {
|
88 |
+
const { workspaceId, roomId } = await producer.createRoom();
|
89 |
+
roomManager.addRoom(workspaceId, roomId);
|
90 |
+
|
91 |
+
const roomState = await producer.getRoomState(workspaceId, roomId);
|
92 |
+
expect(typeof roomState).toBe("object");
|
93 |
+
expect(roomState).toHaveProperty("room_id");
|
94 |
+
expect(roomState).toHaveProperty("workspace_id");
|
95 |
+
expect(roomState).toHaveProperty("joints");
|
96 |
+
expect(roomState).toHaveProperty("participants");
|
97 |
+
expect(roomState).toHaveProperty("timestamp");
|
98 |
+
expect(roomState.room_id).toBe(roomId);
|
99 |
+
expect(roomState.workspace_id).toBe(workspaceId);
|
100 |
+
});
|
101 |
+
|
102 |
+
test("delete room", async () => {
|
103 |
+
const { workspaceId, roomId } = await producer.createRoom();
|
104 |
+
|
105 |
+
// Verify room exists
|
106 |
+
const roomsBefore = await producer.listRooms(workspaceId);
|
107 |
+
const roomIdsBefore = roomsBefore.map(room => room.id);
|
108 |
+
expect(roomIdsBefore).toContain(roomId);
|
109 |
+
|
110 |
+
// Delete room
|
111 |
+
const success = await producer.deleteRoom(workspaceId, roomId);
|
112 |
+
expect(success).toBe(true);
|
113 |
+
|
114 |
+
// Verify room is deleted
|
115 |
+
const roomsAfter = await producer.listRooms(workspaceId);
|
116 |
+
const roomIdsAfter = roomsAfter.map(room => room.id);
|
117 |
+
expect(roomIdsAfter).not.toContain(roomId);
|
118 |
+
});
|
119 |
+
|
120 |
+
test("delete nonexistent room", async () => {
|
121 |
+
const workspaceId = `test-workspace-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
122 |
+
const fakeRoomId = "nonexistent-room-id";
|
123 |
+
const success = await producer.deleteRoom(workspaceId, fakeRoomId);
|
124 |
+
expect(success).toBe(false);
|
125 |
+
});
|
126 |
+
|
127 |
+
test("get room info nonexistent", async () => {
|
128 |
+
const workspaceId = `test-workspace-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
129 |
+
const fakeRoomId = "nonexistent-room-id";
|
130 |
+
|
131 |
+
// This should raise an exception or return error info
|
132 |
+
try {
|
133 |
+
await producer.getRoomInfo(workspaceId, fakeRoomId);
|
134 |
+
// If no exception, this is unexpected for nonexistent room
|
135 |
+
expect(true).toBe(false); // Force test failure
|
136 |
+
} catch (error) {
|
137 |
+
// Expected behavior for nonexistent room
|
138 |
+
expect(error).toBeDefined();
|
139 |
+
}
|
140 |
+
});
|
141 |
+
|
142 |
+
test("get room state nonexistent", async () => {
|
143 |
+
const workspaceId = `test-workspace-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
144 |
+
const fakeRoomId = "nonexistent-room-id";
|
145 |
+
|
146 |
+
// This should raise an exception or return error info
|
147 |
+
try {
|
148 |
+
await producer.getRoomState(workspaceId, fakeRoomId);
|
149 |
+
// If no exception, this is unexpected for nonexistent room
|
150 |
+
expect(true).toBe(false); // Force test failure
|
151 |
+
} catch (error) {
|
152 |
+
// Expected behavior for nonexistent room
|
153 |
+
expect(error).toBeDefined();
|
154 |
+
}
|
155 |
+
});
|
156 |
+
});
|
client/js/tests/setup.ts
ADDED
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/**
|
2 |
+
* Test setup and utilities for RobotHub TransportServer JS Client Tests
|
3 |
+
* Equivalent to Python's conftest.py
|
4 |
+
*/
|
5 |
+
import { expect } from "bun:test";
|
6 |
+
|
7 |
+
// Default server URL for tests
|
8 |
+
export const TEST_SERVER_URL = "http://localhost:8000";
|
9 |
+
|
10 |
+
// Test timeout configuration
|
11 |
+
export const TEST_TIMEOUT = 10000; // 10 seconds
|
12 |
+
|
13 |
+
// Helper to generate test IDs
|
14 |
+
export function generateTestId(prefix: string = "test"): string {
|
15 |
+
return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
16 |
+
}
|
17 |
+
|
18 |
+
// Helper to wait for async operations
|
19 |
+
export function sleep(ms: number): Promise<void> {
|
20 |
+
return new Promise(resolve => setTimeout(resolve, ms));
|
21 |
+
}
|
22 |
+
|
23 |
+
// Test workspace and room management
|
24 |
+
export class TestRoomManager {
|
25 |
+
private createdRooms: Array<{ workspaceId: string; roomId: string }> = [];
|
26 |
+
|
27 |
+
addRoom(workspaceId: string, roomId: string) {
|
28 |
+
this.createdRooms.push({ workspaceId, roomId });
|
29 |
+
}
|
30 |
+
|
31 |
+
async cleanup(client: any) {
|
32 |
+
for (const { workspaceId, roomId } of this.createdRooms) {
|
33 |
+
try {
|
34 |
+
await client.deleteRoom(workspaceId, roomId);
|
35 |
+
} catch (error) {
|
36 |
+
// Ignore cleanup errors
|
37 |
+
console.warn(`Failed to cleanup room ${roomId}:`, error);
|
38 |
+
}
|
39 |
+
}
|
40 |
+
this.createdRooms = [];
|
41 |
+
}
|
42 |
+
}
|
43 |
+
|
44 |
+
// Mock frame source for video tests
|
45 |
+
export async function mockFrameSource(): Promise<ArrayBuffer | null> {
|
46 |
+
// Create a simple test frame (RGBA data)
|
47 |
+
const width = 320;
|
48 |
+
const height = 240;
|
49 |
+
const size = width * height * 4; // RGBA
|
50 |
+
const buffer = new ArrayBuffer(size);
|
51 |
+
const view = new Uint8Array(buffer);
|
52 |
+
|
53 |
+
// Fill with test pattern
|
54 |
+
for (let i = 0; i < size; i += 4) {
|
55 |
+
view[i] = 255; // R
|
56 |
+
view[i + 1] = 0; // G
|
57 |
+
view[i + 2] = 0; // B
|
58 |
+
view[i + 3] = 255; // A
|
59 |
+
}
|
60 |
+
|
61 |
+
return buffer;
|
62 |
+
}
|
63 |
+
|
64 |
+
// Test assertion helpers
|
65 |
+
export function assertIsConnected(client: any, workspaceId: string, roomId: string) {
|
66 |
+
if (!client.isConnected()) {
|
67 |
+
throw new Error("Expected client to be connected");
|
68 |
+
}
|
69 |
+
const info = client.getConnectionInfo();
|
70 |
+
if (!info.connected) {
|
71 |
+
throw new Error("Expected connection info to show connected");
|
72 |
+
}
|
73 |
+
if (info.workspace_id !== workspaceId) {
|
74 |
+
throw new Error(`Expected workspace_id ${workspaceId}, got ${info.workspace_id}`);
|
75 |
+
}
|
76 |
+
if (info.room_id !== roomId) {
|
77 |
+
throw new Error(`Expected room_id ${roomId}, got ${info.room_id}`);
|
78 |
+
}
|
79 |
+
}
|
80 |
+
|
81 |
+
export function assertIsDisconnected(client: any) {
|
82 |
+
if (client.isConnected()) {
|
83 |
+
throw new Error("Expected client to be disconnected");
|
84 |
+
}
|
85 |
+
const info = client.getConnectionInfo();
|
86 |
+
if (info.connected) {
|
87 |
+
throw new Error("Expected connection info to show disconnected");
|
88 |
+
}
|
89 |
+
}
|
90 |
+
|
91 |
+
// Message collection helper for testing callbacks
|
92 |
+
export class MessageCollector<T = any> {
|
93 |
+
private messages: T[] = [];
|
94 |
+
private promise: Promise<T[]> | null = null;
|
95 |
+
private resolve: ((value: T[]) => void) | null = null;
|
96 |
+
|
97 |
+
constructor(private expectedCount: number = 1) {}
|
98 |
+
|
99 |
+
collect = (message: T) => {
|
100 |
+
this.messages.push(message);
|
101 |
+
if (this.messages.length >= this.expectedCount && this.resolve) {
|
102 |
+
this.resolve([...this.messages]);
|
103 |
+
}
|
104 |
+
};
|
105 |
+
|
106 |
+
async waitForMessages(timeoutMs: number = 5000): Promise<T[]> {
|
107 |
+
if (this.messages.length >= this.expectedCount) {
|
108 |
+
return [...this.messages];
|
109 |
+
}
|
110 |
+
|
111 |
+
this.promise = new Promise((resolve, reject) => {
|
112 |
+
this.resolve = resolve;
|
113 |
+
setTimeout(() => {
|
114 |
+
reject(new Error(`Timeout waiting for ${this.expectedCount} messages, got ${this.messages.length}`));
|
115 |
+
}, timeoutMs);
|
116 |
+
});
|
117 |
+
|
118 |
+
return this.promise;
|
119 |
+
}
|
120 |
+
|
121 |
+
getMessages(): T[] {
|
122 |
+
return [...this.messages];
|
123 |
+
}
|
124 |
+
|
125 |
+
clear() {
|
126 |
+
this.messages = [];
|
127 |
+
this.promise = null;
|
128 |
+
this.resolve = null;
|
129 |
+
}
|
130 |
+
}
|
client/js/tests/video-client.test.ts
ADDED
@@ -0,0 +1,254 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/**
|
2 |
+
* Tests for Video Client - equivalent to Python's test_video_client.py
|
3 |
+
*/
|
4 |
+
import { test, expect, describe, beforeEach, afterEach } from "bun:test";
|
5 |
+
import { video } from "../src/index";
|
6 |
+
import { TEST_SERVER_URL, TestRoomManager, mockFrameSource } from "./setup";
|
7 |
+
|
8 |
+
const { VideoProducer, VideoConsumer, createProducerClient, createConsumerClient } = video;
|
9 |
+
|
10 |
+
describe("Video Types", () => {
|
11 |
+
test("resolution creation", () => {
|
12 |
+
const resolution = { width: 1920, height: 1080 };
|
13 |
+
expect(resolution.width).toBe(1920);
|
14 |
+
expect(resolution.height).toBe(1080);
|
15 |
+
});
|
16 |
+
|
17 |
+
test("video config creation", () => {
|
18 |
+
const config = {
|
19 |
+
encoding: "vp8" as video.VideoEncoding,
|
20 |
+
resolution: { width: 640, height: 480 },
|
21 |
+
framerate: 30,
|
22 |
+
bitrate: 1000000
|
23 |
+
};
|
24 |
+
expect(config.encoding).toBe("vp8");
|
25 |
+
expect(config.resolution?.width).toBe(640);
|
26 |
+
expect(config.framerate).toBe(30);
|
27 |
+
});
|
28 |
+
|
29 |
+
test("participant role enum", () => {
|
30 |
+
const producerRole = "producer";
|
31 |
+
const consumerRole = "consumer";
|
32 |
+
expect(producerRole).toBe("producer");
|
33 |
+
expect(consumerRole).toBe("consumer");
|
34 |
+
});
|
35 |
+
});
|
36 |
+
|
37 |
+
describe("Video Core", () => {
|
38 |
+
let roomManager: TestRoomManager;
|
39 |
+
|
40 |
+
beforeEach(() => {
|
41 |
+
roomManager = new TestRoomManager();
|
42 |
+
});
|
43 |
+
|
44 |
+
afterEach(async () => {
|
45 |
+
await roomManager.cleanup(new VideoProducer(TEST_SERVER_URL));
|
46 |
+
});
|
47 |
+
|
48 |
+
test("video producer creation", () => {
|
49 |
+
const producer = new VideoProducer(TEST_SERVER_URL);
|
50 |
+
expect(producer.getConnectionInfo().base_url).toBe(TEST_SERVER_URL);
|
51 |
+
expect(producer.isConnected()).toBe(false);
|
52 |
+
expect(producer.getConnectionInfo().room_id).toBeNull();
|
53 |
+
});
|
54 |
+
|
55 |
+
test("video consumer creation", () => {
|
56 |
+
const consumer = new VideoConsumer(TEST_SERVER_URL);
|
57 |
+
expect(consumer.getConnectionInfo().base_url).toBe(TEST_SERVER_URL);
|
58 |
+
expect(consumer.isConnected()).toBe(false);
|
59 |
+
expect(consumer.getConnectionInfo().room_id).toBeNull();
|
60 |
+
});
|
61 |
+
|
62 |
+
test("producer room creation", async () => {
|
63 |
+
try {
|
64 |
+
const producer = new VideoProducer(TEST_SERVER_URL);
|
65 |
+
const { workspaceId, roomId } = await producer.createRoom();
|
66 |
+
roomManager.addRoom(workspaceId, roomId);
|
67 |
+
|
68 |
+
expect(typeof roomId).toBe("string");
|
69 |
+
expect(roomId.length).toBeGreaterThan(0);
|
70 |
+
console.log(`✅ Created room: ${roomId}`);
|
71 |
+
} catch (error) {
|
72 |
+
console.warn(`⚠️ Server not available: ${error}`);
|
73 |
+
// Skip test if server not available
|
74 |
+
}
|
75 |
+
}, { timeout: 5000 });
|
76 |
+
|
77 |
+
test("consumer list rooms", async () => {
|
78 |
+
try {
|
79 |
+
const consumer = new VideoConsumer(TEST_SERVER_URL);
|
80 |
+
const workspaceId = "test-workspace-id";
|
81 |
+
const rooms = await consumer.listRooms(workspaceId);
|
82 |
+
expect(Array.isArray(rooms)).toBe(true);
|
83 |
+
console.log(`✅ Listed ${rooms.length} rooms`);
|
84 |
+
} catch (error) {
|
85 |
+
console.warn(`⚠️ Server not available: ${error}`);
|
86 |
+
// Skip test if server not available
|
87 |
+
}
|
88 |
+
}, { timeout: 5000 });
|
89 |
+
});
|
90 |
+
|
91 |
+
describe("Video Client Integration", () => {
|
92 |
+
let roomManager: TestRoomManager;
|
93 |
+
|
94 |
+
beforeEach(() => {
|
95 |
+
roomManager = new TestRoomManager();
|
96 |
+
});
|
97 |
+
|
98 |
+
afterEach(async () => {
|
99 |
+
await roomManager.cleanup(new VideoProducer(TEST_SERVER_URL));
|
100 |
+
});
|
101 |
+
|
102 |
+
test("producer consumer setup", () => {
|
103 |
+
// Test producer setup
|
104 |
+
const producer = new VideoProducer(TEST_SERVER_URL);
|
105 |
+
expect(producer.getLocalStream()).toBeNull();
|
106 |
+
|
107 |
+
// Test consumer setup
|
108 |
+
const consumer = new VideoConsumer(TEST_SERVER_URL);
|
109 |
+
expect(consumer.getRemoteStream()).toBeNull();
|
110 |
+
|
111 |
+
console.log("✅ Producer/Consumer setup test passed");
|
112 |
+
});
|
113 |
+
|
114 |
+
test("custom stream setup validation", async () => {
|
115 |
+
const producer = new VideoProducer(TEST_SERVER_URL);
|
116 |
+
|
117 |
+
// This will fail because we're not connected, but it tests the setup
|
118 |
+
try {
|
119 |
+
// Simulate starting a custom stream when not connected
|
120 |
+
await producer.startCamera();
|
121 |
+
expect(true).toBe(false); // Should not reach here
|
122 |
+
} catch (error: any) {
|
123 |
+
expect(error.message).toContain("Must be connected");
|
124 |
+
console.log("✅ Custom stream setup validation passed");
|
125 |
+
}
|
126 |
+
});
|
127 |
+
|
128 |
+
test("factory functions", async () => {
|
129 |
+
// Test that factory functions create the right types
|
130 |
+
// (We can't actually connect without a server)
|
131 |
+
|
132 |
+
try {
|
133 |
+
await createProducerClient(TEST_SERVER_URL);
|
134 |
+
} catch (error) {
|
135 |
+
// Expected to fail without server
|
136 |
+
console.log("✅ Producer factory function exists");
|
137 |
+
}
|
138 |
+
|
139 |
+
try {
|
140 |
+
await createConsumerClient("test-workspace", "test-room", TEST_SERVER_URL);
|
141 |
+
} catch (error) {
|
142 |
+
// Expected to fail without server
|
143 |
+
console.log("✅ Consumer factory function exists");
|
144 |
+
}
|
145 |
+
|
146 |
+
console.log("✅ Factory functions test passed");
|
147 |
+
});
|
148 |
+
|
149 |
+
test("mock frame source", async () => {
|
150 |
+
const frameData = await mockFrameSource();
|
151 |
+
expect(frameData).not.toBeNull();
|
152 |
+
expect(frameData).toBeInstanceOf(ArrayBuffer);
|
153 |
+
|
154 |
+
if (frameData) {
|
155 |
+
expect(frameData.byteLength).toBe(320 * 240 * 4); // RGBA
|
156 |
+
console.log("✅ Mock frame source test passed");
|
157 |
+
}
|
158 |
+
});
|
159 |
+
|
160 |
+
test("video config types", () => {
|
161 |
+
const config: video.VideoConfig = {
|
162 |
+
encoding: "h264",
|
163 |
+
resolution: { width: 1280, height: 720 },
|
164 |
+
framerate: 60,
|
165 |
+
bitrate: 2000000,
|
166 |
+
quality: 90
|
167 |
+
};
|
168 |
+
|
169 |
+
expect(config.encoding).toBe("h264");
|
170 |
+
expect(config.resolution?.width).toBe(1280);
|
171 |
+
expect(config.resolution?.height).toBe(720);
|
172 |
+
expect(config.framerate).toBe(60);
|
173 |
+
expect(config.bitrate).toBe(2000000);
|
174 |
+
expect(config.quality).toBe(90);
|
175 |
+
|
176 |
+
console.log("✅ Video config types test passed");
|
177 |
+
});
|
178 |
+
|
179 |
+
test("recovery config types", () => {
|
180 |
+
const recoveryConfig: video.RecoveryConfig = {
|
181 |
+
frame_timeout_ms: 200,
|
182 |
+
max_frame_reuse_count: 5,
|
183 |
+
recovery_policy: "freeze_last_frame",
|
184 |
+
fallback_policy: "connection_info",
|
185 |
+
show_hold_indicators: true,
|
186 |
+
fade_intensity: 0.8
|
187 |
+
};
|
188 |
+
|
189 |
+
expect(recoveryConfig.frame_timeout_ms).toBe(200);
|
190 |
+
expect(recoveryConfig.recovery_policy).toBe("freeze_last_frame");
|
191 |
+
expect(recoveryConfig.fallback_policy).toBe("connection_info");
|
192 |
+
|
193 |
+
console.log("✅ Recovery config types test passed");
|
194 |
+
});
|
195 |
+
|
196 |
+
test("participant info types", () => {
|
197 |
+
const participantInfo: video.ParticipantInfo = {
|
198 |
+
producer: "producer-123",
|
199 |
+
consumers: ["consumer-1", "consumer-2"],
|
200 |
+
total: 3
|
201 |
+
};
|
202 |
+
|
203 |
+
expect(participantInfo.producer).toBe("producer-123");
|
204 |
+
expect(participantInfo.consumers).toHaveLength(2);
|
205 |
+
expect(participantInfo.total).toBe(3);
|
206 |
+
|
207 |
+
console.log("✅ Participant info types test passed");
|
208 |
+
});
|
209 |
+
|
210 |
+
test("room info types", () => {
|
211 |
+
const roomInfo: video.RoomInfo = {
|
212 |
+
id: "room-123",
|
213 |
+
workspace_id: "workspace-456",
|
214 |
+
participants: {
|
215 |
+
producer: "producer-1",
|
216 |
+
consumers: ["consumer-1"],
|
217 |
+
total: 2
|
218 |
+
},
|
219 |
+
frame_count: 1000,
|
220 |
+
config: {
|
221 |
+
encoding: "vp8",
|
222 |
+
resolution: { width: 640, height: 480 },
|
223 |
+
framerate: 30
|
224 |
+
},
|
225 |
+
has_producer: true,
|
226 |
+
active_consumers: 1
|
227 |
+
};
|
228 |
+
|
229 |
+
expect(roomInfo.id).toBe("room-123");
|
230 |
+
expect(roomInfo.workspace_id).toBe("workspace-456");
|
231 |
+
expect(roomInfo.has_producer).toBe(true);
|
232 |
+
expect(roomInfo.active_consumers).toBe(1);
|
233 |
+
|
234 |
+
console.log("✅ Room info types test passed");
|
235 |
+
});
|
236 |
+
|
237 |
+
test("stream stats types", () => {
|
238 |
+
const streamStats: video.StreamStats = {
|
239 |
+
stream_id: "stream-123",
|
240 |
+
duration_seconds: 120.5,
|
241 |
+
frame_count: 3615,
|
242 |
+
total_bytes: 1048576,
|
243 |
+
average_fps: 30.0,
|
244 |
+
average_bitrate: 1000000
|
245 |
+
};
|
246 |
+
|
247 |
+
expect(streamStats.stream_id).toBe("stream-123");
|
248 |
+
expect(streamStats.duration_seconds).toBe(120.5);
|
249 |
+
expect(streamStats.frame_count).toBe(3615);
|
250 |
+
expect(streamStats.average_fps).toBe(30.0);
|
251 |
+
|
252 |
+
console.log("✅ Stream stats types test passed");
|
253 |
+
});
|
254 |
+
});
|
client/python/.gitignore
ADDED
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Byte-compiled / optimized / DLL files
|
2 |
+
__pycache__/
|
3 |
+
*.py[codz]
|
4 |
+
*$py.class
|
5 |
+
|
6 |
+
# C extensions
|
7 |
+
*.so
|
8 |
+
|
9 |
+
# Distribution / packaging
|
10 |
+
.Python
|
11 |
+
build/
|
12 |
+
develop-eggs/
|
13 |
+
dist/
|
14 |
+
downloads/
|
15 |
+
eggs/
|
16 |
+
.eggs/
|
17 |
+
lib/
|
18 |
+
lib64/
|
19 |
+
parts/
|
20 |
+
sdist/
|
21 |
+
var/
|
22 |
+
wheels/
|
23 |
+
share/python-wheels/
|
24 |
+
*.egg-info/
|
25 |
+
.installed.cfg
|
26 |
+
*.egg
|
27 |
+
MANIFEST
|
28 |
+
|
29 |
+
# PyInstaller
|
30 |
+
# Usually these files are written by a python script from a template
|
31 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
32 |
+
*.manifest
|
33 |
+
*.spec
|
34 |
+
|
35 |
+
# Installer logs
|
36 |
+
pip-log.txt
|
37 |
+
pip-delete-this-directory.txt
|
38 |
+
|
39 |
+
# Unit test / coverage reports
|
40 |
+
htmlcov/
|
41 |
+
.tox/
|
42 |
+
.nox/
|
43 |
+
.coverage
|
44 |
+
.coverage.*
|
45 |
+
.cache
|
46 |
+
nosetests.xml
|
47 |
+
coverage.xml
|
48 |
+
*.cover
|
49 |
+
*.py.cover
|
50 |
+
.hypothesis/
|
51 |
+
.pytest_cache/
|
52 |
+
cover/
|
53 |
+
|
54 |
+
# Translations
|
55 |
+
*.mo
|
56 |
+
*.pot
|
57 |
+
|
58 |
+
# Django stuff:
|
59 |
+
*.log
|
60 |
+
local_settings.py
|
61 |
+
db.sqlite3
|
62 |
+
db.sqlite3-journal
|
63 |
+
|
64 |
+
# Flask stuff:
|
65 |
+
instance/
|
66 |
+
.webassets-cache
|
67 |
+
|
68 |
+
# Scrapy stuff:
|
69 |
+
.scrapy
|
70 |
+
|
71 |
+
# Sphinx documentation
|
72 |
+
docs/_build/
|
73 |
+
|
74 |
+
# PyBuilder
|
75 |
+
.pybuilder/
|
76 |
+
target/
|
77 |
+
|
78 |
+
# Jupyter Notebook
|
79 |
+
.ipynb_checkpoints
|
80 |
+
|
81 |
+
# IPython
|
82 |
+
profile_default/
|
83 |
+
ipython_config.py
|
84 |
+
|
85 |
+
# pyenv
|
86 |
+
# For a library or package, you might want to ignore these files since the code is
|
87 |
+
# intended to run in multiple environments; otherwise, check them in:
|
88 |
+
# .python-version
|
89 |
+
|
90 |
+
# pipenv
|
91 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
92 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
93 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
94 |
+
# install all needed dependencies.
|
95 |
+
#Pipfile.lock
|
96 |
+
|
97 |
+
# UV
|
98 |
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
99 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
100 |
+
# commonly ignored for libraries.
|
101 |
+
#uv.lock
|
102 |
+
|
103 |
+
# poetry
|
104 |
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
105 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
106 |
+
# commonly ignored for libraries.
|
107 |
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
108 |
+
#poetry.lock
|
109 |
+
#poetry.toml
|
110 |
+
|
111 |
+
# pdm
|
112 |
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
113 |
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
114 |
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
115 |
+
#pdm.lock
|
116 |
+
#pdm.toml
|
117 |
+
.pdm-python
|
118 |
+
.pdm-build/
|
119 |
+
|
120 |
+
# pixi
|
121 |
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
122 |
+
#pixi.lock
|
123 |
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
124 |
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
125 |
+
.pixi
|
126 |
+
|
127 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
128 |
+
__pypackages__/
|
129 |
+
|
130 |
+
# Celery stuff
|
131 |
+
celerybeat-schedule
|
132 |
+
celerybeat.pid
|
133 |
+
|
134 |
+
# SageMath parsed files
|
135 |
+
*.sage.py
|
136 |
+
|
137 |
+
# Environments
|
138 |
+
.env
|
139 |
+
.envrc
|
140 |
+
.venv
|
141 |
+
env/
|
142 |
+
venv/
|
143 |
+
ENV/
|
144 |
+
env.bak/
|
145 |
+
venv.bak/
|
146 |
+
|
147 |
+
# Spyder project settings
|
148 |
+
.spyderproject
|
149 |
+
.spyproject
|
150 |
+
|
151 |
+
# Rope project settings
|
152 |
+
.ropeproject
|
153 |
+
|
154 |
+
# mkdocs documentation
|
155 |
+
/site
|
156 |
+
|
157 |
+
# mypy
|
158 |
+
.mypy_cache/
|
159 |
+
.dmypy.json
|
160 |
+
dmypy.json
|
161 |
+
|
162 |
+
# Pyre type checker
|
163 |
+
.pyre/
|
164 |
+
|
165 |
+
# pytype static type analyzer
|
166 |
+
.pytype/
|
167 |
+
|
168 |
+
# Cython debug symbols
|
169 |
+
cython_debug/
|
170 |
+
|
171 |
+
# PyCharm
|
172 |
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
173 |
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
174 |
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
175 |
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
176 |
+
#.idea/
|
177 |
+
|
178 |
+
# Abstra
|
179 |
+
# Abstra is an AI-powered process automation framework.
|
180 |
+
# Ignore directories containing user credentials, local state, and settings.
|
181 |
+
# Learn more at https://abstra.io/docs
|
182 |
+
.abstra/
|
183 |
+
|
184 |
+
# Visual Studio Code
|
185 |
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
186 |
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
187 |
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
188 |
+
# you could uncomment the following to ignore the entire vscode folder
|
189 |
+
# .vscode/
|
190 |
+
|
191 |
+
# Ruff stuff:
|
192 |
+
.ruff_cache/
|
193 |
+
|
194 |
+
# PyPI configuration file
|
195 |
+
.pypirc
|
196 |
+
|
197 |
+
# Cursor
|
198 |
+
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
199 |
+
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
200 |
+
# refer to https://docs.cursor.com/context/ignore-files
|
201 |
+
.cursorignore
|
202 |
+
.cursorindexingignore
|
203 |
+
|
204 |
+
# Marimo
|
205 |
+
marimo/_static/
|
206 |
+
marimo/_lsp/
|
207 |
+
__marimo__/
|
client/python/uv.lock
ADDED
The diff for this file is too large to render.
See raw diff
|
|
server/.gitignore
ADDED
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Byte-compiled / optimized / DLL files
|
2 |
+
__pycache__/
|
3 |
+
*.py[codz]
|
4 |
+
*$py.class
|
5 |
+
|
6 |
+
# C extensions
|
7 |
+
*.so
|
8 |
+
|
9 |
+
# Distribution / packaging
|
10 |
+
.Python
|
11 |
+
build/
|
12 |
+
develop-eggs/
|
13 |
+
dist/
|
14 |
+
downloads/
|
15 |
+
eggs/
|
16 |
+
.eggs/
|
17 |
+
lib/
|
18 |
+
lib64/
|
19 |
+
parts/
|
20 |
+
sdist/
|
21 |
+
var/
|
22 |
+
wheels/
|
23 |
+
share/python-wheels/
|
24 |
+
*.egg-info/
|
25 |
+
.installed.cfg
|
26 |
+
*.egg
|
27 |
+
MANIFEST
|
28 |
+
|
29 |
+
# PyInstaller
|
30 |
+
# Usually these files are written by a python script from a template
|
31 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
32 |
+
*.manifest
|
33 |
+
*.spec
|
34 |
+
|
35 |
+
# Installer logs
|
36 |
+
pip-log.txt
|
37 |
+
pip-delete-this-directory.txt
|
38 |
+
|
39 |
+
# Unit test / coverage reports
|
40 |
+
htmlcov/
|
41 |
+
.tox/
|
42 |
+
.nox/
|
43 |
+
.coverage
|
44 |
+
.coverage.*
|
45 |
+
.cache
|
46 |
+
nosetests.xml
|
47 |
+
coverage.xml
|
48 |
+
*.cover
|
49 |
+
*.py.cover
|
50 |
+
.hypothesis/
|
51 |
+
.pytest_cache/
|
52 |
+
cover/
|
53 |
+
|
54 |
+
# Translations
|
55 |
+
*.mo
|
56 |
+
*.pot
|
57 |
+
|
58 |
+
# Django stuff:
|
59 |
+
*.log
|
60 |
+
local_settings.py
|
61 |
+
db.sqlite3
|
62 |
+
db.sqlite3-journal
|
63 |
+
|
64 |
+
# Flask stuff:
|
65 |
+
instance/
|
66 |
+
.webassets-cache
|
67 |
+
|
68 |
+
# Scrapy stuff:
|
69 |
+
.scrapy
|
70 |
+
|
71 |
+
# Sphinx documentation
|
72 |
+
docs/_build/
|
73 |
+
|
74 |
+
# PyBuilder
|
75 |
+
.pybuilder/
|
76 |
+
target/
|
77 |
+
|
78 |
+
# Jupyter Notebook
|
79 |
+
.ipynb_checkpoints
|
80 |
+
|
81 |
+
# IPython
|
82 |
+
profile_default/
|
83 |
+
ipython_config.py
|
84 |
+
|
85 |
+
# pyenv
|
86 |
+
# For a library or package, you might want to ignore these files since the code is
|
87 |
+
# intended to run in multiple environments; otherwise, check them in:
|
88 |
+
# .python-version
|
89 |
+
|
90 |
+
# pipenv
|
91 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
92 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
93 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
94 |
+
# install all needed dependencies.
|
95 |
+
#Pipfile.lock
|
96 |
+
|
97 |
+
# UV
|
98 |
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
99 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
100 |
+
# commonly ignored for libraries.
|
101 |
+
#uv.lock
|
102 |
+
|
103 |
+
# poetry
|
104 |
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
105 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
106 |
+
# commonly ignored for libraries.
|
107 |
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
108 |
+
#poetry.lock
|
109 |
+
#poetry.toml
|
110 |
+
|
111 |
+
# pdm
|
112 |
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
113 |
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
114 |
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
115 |
+
#pdm.lock
|
116 |
+
#pdm.toml
|
117 |
+
.pdm-python
|
118 |
+
.pdm-build/
|
119 |
+
|
120 |
+
# pixi
|
121 |
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
122 |
+
#pixi.lock
|
123 |
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
124 |
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
125 |
+
.pixi
|
126 |
+
|
127 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
128 |
+
__pypackages__/
|
129 |
+
|
130 |
+
# Celery stuff
|
131 |
+
celerybeat-schedule
|
132 |
+
celerybeat.pid
|
133 |
+
|
134 |
+
# SageMath parsed files
|
135 |
+
*.sage.py
|
136 |
+
|
137 |
+
# Environments
|
138 |
+
.env
|
139 |
+
.envrc
|
140 |
+
.venv
|
141 |
+
env/
|
142 |
+
venv/
|
143 |
+
ENV/
|
144 |
+
env.bak/
|
145 |
+
venv.bak/
|
146 |
+
|
147 |
+
# Spyder project settings
|
148 |
+
.spyderproject
|
149 |
+
.spyproject
|
150 |
+
|
151 |
+
# Rope project settings
|
152 |
+
.ropeproject
|
153 |
+
|
154 |
+
# mkdocs documentation
|
155 |
+
/site
|
156 |
+
|
157 |
+
# mypy
|
158 |
+
.mypy_cache/
|
159 |
+
.dmypy.json
|
160 |
+
dmypy.json
|
161 |
+
|
162 |
+
# Pyre type checker
|
163 |
+
.pyre/
|
164 |
+
|
165 |
+
# pytype static type analyzer
|
166 |
+
.pytype/
|
167 |
+
|
168 |
+
# Cython debug symbols
|
169 |
+
cython_debug/
|
170 |
+
|
171 |
+
# PyCharm
|
172 |
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
173 |
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
174 |
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
175 |
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
176 |
+
#.idea/
|
177 |
+
|
178 |
+
# Abstra
|
179 |
+
# Abstra is an AI-powered process automation framework.
|
180 |
+
# Ignore directories containing user credentials, local state, and settings.
|
181 |
+
# Learn more at https://abstra.io/docs
|
182 |
+
.abstra/
|
183 |
+
|
184 |
+
# Visual Studio Code
|
185 |
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
186 |
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
187 |
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
188 |
+
# you could uncomment the following to ignore the entire vscode folder
|
189 |
+
# .vscode/
|
190 |
+
|
191 |
+
# Ruff stuff:
|
192 |
+
.ruff_cache/
|
193 |
+
|
194 |
+
# PyPI configuration file
|
195 |
+
.pypirc
|
196 |
+
|
197 |
+
# Cursor
|
198 |
+
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
199 |
+
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
200 |
+
# refer to https://docs.cursor.com/context/ignore-files
|
201 |
+
.cursorignore
|
202 |
+
.cursorindexingignore
|
203 |
+
|
204 |
+
# Marimo
|
205 |
+
marimo/_static/
|
206 |
+
marimo/_lsp/
|
207 |
+
__marimo__/
|
server/launch_with_ui.py
CHANGED
@@ -107,7 +107,7 @@ else:
|
|
107 |
|
108 |
if __name__ == "__main__":
|
109 |
port = int(os.getenv("PORT", 7860))
|
110 |
-
host = os.getenv("HOST", "
|
111 |
|
112 |
logger.info("🤖 Starting RobotHub TransportServer Combined Server...")
|
113 |
logger.info(f" - API available at: http://{host}:{port}/api/")
|
|
|
107 |
|
108 |
if __name__ == "__main__":
|
109 |
port = int(os.getenv("PORT", 7860))
|
110 |
+
host = os.getenv("HOST", "localhost")
|
111 |
|
112 |
logger.info("🤖 Starting RobotHub TransportServer Combined Server...")
|
113 |
logger.info(f" - API available at: http://{host}:{port}/api/")
|
server/launch_without_ui.py
ADDED
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import logging
|
2 |
+
import os
|
3 |
+
|
4 |
+
import uvicorn
|
5 |
+
|
6 |
+
# Import the existing API
|
7 |
+
|
8 |
+
logging.basicConfig(
|
9 |
+
level=logging.INFO,
|
10 |
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
11 |
+
handlers=[logging.StreamHandler(), logging.FileHandler("server.log")],
|
12 |
+
)
|
13 |
+
|
14 |
+
logger = logging.getLogger(__name__)
|
15 |
+
|
16 |
+
if __name__ == "__main__":
|
17 |
+
port = int(os.getenv("PORT", 8000))
|
18 |
+
host = os.getenv("HOST", "localhost")
|
19 |
+
|
20 |
+
logger.info("🤖 Starting RobotHub TransportServer API Server (Hot Reload Mode)...")
|
21 |
+
logger.info(f" - API available at: http://{host}:{port}/")
|
22 |
+
logger.info(f" - API docs at: http://{host}:{port}/docs")
|
23 |
+
logger.info(" - Hot reload enabled for development")
|
24 |
+
|
25 |
+
print(
|
26 |
+
f"🤖 Starting RobotHub TransportServer API Server on {host}:{port} (Hot Reload)"
|
27 |
+
)
|
28 |
+
|
29 |
+
uvicorn.run(
|
30 |
+
"src.api:app",
|
31 |
+
host=host,
|
32 |
+
port=port,
|
33 |
+
reload=True,
|
34 |
+
log_level="info",
|
35 |
+
)
|
server/server.log
CHANGED
@@ -2949,3 +2949,53 @@ RuntimeError: WebSocket is not connected. Need to call "accept" first.
|
|
2949 |
2025-06-24 23:21:44,637 - src.robotics.core - INFO - Producer producer_1750800104635_8x1wksqls left room 2d5e7658-50e7-4353-984a-3f242071514c in workspace 66f07ebe-6ed2-474a-83b8-961f30d15134
|
2950 |
2025-06-24 23:21:44,637 - src.robotics.core - INFO - Deleted room 2d5e7658-50e7-4353-984a-3f242071514c from workspace 66f07ebe-6ed2-474a-83b8-961f30d15134
|
2951 |
2025-06-24 23:21:44,643 - src.video.core - INFO - Producer producer_1750800100711_nxb7n7u43 left video room 4995e7c2-a10c-4017-b49d-9bcb60468ad9 in workspace 72b50832-75e5-447c-b8b5-07837b54c0fc
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2949 |
2025-06-24 23:21:44,637 - src.robotics.core - INFO - Producer producer_1750800104635_8x1wksqls left room 2d5e7658-50e7-4353-984a-3f242071514c in workspace 66f07ebe-6ed2-474a-83b8-961f30d15134
|
2950 |
2025-06-24 23:21:44,637 - src.robotics.core - INFO - Deleted room 2d5e7658-50e7-4353-984a-3f242071514c from workspace 66f07ebe-6ed2-474a-83b8-961f30d15134
|
2951 |
2025-06-24 23:21:44,643 - src.video.core - INFO - Producer producer_1750800100711_nxb7n7u43 left video room 4995e7c2-a10c-4017-b49d-9bcb60468ad9 in workspace 72b50832-75e5-447c-b8b5-07837b54c0fc
|
2952 |
+
2025-06-25 01:50:30,088 - __main__ - INFO - 🤖 Starting RobotHub TransportServer API Server (Hot Reload Mode)...
|
2953 |
+
2025-06-25 01:50:30,088 - __main__ - INFO - - API available at: http://localhost:8000/
|
2954 |
+
2025-06-25 01:50:30,088 - __main__ - INFO - - API docs at: http://localhost:8000/docs
|
2955 |
+
2025-06-25 01:50:30,088 - __main__ - INFO - - Hot reload enabled for development
|
2956 |
+
2025-06-25 01:50:57,314 - src.video.core - INFO - Created video room quick-example-1750809057304-front in workspace 095e87f9-6331-4c0a-b13c-ede63936ef78
|
2957 |
+
2025-06-25 01:50:57,319 - src.robotics.core - INFO - Created room quick-example-1750809057304-joint-input in workspace 095e87f9-6331-4c0a-b13c-ede63936ef78
|
2958 |
+
2025-06-25 01:50:57,321 - src.robotics.core - INFO - Created room quick-example-1750809057304-joint-output in workspace 095e87f9-6331-4c0a-b13c-ede63936ef78
|
2959 |
+
2025-06-25 01:51:08,913 - watchfiles.main - INFO - 3 changes detected
|
2960 |
+
2025-06-25 01:51:25,277 - src.video.core - INFO - Created video room quick-example-1750809085270-front in workspace f0e7d351-fb17-4bfa-8285-6d24f6887bbd
|
2961 |
+
2025-06-25 01:51:25,282 - src.robotics.core - INFO - Created room quick-example-1750809085270-joint-input in workspace f0e7d351-fb17-4bfa-8285-6d24f6887bbd
|
2962 |
+
2025-06-25 01:51:25,283 - src.robotics.core - INFO - Created room quick-example-1750809085270-joint-output in workspace f0e7d351-fb17-4bfa-8285-6d24f6887bbd
|
2963 |
+
2025-06-25 01:52:33,163 - src.video.core - INFO - Created video room quick-example-1750809153152-front in workspace 117d5428-eca1-455f-a8c3-1e27c311213e
|
2964 |
+
2025-06-25 01:52:33,168 - src.robotics.core - INFO - Created room quick-example-1750809153152-joint-input in workspace 117d5428-eca1-455f-a8c3-1e27c311213e
|
2965 |
+
2025-06-25 01:52:33,169 - src.robotics.core - INFO - Created room quick-example-1750809153152-joint-output in workspace 117d5428-eca1-455f-a8c3-1e27c311213e
|
2966 |
+
2025-06-25 01:52:42,075 - src.video.core - INFO - Consumer quick-example-1750809153152-front-consumer joined video room quick-example-1750809153152-front in workspace 117d5428-eca1-455f-a8c3-1e27c311213e
|
2967 |
+
2025-06-25 01:52:42,581 - src.robotics.core - INFO - Consumer quick-example-1750809153152-joint-input-consumer joined room quick-example-1750809153152-joint-input in workspace 117d5428-eca1-455f-a8c3-1e27c311213e
|
2968 |
+
2025-06-25 01:52:42,585 - src.robotics.core - INFO - Producer quick-example-1750809153152-joint-output-producer joined room quick-example-1750809153152-joint-output in workspace 117d5428-eca1-455f-a8c3-1e27c311213e
|
2969 |
+
2025-06-25 01:52:44,608 - src.video.core - INFO - Consumer quick-example-1750809153152-front-consumer left video room quick-example-1750809153152-front in workspace 117d5428-eca1-455f-a8c3-1e27c311213e
|
2970 |
+
2025-06-25 01:52:44,611 - src.robotics.core - INFO - Consumer quick-example-1750809153152-joint-input-consumer left room quick-example-1750809153152-joint-input in workspace 117d5428-eca1-455f-a8c3-1e27c311213e
|
2971 |
+
2025-06-25 01:52:44,612 - src.robotics.core - INFO - Producer quick-example-1750809153152-joint-output-producer left room quick-example-1750809153152-joint-output in workspace 117d5428-eca1-455f-a8c3-1e27c311213e
|
2972 |
+
2025-06-25 01:53:27,540 - src.video.core - INFO - Created video room quick-example-1750809207533-front in workspace 05a016a6-04f8-428e-8ca1-ecf1c01f8886
|
2973 |
+
2025-06-25 01:53:27,543 - src.robotics.core - INFO - Created room quick-example-1750809207533-joint-input in workspace 05a016a6-04f8-428e-8ca1-ecf1c01f8886
|
2974 |
+
2025-06-25 01:53:27,545 - src.robotics.core - INFO - Created room quick-example-1750809207533-joint-output in workspace 05a016a6-04f8-428e-8ca1-ecf1c01f8886
|
2975 |
+
2025-06-25 01:53:29,118 - src.video.core - INFO - Consumer quick-example-1750809207533-front-consumer joined video room quick-example-1750809207533-front in workspace 05a016a6-04f8-428e-8ca1-ecf1c01f8886
|
2976 |
+
2025-06-25 01:53:29,627 - src.robotics.core - INFO - Consumer quick-example-1750809207533-joint-input-consumer joined room quick-example-1750809207533-joint-input in workspace 05a016a6-04f8-428e-8ca1-ecf1c01f8886
|
2977 |
+
2025-06-25 01:53:29,631 - src.robotics.core - INFO - Producer quick-example-1750809207533-joint-output-producer joined room quick-example-1750809207533-joint-output in workspace 05a016a6-04f8-428e-8ca1-ecf1c01f8886
|
2978 |
+
2025-06-25 01:53:31,654 - src.video.core - INFO - Consumer quick-example-1750809207533-front-consumer left video room quick-example-1750809207533-front in workspace 05a016a6-04f8-428e-8ca1-ecf1c01f8886
|
2979 |
+
2025-06-25 01:53:31,660 - src.robotics.core - INFO - Consumer quick-example-1750809207533-joint-input-consumer left room quick-example-1750809207533-joint-input in workspace 05a016a6-04f8-428e-8ca1-ecf1c01f8886
|
2980 |
+
2025-06-25 01:53:31,661 - src.robotics.core - INFO - Producer quick-example-1750809207533-joint-output-producer left room quick-example-1750809207533-joint-output in workspace 05a016a6-04f8-428e-8ca1-ecf1c01f8886
|
2981 |
+
2025-06-25 01:53:39,480 - src.video.core - INFO - Created video room example-session-1750809219478-front in workspace 756391fd-94b7-46dd-ad05-09def2b8c5e5
|
2982 |
+
2025-06-25 01:53:39,481 - src.video.core - INFO - Created video room example-session-1750809219478-wrist in workspace 756391fd-94b7-46dd-ad05-09def2b8c5e5
|
2983 |
+
2025-06-25 01:53:39,482 - src.robotics.core - INFO - Created room example-session-1750809219478-joint-input in workspace 756391fd-94b7-46dd-ad05-09def2b8c5e5
|
2984 |
+
2025-06-25 01:53:39,483 - src.robotics.core - INFO - Created room example-session-1750809219478-joint-output in workspace 756391fd-94b7-46dd-ad05-09def2b8c5e5
|
2985 |
+
2025-06-25 01:53:41,045 - src.video.core - INFO - Consumer example-session-1750809219478-front-consumer joined video room example-session-1750809219478-front in workspace 756391fd-94b7-46dd-ad05-09def2b8c5e5
|
2986 |
+
2025-06-25 01:53:41,554 - src.video.core - INFO - Consumer example-session-1750809219478-wrist-consumer joined video room example-session-1750809219478-wrist in workspace 756391fd-94b7-46dd-ad05-09def2b8c5e5
|
2987 |
+
2025-06-25 01:53:42,059 - src.robotics.core - INFO - Consumer example-session-1750809219478-joint-input-consumer joined room example-session-1750809219478-joint-input in workspace 756391fd-94b7-46dd-ad05-09def2b8c5e5
|
2988 |
+
2025-06-25 01:53:42,063 - src.robotics.core - INFO - Producer example-session-1750809219478-joint-output-producer joined room example-session-1750809219478-joint-output in workspace 756391fd-94b7-46dd-ad05-09def2b8c5e5
|
2989 |
+
2025-06-25 01:53:52,092 - src.video.core - INFO - Consumer example-session-1750809219478-front-consumer left video room example-session-1750809219478-front in workspace 756391fd-94b7-46dd-ad05-09def2b8c5e5
|
2990 |
+
2025-06-25 01:53:52,094 - src.video.core - INFO - Consumer example-session-1750809219478-wrist-consumer left video room example-session-1750809219478-wrist in workspace 756391fd-94b7-46dd-ad05-09def2b8c5e5
|
2991 |
+
2025-06-25 01:53:52,094 - src.robotics.core - INFO - Consumer example-session-1750809219478-joint-input-consumer left room example-session-1750809219478-joint-input in workspace 756391fd-94b7-46dd-ad05-09def2b8c5e5
|
2992 |
+
2025-06-25 01:53:52,094 - src.robotics.core - INFO - Producer example-session-1750809219478-joint-output-producer left room example-session-1750809219478-joint-output in workspace 756391fd-94b7-46dd-ad05-09def2b8c5e5
|
2993 |
+
2025-06-25 01:55:11,230 - src.video.core - INFO - Created video room my-robot-01-front in workspace 3a6e0b4d-21e1-4625-9af9-61bc8cabe831
|
2994 |
+
2025-06-25 01:55:11,248 - src.robotics.core - INFO - Created room my-robot-01-joint-input in workspace 3a6e0b4d-21e1-4625-9af9-61bc8cabe831
|
2995 |
+
2025-06-25 01:55:11,254 - src.robotics.core - INFO - Created room my-robot-01-joint-output in workspace 3a6e0b4d-21e1-4625-9af9-61bc8cabe831
|
2996 |
+
2025-06-25 01:55:12,987 - src.video.core - INFO - Consumer my-robot-01-front-consumer joined video room my-robot-01-front in workspace 3a6e0b4d-21e1-4625-9af9-61bc8cabe831
|
2997 |
+
2025-06-25 01:55:13,501 - src.robotics.core - INFO - Consumer my-robot-01-joint-input-consumer joined room my-robot-01-joint-input in workspace 3a6e0b4d-21e1-4625-9af9-61bc8cabe831
|
2998 |
+
2025-06-25 01:55:13,504 - src.robotics.core - INFO - Producer my-robot-01-joint-output-producer joined room my-robot-01-joint-output in workspace 3a6e0b4d-21e1-4625-9af9-61bc8cabe831
|
2999 |
+
2025-06-25 01:55:13,508 - src.video.core - INFO - Consumer my-robot-01-front-consumer left video room my-robot-01-front in workspace 3a6e0b4d-21e1-4625-9af9-61bc8cabe831
|
3000 |
+
2025-06-25 01:55:13,509 - src.robotics.core - INFO - Consumer my-robot-01-joint-input-consumer left room my-robot-01-joint-input in workspace 3a6e0b4d-21e1-4625-9af9-61bc8cabe831
|
3001 |
+
2025-06-25 01:55:13,509 - src.robotics.core - INFO - Producer my-robot-01-joint-output-producer left room my-robot-01-joint-output in workspace 3a6e0b4d-21e1-4625-9af9-61bc8cabe831
|