@testdriverai/runner 7.8.0-test.41 → 7.8.0-test.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +187 -1
- package/lib/ably-service.js +25 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1 +1,187 @@
|
|
|
1
|
-
# runner
|
|
1
|
+
# @testdriverai/runner
|
|
2
|
+
|
|
3
|
+
The TestDriver Runner is a desktop automation agent that connects to the TestDriver API via [Ably](https://ably.com/) realtime messaging. It receives commands from the SDK (click, type, find, screenshot, etc.) and executes them on a desktop environment using PyAutoGUI and Sharp.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
The runner operates in two modes:
|
|
8
|
+
|
|
9
|
+
| Mode | Binary | Use Case |
|
|
10
|
+
|------|--------|----------|
|
|
11
|
+
| **Presence Runner** | `testdriver-runner` | Self-registers with the API, enters Ably presence, and waits for SDK sessions to claim it. Used for persistent/pooled runners. |
|
|
12
|
+
| **Sandbox Agent** | `testdriver-sandbox-agent` | Reads pre-provisioned credentials from a config file or environment variables. Used for ephemeral cloud sandboxes (E2B, AWS EC2). |
|
|
13
|
+
|
|
14
|
+
## Prerequisites
|
|
15
|
+
|
|
16
|
+
### System Requirements
|
|
17
|
+
|
|
18
|
+
- **Node.js** >= 18
|
|
19
|
+
- **Python 3** with `pyautogui` and `Pillow`
|
|
20
|
+
- A desktop environment (physical display, VNC, or virtual framebuffer)
|
|
21
|
+
|
|
22
|
+
### Desktop Environment (Linux)
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Virtual display + desktop
|
|
26
|
+
apt-get install -y xvfb xfce4 xfce4-terminal dbus-x11 wmctrl
|
|
27
|
+
|
|
28
|
+
# VNC access (optional, for debugging)
|
|
29
|
+
apt-get install -y tigervnc-standalone-server novnc websockify
|
|
30
|
+
|
|
31
|
+
# Python automation
|
|
32
|
+
pip3 install pyautogui python-xlib Pillow
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Desktop Environment (Windows)
|
|
36
|
+
|
|
37
|
+
- Standard Windows desktop (RDP or console session)
|
|
38
|
+
- Python 3 with `pyautogui` and `Pillow`:
|
|
39
|
+
```powershell
|
|
40
|
+
pip install pyautogui Pillow
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Chrome
|
|
44
|
+
|
|
45
|
+
Google Chrome or Chrome for Testing must be installed and accessible on `PATH`.
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
### From the TestDriver API (recommended)
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
curl -fSL -H "x-api-key: $TD_API_KEY" \
|
|
53
|
+
https://api.testdriver.ai/api/v7/runner/download \
|
|
54
|
+
-o /tmp/testdriverai-runner.tgz && \
|
|
55
|
+
npm install -g /tmp/testdriverai-runner.tgz && \
|
|
56
|
+
rm /tmp/testdriverai-runner.tgz
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### From source (development)
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
cd runner
|
|
63
|
+
npm install
|
|
64
|
+
npm start
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Quick Start
|
|
68
|
+
|
|
69
|
+
### Presence Runner
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
export TD_API_KEY="your-team-api-key"
|
|
73
|
+
testdriver-runner
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The runner will:
|
|
77
|
+
1. Register with the API at `/api/v7/runner/register`
|
|
78
|
+
2. Receive an Ably token and channel
|
|
79
|
+
3. Enter presence on the runner channel
|
|
80
|
+
4. Wait for SDK sessions to claim it
|
|
81
|
+
|
|
82
|
+
### Sandbox Agent
|
|
83
|
+
|
|
84
|
+
The sandbox agent reads credentials from a JSON config file that the API provisions (via SSM, cloud-init, etc.):
|
|
85
|
+
|
|
86
|
+
**Linux:** `/tmp/testdriver-agent.json`
|
|
87
|
+
**Windows:** `C:\Windows\Temp\testdriver-agent.json`
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"sandboxId": "sb-abc123",
|
|
92
|
+
"ably": {
|
|
93
|
+
"token": "ably-token-string",
|
|
94
|
+
"channel": "testdriver:env:team:sandbox"
|
|
95
|
+
},
|
|
96
|
+
"apiRoot": "https://api.testdriver.ai",
|
|
97
|
+
"apiKey": "team-api-key"
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Start the agent (it will wait up to 5 minutes for the config file to appear):
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
testdriver-sandbox-agent
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Or pass credentials via environment variables instead:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
export SANDBOX_ID="my-sandbox"
|
|
111
|
+
export ABLY_TOKEN='{"token":"..."}'
|
|
112
|
+
export ABLY_CHANNEL="testdriver:env:team:sandbox"
|
|
113
|
+
testdriver-sandbox-agent
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Environment Variables
|
|
117
|
+
|
|
118
|
+
### Required
|
|
119
|
+
|
|
120
|
+
| Variable | Description |
|
|
121
|
+
|----------|-------------|
|
|
122
|
+
| `TD_API_KEY` | Team API key (presence runner mode) |
|
|
123
|
+
|
|
124
|
+
### Optional
|
|
125
|
+
|
|
126
|
+
| Variable | Default | Description |
|
|
127
|
+
|----------|---------|-------------|
|
|
128
|
+
| `TD_API_ROOT` | Per `TD_ENV` | API server URL |
|
|
129
|
+
| `TD_ENV` | `stable` | Environment (`dev` / `test` / `canary` / `stable`) |
|
|
130
|
+
| `TD_RUNNER_ID` | Auto-generated UUID | Fixed runner identifier |
|
|
131
|
+
| `TD_RUNNER_SINGLE` | `false` | Exit after one session |
|
|
132
|
+
| `TD_RUNNER_OS` | Auto-detected | OS capability advertised to API |
|
|
133
|
+
| `TD_VNC_URL` | Auto-detected | Public VNC URL override |
|
|
134
|
+
| `TD_NOVNC_PORT` | Auto-detected | noVNC WebSocket proxy port |
|
|
135
|
+
| `SANDBOX_ID` | Auto-generated | Sandbox identifier (agent mode) |
|
|
136
|
+
| `ABLY_TOKEN` | From config file | Ably auth token JSON (agent mode) |
|
|
137
|
+
| `ABLY_CHANNEL` | From config file | Ably channel name (agent mode) |
|
|
138
|
+
| `CONFIG_PATH` | `/tmp/testdriver-agent.json` | Config file path override (agent mode) |
|
|
139
|
+
| `SCREEN_WIDTH` | `1366` | Virtual display width (Linux) |
|
|
140
|
+
| `SCREEN_HEIGHT` | `768` | Virtual display height (Linux) |
|
|
141
|
+
| `DISPLAY` | `:0` | X11 display (Linux) |
|
|
142
|
+
|
|
143
|
+
## Logs
|
|
144
|
+
|
|
145
|
+
| Platform | Runner Log | Agent Log |
|
|
146
|
+
|----------|-----------|-----------|
|
|
147
|
+
| Linux/macOS | `/tmp/testdriver-runner.log` | `/tmp/testdriver-agent.log` |
|
|
148
|
+
| Windows | `C:\Windows\Temp\testdriver-runner.log` | `C:\Windows\Temp\testdriver-agent.log` |
|
|
149
|
+
|
|
150
|
+
## Desktop Scripts
|
|
151
|
+
|
|
152
|
+
Helper scripts in `scripts-desktop/` for managing the Linux desktop environment:
|
|
153
|
+
|
|
154
|
+
| Script | Purpose |
|
|
155
|
+
|--------|---------|
|
|
156
|
+
| `start-desktop.sh` | Starts Xvfb, XFCE, D-Bus, disables screen blanking |
|
|
157
|
+
| `launch_chrome.sh` | Launches Chrome with standard flags |
|
|
158
|
+
| `launch_chrome_for_testing.sh` | Launches Chrome for Testing with remote debugging (port 9222) |
|
|
159
|
+
| `control_window.sh` | Window management (minimize, restore, focus) via wmctrl |
|
|
160
|
+
|
|
161
|
+
## Deployment
|
|
162
|
+
|
|
163
|
+
### AWS AMI (Packer)
|
|
164
|
+
|
|
165
|
+
See `packer/` for Packer templates that build AMIs with the runner pre-installed. The AMI includes the full desktop stack, Chrome, Python, and the runner.
|
|
166
|
+
|
|
167
|
+
### E2B Sandboxes
|
|
168
|
+
|
|
169
|
+
The E2B template installs the runner in a Dockerfile. See `sdk/setup/e2b/` for the recommended setup.
|
|
170
|
+
|
|
171
|
+
### Docker
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
TD_API_KEY=your-key docker compose up --build
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Updating
|
|
178
|
+
|
|
179
|
+
Re-download and reinstall:
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
curl -fSL -H "x-api-key: $TD_API_KEY" \
|
|
183
|
+
https://api.testdriver.ai/api/v7/runner/download \
|
|
184
|
+
-o /tmp/testdriverai-runner.tgz && \
|
|
185
|
+
npm install -g /tmp/testdriverai-runner.tgz && \
|
|
186
|
+
rm /tmp/testdriverai-runner.tgz
|
|
187
|
+
```
|
package/lib/ably-service.js
CHANGED
|
@@ -266,7 +266,7 @@ class AblyService extends EventEmitter {
|
|
|
266
266
|
});
|
|
267
267
|
|
|
268
268
|
// Subscribe to commands — save subscription ref for historyBeforeSubscribe()
|
|
269
|
-
this.
|
|
269
|
+
this._onCommandMsg = async (msg) => {
|
|
270
270
|
const message = msg.data;
|
|
271
271
|
if (!message) return;
|
|
272
272
|
|
|
@@ -328,7 +328,8 @@ class AblyService extends EventEmitter {
|
|
|
328
328
|
} else {
|
|
329
329
|
await executeCommand();
|
|
330
330
|
}
|
|
331
|
-
}
|
|
331
|
+
};
|
|
332
|
+
this._commandSubscription = await this._sessionChannel.subscribe('command', this._onCommandMsg);
|
|
332
333
|
|
|
333
334
|
// ─── Ably connection state monitoring → Sentry ─────────────────────────
|
|
334
335
|
this._ably.connection.on((stateChange) => {
|
|
@@ -431,7 +432,7 @@ class AblyService extends EventEmitter {
|
|
|
431
432
|
}
|
|
432
433
|
|
|
433
434
|
// Subscribe to control messages — save subscription ref for historyBeforeSubscribe()
|
|
434
|
-
this.
|
|
435
|
+
this._onControlMsg = async (msg) => {
|
|
435
436
|
const message = msg.data;
|
|
436
437
|
if (!message) return;
|
|
437
438
|
|
|
@@ -450,7 +451,8 @@ class AblyService extends EventEmitter {
|
|
|
450
451
|
this._debugMode = !!message.enabled;
|
|
451
452
|
this.emit('log', `Debug mode ${this._debugMode ? 'enabled' : 'disabled'}`);
|
|
452
453
|
}
|
|
453
|
-
}
|
|
454
|
+
};
|
|
455
|
+
this._controlSubscription = await this._sessionChannel.subscribe('control', this._onControlMsg);
|
|
454
456
|
|
|
455
457
|
this.emit('log', 'Listening for commands on Ably');
|
|
456
458
|
|
|
@@ -519,24 +521,37 @@ class AblyService extends EventEmitter {
|
|
|
519
521
|
/**
|
|
520
522
|
* Recover missed messages after a channel discontinuity.
|
|
521
523
|
* Uses historyBeforeSubscribe() on each subscription, which guarantees
|
|
522
|
-
* no gap between historical and live messages.
|
|
524
|
+
* no gap between historical and live messages. Each recovered message
|
|
525
|
+
* is dispatched through the same handler that processes live messages
|
|
526
|
+
* so that missed commands are actually executed.
|
|
523
527
|
*/
|
|
524
528
|
async _recoverFromDiscontinuity() {
|
|
525
529
|
const subs = [
|
|
526
|
-
{ name: 'command', sub: this._commandSubscription },
|
|
527
|
-
{ name: 'control', sub: this._controlSubscription },
|
|
530
|
+
{ name: 'command', sub: this._commandSubscription, handler: this._onCommandMsg },
|
|
531
|
+
{ name: 'control', sub: this._controlSubscription, handler: this._onControlMsg },
|
|
528
532
|
];
|
|
529
|
-
for (const { name, sub } of subs) {
|
|
533
|
+
for (const { name, sub, handler } of subs) {
|
|
530
534
|
if (!sub) continue;
|
|
531
535
|
try {
|
|
532
536
|
this.emit('log', `Discontinuity recovery: fetching historyBeforeSubscribe for ${name}...`);
|
|
533
537
|
let page = await sub.historyBeforeSubscribe({ limit: 100 });
|
|
534
538
|
let recovered = 0;
|
|
535
539
|
while (page) {
|
|
536
|
-
|
|
540
|
+
for (const item of page.items) {
|
|
541
|
+
recovered++;
|
|
542
|
+
try {
|
|
543
|
+
if (handler) {
|
|
544
|
+
this.emit('log', `Replaying recovered ${name} message (requestId=${item.data && item.data.requestId || 'none'})`);
|
|
545
|
+
await handler(item);
|
|
546
|
+
}
|
|
547
|
+
} catch (replayErr) {
|
|
548
|
+
this.emit('log', `Error replaying recovered ${name} message: ${replayErr.message}`);
|
|
549
|
+
Sentry.captureException(replayErr);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
537
552
|
page = page.hasNext() ? await page.next() : null;
|
|
538
553
|
}
|
|
539
|
-
this.emit('log', `Discontinuity recovery:
|
|
554
|
+
this.emit('log', `Discontinuity recovery: replayed ${recovered} ${name} message(s) from gap`);
|
|
540
555
|
} catch (err) {
|
|
541
556
|
this.emit('log', `Discontinuity recovery failed for ${name}: ${err.message}`);
|
|
542
557
|
Sentry.captureException(err);
|