@testdriverai/runner 7.8.0-canary.13 → 7.8.0-canary.15

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.
@@ -275,6 +275,9 @@ 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
+
278
281
  // Per-command timeout: use message.timeout if provided, else default 120s
279
282
  // Prevents hanging forever if screenshot capture or S3 upload stalls
280
283
  const commandTimeout = (message.timeout && message.timeout > 0)
@@ -415,8 +418,8 @@ class AblyService extends EventEmitter {
415
418
 
416
419
  // Detect discontinuity: channel re-attached but message continuity was lost.
417
420
  // Use historyBeforeSubscribe() on each subscription to recover missed messages.
418
- if (current === 'attached' && stateChange.resumed === false && previous) {
419
- this.emit('log', `Ably channel [session]: DISCONTINUITY (resumed=false)${reasonMsg ? ' — ' + reasonMsg : ''}`);
421
+ if (current === 'attached' && stateChange.resumed === false && previous === 'attached') {
422
+ this.emit('log', `Ably channel [session]: DISCONTINUITY (resumed=false)${reasonMsg ? ' — ' + reasonMsg : ''}`);
420
423
 
421
424
  Sentry.withScope((scope) => {
422
425
  scope.setTag('ably.client', 'runner');
@@ -459,7 +462,7 @@ class AblyService extends EventEmitter {
459
462
  // Signal readiness to SDK — commands sent before this would be lost
460
463
  const readyPayload = {
461
464
  type: 'runner.ready',
462
- os: 'windows',
465
+ os: process.platform === 'win32' ? 'windows' : 'linux',
463
466
  sandboxId: this._sandboxId,
464
467
  runnerVersion: getLocalVersion() || 'unknown',
465
468
  timestamp: Date.now(),
@@ -473,6 +476,39 @@ class AblyService extends EventEmitter {
473
476
  }
474
477
  await this._sessionChannel.publish('control', readyPayload);
475
478
  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
+ }
476
512
  }
477
513
 
478
514
  /**
@@ -617,6 +653,8 @@ class AblyService extends EventEmitter {
617
653
  async close() {
618
654
  this.emit('log', 'Closing Ably service...');
619
655
 
656
+ this._stopReadySignal();
657
+
620
658
  if (this._statsInterval) {
621
659
  clearInterval(this._statsInterval);
622
660
  this._statsInterval = null;
package/lib/automation.js CHANGED
@@ -45,8 +45,10 @@ 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
48
50
  const PY_IMPORT = IS_LINUX
49
- ? "import os; os.environ['DISPLAY'] = ':0'; import pyautogui, sys; pyautogui.FAILSAFE = False; "
51
+ ? "import os; os.environ.setdefault('DISPLAY', ':0'); import pyautogui, sys; pyautogui.FAILSAFE = False; "
50
52
  : 'import pyautogui, sys; pyautogui.FAILSAFE = False; ';
51
53
 
52
54
  /**
@@ -525,11 +527,14 @@ class Automation extends EventEmitter {
525
527
  const timeout = Math.ceil((data.timeout || 300000) / 1000); // ms to seconds
526
528
  const requestId = data.requestId;
527
529
 
528
- // Buffer stdout chunks to ~16KB before emitting over Ably.
530
+ // Buffer stdout chunks to ~32KB before emitting over Ably.
529
531
  // This reduces message count while keeping each message well under
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
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
533
538
  let chunkBuffer = '';
534
539
  const flushChunkBuffer = () => {
535
540
  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.13",
3
+ "version": "7.8.0-canary.15",
4
4
  "description": "TestDriver Runner - Ably-based remote automation agent with Node.js automation",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -0,0 +1,105 @@
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