@testdriverai/runner 7.8.0-canary.15 → 7.8.0-test.40

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 CHANGED
@@ -1,187 +1 @@
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
- ```
1
+ # runner
@@ -266,7 +266,7 @@ class AblyService extends EventEmitter {
266
266
  });
267
267
 
268
268
  // Subscribe to commands — save subscription ref for historyBeforeSubscribe()
269
- this._onCommandMsg = async (msg) => {
269
+ this._commandSubscription = await this._sessionChannel.subscribe('command', async (msg) => {
270
270
  const message = msg.data;
271
271
  if (!message) return;
272
272
 
@@ -275,9 +275,6 @@ class AblyService extends EventEmitter {
275
275
 
276
276
  this.emit('log', `Command received: ${type} (requestId=${requestId})`);
277
277
 
278
- // Stop re-publishing runner.ready once we get the first command
279
- this._stopReadySignal();
280
-
281
278
  // Per-command timeout: use message.timeout if provided, else default 120s
282
279
  // Prevents hanging forever if screenshot capture or S3 upload stalls
283
280
  const commandTimeout = (message.timeout && message.timeout > 0)
@@ -331,8 +328,7 @@ class AblyService extends EventEmitter {
331
328
  } else {
332
329
  await executeCommand();
333
330
  }
334
- };
335
- this._commandSubscription = await this._sessionChannel.subscribe('command', this._onCommandMsg);
331
+ });
336
332
 
337
333
  // ─── Ably connection state monitoring → Sentry ─────────────────────────
338
334
  this._ably.connection.on((stateChange) => {
@@ -418,8 +414,8 @@ class AblyService extends EventEmitter {
418
414
 
419
415
  // Detect discontinuity: channel re-attached but message continuity was lost.
420
416
  // Use historyBeforeSubscribe() on each subscription to recover missed messages.
421
- if (current === 'attached' && stateChange.resumed === false && previous === 'attached') {
422
- this.emit('log', `Ably channel [session]: DISCONTINUITY (resumed=false)${reasonMsg ? ' — ' + reasonMsg : ''}`);
417
+ if (current === 'attached' && stateChange.resumed === false && previous) {
418
+ this.emit('log', `Ably channel [session]: DISCONTINUITY (resumed=false)${reasonMsg ? ' — ' + reasonMsg : ''}`);
423
419
 
424
420
  Sentry.withScope((scope) => {
425
421
  scope.setTag('ably.client', 'runner');
@@ -435,7 +431,7 @@ class AblyService extends EventEmitter {
435
431
  }
436
432
 
437
433
  // Subscribe to control messages — save subscription ref for historyBeforeSubscribe()
438
- this._onControlMsg = async (msg) => {
434
+ this._controlSubscription = await this._sessionChannel.subscribe('control', async (msg) => {
439
435
  const message = msg.data;
440
436
  if (!message) return;
441
437
 
@@ -454,15 +450,14 @@ class AblyService extends EventEmitter {
454
450
  this._debugMode = !!message.enabled;
455
451
  this.emit('log', `Debug mode ${this._debugMode ? 'enabled' : 'disabled'}`);
456
452
  }
457
- };
458
- this._controlSubscription = await this._sessionChannel.subscribe('control', this._onControlMsg);
453
+ });
459
454
 
460
455
  this.emit('log', 'Listening for commands on Ably');
461
456
 
462
457
  // Signal readiness to SDK — commands sent before this would be lost
463
458
  const readyPayload = {
464
459
  type: 'runner.ready',
465
- os: process.platform === 'win32' ? 'windows' : 'linux',
460
+ os: 'windows',
466
461
  sandboxId: this._sandboxId,
467
462
  runnerVersion: getLocalVersion() || 'unknown',
468
463
  timestamp: Date.now(),
@@ -476,39 +471,6 @@ class AblyService extends EventEmitter {
476
471
  }
477
472
  await this._sessionChannel.publish('control', readyPayload);
478
473
  this.emit('log', 'Published runner.ready signal');
479
-
480
- // Re-publish runner.ready every 3s for up to 60s.
481
- // The SDK may connect after the first publish (race condition),
482
- // and Ably channel history may not be enabled. Repeating ensures
483
- // the SDK catches at least one live runner.ready message.
484
- this._readyInterval = setInterval(async () => {
485
- try {
486
- readyPayload.timestamp = Date.now();
487
- await this._sessionChannel.publish('control', readyPayload);
488
- this.emit('log', 'Re-published runner.ready signal');
489
- } catch (err) {
490
- this.emit('log', `Failed to re-publish runner.ready: ${err.message}`);
491
- }
492
- }, 3000);
493
-
494
- // Stop after 60s regardless
495
- this._readyTimeout = setTimeout(() => {
496
- this._stopReadySignal();
497
- }, 60000);
498
- }
499
-
500
- /**
501
- * Stop the repeated runner.ready signal (called on first command or after timeout).
502
- */
503
- _stopReadySignal() {
504
- if (this._readyInterval) {
505
- clearInterval(this._readyInterval);
506
- this._readyInterval = null;
507
- }
508
- if (this._readyTimeout) {
509
- clearTimeout(this._readyTimeout);
510
- this._readyTimeout = null;
511
- }
512
474
  }
513
475
 
514
476
  /**
@@ -557,37 +519,24 @@ class AblyService extends EventEmitter {
557
519
  /**
558
520
  * Recover missed messages after a channel discontinuity.
559
521
  * Uses historyBeforeSubscribe() on each subscription, which guarantees
560
- * no gap between historical and live messages. Each recovered message
561
- * is dispatched through the same handler that processes live messages
562
- * so that missed commands are actually executed.
522
+ * no gap between historical and live messages.
563
523
  */
564
524
  async _recoverFromDiscontinuity() {
565
525
  const subs = [
566
- { name: 'command', sub: this._commandSubscription, handler: this._onCommandMsg },
567
- { name: 'control', sub: this._controlSubscription, handler: this._onControlMsg },
526
+ { name: 'command', sub: this._commandSubscription },
527
+ { name: 'control', sub: this._controlSubscription },
568
528
  ];
569
- for (const { name, sub, handler } of subs) {
529
+ for (const { name, sub } of subs) {
570
530
  if (!sub) continue;
571
531
  try {
572
532
  this.emit('log', `Discontinuity recovery: fetching historyBeforeSubscribe for ${name}...`);
573
533
  let page = await sub.historyBeforeSubscribe({ limit: 100 });
574
534
  let recovered = 0;
575
535
  while (page) {
576
- for (const item of page.items) {
577
- recovered++;
578
- try {
579
- if (handler) {
580
- this.emit('log', `Replaying recovered ${name} message (requestId=${item.data && item.data.requestId || 'none'})`);
581
- await handler(item);
582
- }
583
- } catch (replayErr) {
584
- this.emit('log', `Error replaying recovered ${name} message: ${replayErr.message}`);
585
- Sentry.captureException(replayErr);
586
- }
587
- }
536
+ recovered += page.items.length;
588
537
  page = page.hasNext() ? await page.next() : null;
589
538
  }
590
- this.emit('log', `Discontinuity recovery: replayed ${recovered} ${name} message(s) from gap`);
539
+ this.emit('log', `Discontinuity recovery: found ${recovered} ${name} message(s) in gap`);
591
540
  } catch (err) {
592
541
  this.emit('log', `Discontinuity recovery failed for ${name}: ${err.message}`);
593
542
  Sentry.captureException(err);
@@ -653,8 +602,6 @@ class AblyService extends EventEmitter {
653
602
  async close() {
654
603
  this.emit('log', 'Closing Ably service...');
655
604
 
656
- this._stopReadySignal();
657
-
658
605
  if (this._statsInterval) {
659
606
  clearInterval(this._statsInterval);
660
607
  this._statsInterval = null;
package/lib/automation.js CHANGED
@@ -45,10 +45,8 @@ const API_KEY = process.env.TD_API_KEY;
45
45
  // shell injection and escaping issues.
46
46
 
47
47
  const PYTHON = IS_WINDOWS ? 'python' : 'python3';
48
- // On Linux, ensure DISPLAY is set (use env var or fallback to :0)
49
- // The os.environ.get() preserves the parent's DISPLAY setting for E2B's :1 display
50
48
  const PY_IMPORT = IS_LINUX
51
- ? "import os; os.environ.setdefault('DISPLAY', ':0'); import pyautogui, sys; pyautogui.FAILSAFE = False; "
49
+ ? "import os; os.environ['DISPLAY'] = ':0'; import pyautogui, sys; pyautogui.FAILSAFE = False; "
52
50
  : 'import pyautogui, sys; pyautogui.FAILSAFE = False; ';
53
51
 
54
52
  /**
@@ -527,14 +525,11 @@ class Automation extends EventEmitter {
527
525
  const timeout = Math.ceil((data.timeout || 300000) / 1000); // ms to seconds
528
526
  const requestId = data.requestId;
529
527
 
530
- // Buffer stdout chunks to ~32KB before emitting over Ably.
528
+ // Buffer stdout chunks to ~16KB before emitting over Ably.
531
529
  // This reduces message count while keeping each message well under
532
- // Ably's 64KB limit. 32KB leaves headroom for the JSON envelope +
533
- // string escaping while halving the message count vs the previous
534
- // 16KB size, helping avoid Ably's per-channel rate limit on verbose
535
- // commands. The SDK accumulates these chunks and reconstructs the
536
- // full stdout — the final response only carries returncode + stderr.
537
- const CHUNK_FLUSH_SIZE = 32 * 1024; // 32KB
530
+ // Ably's 64KB limit. The SDK accumulates these chunks and reconstructs
531
+ // the full stdout the final response only carries returncode + stderr.
532
+ const CHUNK_FLUSH_SIZE = 16 * 1024; // 16KB
538
533
  let chunkBuffer = '';
539
534
  const flushChunkBuffer = () => {
540
535
  if (chunkBuffer.length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testdriverai/runner",
3
- "version": "7.8.0-canary.15",
3
+ "version": "7.8.0-test.40",
4
4
  "description": "TestDriver Runner - Ably-based remote automation agent with Node.js automation",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -1,105 +0,0 @@
1
- #!/bin/bash
2
- # ─── TestDriver Sandbox Agent Startup ────────────────────────────────────────
3
- # Starts the sandbox-agent.js (Ably-based automation agent) inside the E2B
4
- # sandbox. This script is called by the API after writing the config file
5
- # to /tmp/testdriver-agent.json.
6
- #
7
- # This matches the Windows runner pattern: the agent runs locally on the
8
- # sandbox and executes commands via pyautogui (instead of @e2b/desktop RPC).
9
- #
10
- # Usage: bash /opt/testdriver-runner/scripts-desktop/start-agent.sh [&]
11
- #
12
- # Prerequisites:
13
- # - Desktop environment running (start-desktop.sh completed)
14
- # - Config file at /tmp/testdriver-agent.json with Ably credentials
15
- # - Node.js installed
16
- # - Runner installed at /opt/testdriver-runner
17
-
18
- set -e
19
-
20
- export DISPLAY="${DISPLAY:-:0}"
21
- export XAUTHORITY="${XAUTHORITY:-${HOME}/.Xauthority}"
22
-
23
- RUNNER_DIR="/opt/testdriver-runner"
24
- CONFIG_PATH="/tmp/testdriver-agent.json"
25
- LOG_FILE="/tmp/sandbox-agent.log"
26
- PID_FILE="/tmp/sandbox-agent.pid"
27
-
28
- log() {
29
- echo "[$(date -Iseconds)] [start-agent] $1" | tee -a "$LOG_FILE"
30
- }
31
-
32
- # ─── Check if already running ─────────────────────────────────────────────────
33
- if [ -f "$PID_FILE" ]; then
34
- existing_pid=$(cat "$PID_FILE")
35
- if kill -0 "$existing_pid" 2>/dev/null; then
36
- log "Agent already running (PID: $existing_pid), exiting"
37
- exit 0
38
- else
39
- log "Stale PID file found, removing"
40
- rm -f "$PID_FILE"
41
- fi
42
- fi
43
-
44
- # ─── Verify prerequisites ─────────────────────────────────────────────────────
45
- if [ ! -d "$RUNNER_DIR" ]; then
46
- log "ERROR: Runner not found at $RUNNER_DIR"
47
- exit 1
48
- fi
49
-
50
- if [ ! -f "$RUNNER_DIR/sandbox-agent.js" ]; then
51
- log "ERROR: sandbox-agent.js not found in $RUNNER_DIR"
52
- exit 1
53
- fi
54
-
55
- if ! command -v node &> /dev/null; then
56
- log "ERROR: Node.js not installed"
57
- exit 1
58
- fi
59
-
60
- # ─── Wait for config file (with timeout) ─────────────────────────────────────
61
- # The API writes the config file before calling this script, but we add a
62
- # brief wait just in case there's any race condition.
63
- WAIT_TIMEOUT=30
64
- WAIT_INTERVAL=1
65
- elapsed=0
66
-
67
- log "Waiting for config file: $CONFIG_PATH"
68
- while [ ! -f "$CONFIG_PATH" ] && [ $elapsed -lt $WAIT_TIMEOUT ]; do
69
- sleep $WAIT_INTERVAL
70
- elapsed=$((elapsed + WAIT_INTERVAL))
71
- done
72
-
73
- if [ ! -f "$CONFIG_PATH" ]; then
74
- log "ERROR: Config file not found after ${WAIT_TIMEOUT}s: $CONFIG_PATH"
75
- exit 1
76
- fi
77
-
78
- log "Config file found"
79
-
80
- # ─── Start the agent ──────────────────────────────────────────────────────────
81
- log "Starting sandbox-agent.js..."
82
- log "DISPLAY=$DISPLAY, RUNNER_DIR=$RUNNER_DIR"
83
-
84
- # Run in background, redirect output to log file
85
- cd "$RUNNER_DIR"
86
- nohup node sandbox-agent.js >> "$LOG_FILE" 2>&1 &
87
- AGENT_PID=$!
88
-
89
- # Write PID file for process management
90
- echo "$AGENT_PID" > "$PID_FILE"
91
-
92
- log "Agent started (PID: $AGENT_PID)"
93
- log "Log file: $LOG_FILE"
94
-
95
- # Brief pause to catch any immediate startup errors
96
- sleep 2
97
-
98
- if kill -0 "$AGENT_PID" 2>/dev/null; then
99
- log "Agent running successfully"
100
- exit 0
101
- else
102
- log "ERROR: Agent exited unexpectedly. Check $LOG_FILE for details"
103
- tail -20 "$LOG_FILE" | while read line; do log " $line"; done
104
- exit 1
105
- fi