testdriverai 7.2.3 → 7.2.10

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.
Files changed (142) hide show
  1. package/.github/workflows/publish.yaml +15 -7
  2. package/.github/workflows/testdriver.yml +163 -0
  3. package/.testdriver/last-sandbox +7 -0
  4. package/agent/events.js +1 -0
  5. package/agent/index.js +99 -163
  6. package/agent/lib/sandbox.js +11 -1
  7. package/agents.md +393 -0
  8. package/bin/testdriverai.js +8 -0
  9. package/debug/01-table-initial.png +0 -0
  10. package/debug/02-after-ai-explore.png +0 -0
  11. package/debug/02-after-scroll.png +0 -0
  12. package/debugger/index.html +37 -0
  13. package/docs/docs.json +93 -125
  14. package/docs/v7/_drafts/architecture.mdx +1 -26
  15. package/docs/v7/_drafts/caching.mdx +2 -2
  16. package/docs/v7/{getting-started → _drafts}/installation.mdx +0 -66
  17. package/docs/v7/{features/coverage.mdx → _drafts/powerful.mdx} +1 -90
  18. package/docs/v7/_drafts/quick-start-test-recording.mdx +0 -1
  19. package/docs/v7/{features → _drafts}/scalable.mdx +126 -4
  20. package/docs/v7/_drafts/screenshot.mdx +155 -0
  21. package/docs/v7/_drafts/test-recording.mdx +0 -6
  22. package/docs/v7/_drafts/writing-tests.mdx +25 -0
  23. package/docs/v7/{api/act.mdx → ai.mdx} +28 -27
  24. package/docs/v7/{api/assert.mdx → assert.mdx} +3 -3
  25. package/docs/v7/aws-setup.mdx +338 -0
  26. package/docs/v7/caching.mdx +128 -0
  27. package/docs/v7/ci-cd.mdx +605 -0
  28. package/docs/v7/{api/click.mdx → click.mdx} +4 -4
  29. package/docs/v7/cloud.mdx +120 -0
  30. package/docs/v7/customizing-devices.mdx +129 -0
  31. package/docs/v7/{api/doubleClick.mdx → double-click.mdx} +5 -5
  32. package/docs/v7/enterprise.mdx +135 -0
  33. package/docs/v7/examples.mdx +5 -0
  34. package/docs/v7/{api/exec.mdx → exec.mdx} +3 -3
  35. package/docs/v7/{api/find.mdx → find.mdx} +17 -21
  36. package/docs/v7/{api/focusApplication.mdx → focus-application.mdx} +3 -3
  37. package/docs/v7/generating-tests.mdx +32 -0
  38. package/docs/v7/{api/hover.mdx → hover.mdx} +3 -3
  39. package/docs/v7/locating-elements.mdx +71 -0
  40. package/docs/v7/making-assertions.mdx +32 -0
  41. package/docs/v7/{api/mouseDown.mdx → mouse-down.mdx} +7 -7
  42. package/docs/v7/{api/mouseUp.mdx → mouse-up.mdx} +8 -8
  43. package/docs/v7/performing-actions.mdx +51 -0
  44. package/docs/v7/{api/pressKeys.mdx → press-keys.mdx} +3 -3
  45. package/docs/v7/quickstart.mdx +162 -0
  46. package/docs/v7/reusable-code.mdx +240 -0
  47. package/docs/v7/{api/rightClick.mdx → right-click.mdx} +5 -5
  48. package/docs/v7/running-tests.mdx +181 -0
  49. package/docs/v7/{api/scroll.mdx → scroll.mdx} +3 -3
  50. package/docs/v7/secrets.mdx +115 -0
  51. package/docs/v7/self-hosted.mdx +66 -0
  52. package/docs/v7/{api/type.mdx → type.mdx} +3 -3
  53. package/docs/v7/variables.mdx +111 -0
  54. package/docs/v7/waiting-for-elements.mdx +66 -0
  55. package/docs/v7/what-is-testdriver.mdx +54 -0
  56. package/interfaces/cli/commands/init.js +33 -19
  57. package/interfaces/cli/lib/base.js +24 -0
  58. package/interfaces/cli.js +8 -1
  59. package/interfaces/logger.js +8 -3
  60. package/interfaces/vitest-plugin.mjs +16 -71
  61. package/lib/sentry.js +343 -0
  62. package/lib/vitest/hooks.mjs +81 -81
  63. package/package.json +4 -3
  64. package/sdk-log-formatter.js +41 -0
  65. package/sdk.d.ts +22 -9
  66. package/sdk.js +344 -100
  67. package/test/manual/reconnect-provision.test.mjs +49 -0
  68. package/test/manual/reconnect-signin.test.mjs +41 -0
  69. package/test/testdriver/act.test.mjs +30 -0
  70. package/test/testdriver/ai.test.mjs +30 -0
  71. package/test/testdriver/assert.test.mjs +1 -1
  72. package/test/testdriver/hover-text.test.mjs +1 -1
  73. package/test/testdriver/setup/testHelpers.mjs +8 -119
  74. package/test/testdriver/windows-installer.test.mjs +61 -0
  75. package/tests/example.test.js +33 -0
  76. package/tests/login.js +28 -0
  77. package/tests/table-sort-enrollments.test.mjs +72 -0
  78. package/tests/table-sort-experiment.test.mjs +42 -0
  79. package/tests/table-sort-setup.test.mjs +59 -0
  80. package/vitest.config.mjs +3 -1
  81. package/agent/lib/cache.js +0 -142
  82. package/docs/v7/api/assertions.mdx +0 -403
  83. package/docs/v7/features/ai-native.mdx +0 -413
  84. package/docs/v7/features/application-logs.mdx +0 -353
  85. package/docs/v7/features/browser-logs.mdx +0 -414
  86. package/docs/v7/features/cache-management.mdx +0 -402
  87. package/docs/v7/features/continuous-testing.mdx +0 -346
  88. package/docs/v7/features/data-driven-testing.mdx +0 -441
  89. package/docs/v7/features/easy-to-write.mdx +0 -280
  90. package/docs/v7/features/enterprise.mdx +0 -656
  91. package/docs/v7/features/fast.mdx +0 -406
  92. package/docs/v7/features/managed-sandboxes.mdx +0 -384
  93. package/docs/v7/features/network-monitoring.mdx +0 -568
  94. package/docs/v7/features/parallel-execution.mdx +0 -381
  95. package/docs/v7/features/powerful.mdx +0 -531
  96. package/docs/v7/features/sandbox-customization.mdx +0 -229
  97. package/docs/v7/features/stable.mdx +0 -473
  98. package/docs/v7/features/system-performance.mdx +0 -616
  99. package/docs/v7/features/test-analytics.mdx +0 -373
  100. package/docs/v7/features/test-cases.mdx +0 -393
  101. package/docs/v7/features/test-replays.mdx +0 -408
  102. package/docs/v7/features/test-reports.mdx +0 -308
  103. package/docs/v7/getting-started/debugging-tests.mdx +0 -382
  104. package/docs/v7/getting-started/quickstart.mdx +0 -90
  105. package/docs/v7/getting-started/running-tests.mdx +0 -173
  106. package/docs/v7/getting-started/setting-up-in-ci.mdx +0 -612
  107. package/docs/v7/getting-started/writing-tests.mdx +0 -534
  108. package/docs/v7/overview/what-is-testdriver.mdx +0 -386
  109. package/docs/v7/presets/chrome-extension.mdx +0 -248
  110. package/docs/v7/presets/chrome.mdx +0 -300
  111. package/docs/v7/presets/electron.mdx +0 -460
  112. package/docs/v7/presets/vscode.mdx +0 -417
  113. package/docs/v7/presets/webapp.mdx +0 -393
  114. /package/docs/v7/{commands → _drafts/commands}/assert.mdx +0 -0
  115. /package/docs/v7/{commands → _drafts/commands}/exec.mdx +0 -0
  116. /package/docs/v7/{commands → _drafts/commands}/focus-application.mdx +0 -0
  117. /package/docs/v7/{commands → _drafts/commands}/hover-image.mdx +0 -0
  118. /package/docs/v7/{commands → _drafts/commands}/hover-text.mdx +0 -0
  119. /package/docs/v7/{commands → _drafts/commands}/if.mdx +0 -0
  120. /package/docs/v7/{commands → _drafts/commands}/match-image.mdx +0 -0
  121. /package/docs/v7/{commands → _drafts/commands}/press-keys.mdx +0 -0
  122. /package/docs/v7/{commands → _drafts/commands}/remember.mdx +0 -0
  123. /package/docs/v7/{commands → _drafts/commands}/run.mdx +0 -0
  124. /package/docs/v7/{commands → _drafts/commands}/scroll-until-image.mdx +0 -0
  125. /package/docs/v7/{commands → _drafts/commands}/scroll-until-text.mdx +0 -0
  126. /package/docs/v7/{commands → _drafts/commands}/scroll.mdx +0 -0
  127. /package/docs/v7/{commands → _drafts/commands}/type.mdx +0 -0
  128. /package/docs/v7/{commands → _drafts/commands}/wait-for-image.mdx +0 -0
  129. /package/docs/v7/{commands → _drafts/commands}/wait-for-text.mdx +0 -0
  130. /package/docs/v7/{commands → _drafts/commands}/wait.mdx +0 -0
  131. /package/docs/v7/{getting-started → _drafts}/configuration.mdx +0 -0
  132. /package/docs/v7/{features → _drafts}/observable.mdx +0 -0
  133. /package/docs/v7/{platforms → _drafts/platforms}/linux.mdx +0 -0
  134. /package/docs/v7/{platforms → _drafts/platforms}/macos.mdx +0 -0
  135. /package/docs/v7/{platforms → _drafts/platforms}/windows.mdx +0 -0
  136. /package/docs/v7/{playwright.mdx → _drafts/playwright.mdx} +0 -0
  137. /package/docs/v7/{overview → _drafts}/readme.mdx +0 -0
  138. /package/docs/v7/{features → _drafts}/reports.mdx +0 -0
  139. /package/docs/v7/{api/client.mdx → client.mdx} +0 -0
  140. /package/docs/v7/{api/dashcam.mdx → dashcam.mdx} +0 -0
  141. /package/docs/v7/{api/elements.mdx → elements.mdx} +0 -0
  142. /package/docs/v7/{api/sandbox.mdx → sandbox.mdx} +0 -0
@@ -1,12 +1,13 @@
1
- name: Publish Beta
1
+ name: Publish
2
2
  permissions:
3
3
  contents: write
4
+ id-token: write # Required for OIDC
4
5
  on:
5
6
  push:
6
7
  branches: [ main ]
7
8
 
8
9
  jobs:
9
- publish-beta:
10
+ publish:
10
11
  runs-on: ubuntu-latest
11
12
 
12
13
  steps:
@@ -29,16 +30,23 @@ jobs:
29
30
  - name: Install dependencies
30
31
  run: npm ci
31
32
 
32
- - name: Bump version (prerelease beta)
33
- run: npm version prerelease --preid=beta --no-git-tag-version
33
+ - name: Bump version (patch)
34
+ run: npm version patch --no-git-tag-version
34
35
 
35
36
  - name: Commit and push version bump
36
37
  run: |
37
38
  git add package.json package-lock.json
38
- git commit -m "chore: bump beta version to $(node -p "require('./package.json').version")"
39
+ git commit -m "chore: bump version to $(node -p "require('./package.json').version")"
39
40
  git push
40
41
 
41
- - name: Publish to npm under beta tag
42
- run: npm publish --tag beta
42
+ - name: Debug NPM Token
43
+ run: |
44
+ echo "NPM_TOKEN is set: ${{ secrets.NPM_TOKEN != '' }}"
45
+ echo "NPM_TOKEN first 4 chars: ${NPM_TOKEN:0:4}..."
46
+ env:
47
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
48
+
49
+ - name: Publish to npm
50
+ run: npm publish --tag beta
43
51
  env:
44
52
  NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -0,0 +1,163 @@
1
+ name: TestDriver.ai Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [ main, master ]
6
+ pull_request:
7
+ branches: [ main, master ]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - name: Setup Node.js
17
+ uses: actions/setup-node@v4
18
+ with:
19
+ node-version: '20'
20
+ cache: 'npm'
21
+
22
+ - name: Install dependencies
23
+ run: npm ci
24
+
25
+ - name: Run TestDriver.ai tests
26
+ env:
27
+ TD_API_KEY: ${{ secrets.TD_API_KEY }}
28
+ run: npx vitest run
29
+
30
+ - name: Upload test results
31
+ if: always()
32
+ uses: actions/upload-artifact@v4
33
+ with:
34
+ name: test-results
35
+ path: test-results/
36
+ retention-days: 30
37
+
38
+ # Init command test - only runs on PRs, not on main/master
39
+ - name: Create test directory for init
40
+ if: github.event_name == 'pull_request'
41
+ run: |
42
+ mkdir -p /tmp/test-init-project
43
+ cd /tmp/test-init-project
44
+
45
+ - name: Run init command (skip prompts)
46
+ if: github.event_name == 'pull_request'
47
+ working-directory: /tmp/test-init-project
48
+ run: |
49
+ # Create .env with API key first to skip the prompt
50
+ echo "TD_API_KEY=${{ secrets.TD_API_KEY }}" > .env
51
+
52
+ # Run init command using the CLI from the repo
53
+ node ${{ github.workspace }}/bin/testdriverai.js init
54
+ env:
55
+ TD_API_KEY: ${{ secrets.TD_API_KEY }}
56
+
57
+ - name: Verify project structure
58
+ if: github.event_name == 'pull_request'
59
+ working-directory: /tmp/test-init-project
60
+ run: |
61
+ echo "Checking generated files..."
62
+
63
+ # Check for package.json
64
+ if [ ! -f "package.json" ]; then
65
+ echo "❌ package.json not found"
66
+ exit 1
67
+ fi
68
+ echo "✓ package.json exists"
69
+
70
+ # Check for vitest config
71
+ if [ ! -f "vitest.config.js" ]; then
72
+ echo "❌ vitest.config.js not found"
73
+ exit 1
74
+ fi
75
+ echo "✓ vitest.config.js exists"
76
+
77
+ # Check for test file
78
+ if [ ! -f "tests/example.test.js" ]; then
79
+ echo "❌ tests/example.test.js not found"
80
+ exit 1
81
+ fi
82
+ echo "✓ tests/example.test.js exists"
83
+
84
+ # Check for .env file
85
+ if [ ! -f ".env" ]; then
86
+ echo "❌ .env not found"
87
+ exit 1
88
+ fi
89
+ echo "✓ .env exists"
90
+
91
+ # Check for .gitignore
92
+ if [ ! -f ".gitignore" ]; then
93
+ echo "❌ .gitignore not found"
94
+ exit 1
95
+ fi
96
+ echo "✓ .gitignore exists"
97
+
98
+ # Check for GitHub workflow
99
+ if [ ! -f ".github/workflows/testdriver.yml" ]; then
100
+ echo "❌ .github/workflows/testdriver.yml not found"
101
+ exit 1
102
+ fi
103
+ echo "✓ .github/workflows/testdriver.yml exists"
104
+
105
+ - name: Verify vitest config contents
106
+ if: github.event_name == 'pull_request'
107
+ working-directory: /tmp/test-init-project
108
+ run: |
109
+ echo "Checking vitest.config.js contents..."
110
+
111
+ # Check for TestDriver reporter
112
+ if ! grep -q "TestDriver()" vitest.config.js; then
113
+ echo "❌ TestDriver reporter not found in vitest.config.js"
114
+ cat vitest.config.js
115
+ exit 1
116
+ fi
117
+ echo "✓ TestDriver reporter is configured"
118
+
119
+ # Check for setupFiles
120
+ if ! grep -q "setupFiles.*testdriverai/vitest/setup" vitest.config.js; then
121
+ echo "❌ setupFiles not configured correctly"
122
+ cat vitest.config.js
123
+ exit 1
124
+ fi
125
+ echo "✓ setupFiles is configured"
126
+
127
+ - name: Verify test file contents
128
+ if: github.event_name == 'pull_request'
129
+ working-directory: /tmp/test-init-project
130
+ run: |
131
+ echo "Checking test file contents..."
132
+
133
+ # Check for .provision usage
134
+ if ! grep -q "\.provision\.chrome" tests/example.test.js; then
135
+ echo "❌ Test does not use .provision.chrome"
136
+ cat tests/example.test.js
137
+ exit 1
138
+ fi
139
+ echo "✓ Test uses .provision.chrome"
140
+
141
+ # Check for TestDriver import
142
+ if ! grep -q "from 'testdriverai/vitest/hooks'" tests/example.test.js; then
143
+ echo "❌ Test does not import from testdriverai/vitest/hooks"
144
+ cat tests/example.test.js
145
+ exit 1
146
+ fi
147
+ echo "✓ Test imports TestDriver from vitest/hooks"
148
+
149
+ - name: Run the generated test
150
+ if: github.event_name == 'pull_request'
151
+ working-directory: /tmp/test-init-project
152
+ run: npm test
153
+ env:
154
+ TD_API_KEY: ${{ secrets.TD_API_KEY }}
155
+
156
+ - name: Upload init test results
157
+ if: always() && github.event_name == 'pull_request'
158
+ uses: actions/upload-artifact@v4
159
+ with:
160
+ name: test-init-results
161
+ path: /tmp/test-init-project/test-results/
162
+ retention-days: 7
163
+ if-no-files-found: warn
@@ -0,0 +1,7 @@
1
+ {
2
+ "sandboxId": "i-034317335b2eb9678",
3
+ "os": "windows",
4
+ "ami": null,
5
+ "instanceType": null,
6
+ "timestamp": "2025-12-22T23:09:20.319Z"
7
+ }
package/agent/events.js CHANGED
@@ -99,6 +99,7 @@ const events = {
99
99
  disconnect: "sandbox:disconnected",
100
100
  sent: "sandbox:sent",
101
101
  received: "sandbox:received",
102
+ progress: "sandbox:progress",
102
103
  },
103
104
  redraw: {
104
105
  status: "redraw:status",
package/agent/index.js CHANGED
@@ -17,7 +17,6 @@ const diff = require("diff");
17
17
 
18
18
  // global utilities
19
19
  const generator = require("./lib/generator.js");
20
- const promptCache = require("./lib/cache.js");
21
20
  const theme = require("./lib/theme.js");
22
21
  const SourceMapper = require("./lib/source-mapper.js");
23
22
 
@@ -110,6 +109,10 @@ class TestDriverAgent extends EventEmitter2 {
110
109
  // Create sandbox instance with this agent's emitter, analytics, and session
111
110
  this.sandbox = createSandbox(this.emitter, this.analytics, this.session);
112
111
 
112
+ // Attach Sentry log listeners to capture CLI logs as breadcrumbs
113
+ const sentry = require("../lib/sentry");
114
+ sentry.attachLogListeners(this.emitter);
115
+
113
116
  // Set the OS for the sandbox to use
114
117
  this.sandbox.os = this.sandboxOs;
115
118
 
@@ -191,6 +194,15 @@ class TestDriverAgent extends EventEmitter2 {
191
194
  this.redraw.cleanup();
192
195
  }
193
196
 
197
+ // Close sandbox connection to release the connection slot
198
+ if (this.sandbox) {
199
+ try {
200
+ this.sandbox.close();
201
+ } catch (err) {
202
+ // Ignore sandbox close errors during exit
203
+ }
204
+ }
205
+
194
206
  shouldRunPostrun =
195
207
  !this.hasRunPostrun &&
196
208
  (shouldRunPostrun || this.cliArgs?.command == "run");
@@ -356,7 +368,7 @@ class TestDriverAgent extends EventEmitter2 {
356
368
  image,
357
369
  },
358
370
  (chunk) => {
359
- if (chunk.type === "data") {
371
+ if (chunk.type === "data" && chunk.data) {
360
372
  this.emitter.emit(events.log.markdown.chunk, streamId, chunk.data);
361
373
  }
362
374
  },
@@ -420,9 +432,6 @@ class TestDriverAgent extends EventEmitter2 {
420
432
  let mousePosition = await this.system.getMousePosition();
421
433
  let activeWindow = await this.system.activeWin();
422
434
 
423
- const streamId = `check-${Date.now()}`;
424
- this.emitter.emit(events.log.markdown.start, streamId);
425
-
426
435
  let response = await this.sdk.req(
427
436
  "check",
428
437
  {
@@ -430,15 +439,10 @@ class TestDriverAgent extends EventEmitter2 {
430
439
  images,
431
440
  mousePosition,
432
441
  activeWindow,
433
- },
434
- (chunk) => {
435
- if (chunk.type === "data") {
436
- this.emitter.emit(events.log.markdown.chunk, streamId, chunk.data);
437
- }
438
- },
442
+ }
439
443
  );
440
444
 
441
- this.emitter.emit(events.log.markdown.end, streamId);
445
+ this.emitter.emit(events.log.markdown.static, response.data);
442
446
 
443
447
  this.lastScreenshot = thisScreenshot;
444
448
 
@@ -869,8 +873,7 @@ commands:
869
873
  currentTask,
870
874
  dry = false,
871
875
  validateAndLoop = false,
872
- shouldSave = true,
873
- useCache = true,
876
+ shouldSave = true
874
877
  ) {
875
878
  // Check if execution has been stopped
876
879
  if (this.stopped) {
@@ -889,56 +892,10 @@ commands:
889
892
 
890
893
  this.tasks.push(currentTask);
891
894
 
892
- // Check cache first (if enabled via parameter)
893
- const cachedYaml = useCache ? promptCache.readCache(currentTask) : null;
894
-
895
- if (cachedYaml) {
896
- // Cache hit - load and execute the cached YAML file
897
- this.emitter.emit(
898
- events.log.debug,
899
- `Using cached response for prompt: "${currentTask}"`,
900
- );
901
- this.emitter.emit(events.log.log, theme.dim("(using cached response)"));
902
-
903
- try {
904
- // Load the YAML using hydrateFromYML
905
- const parsed = await generator.hydrateFromYML(
906
- cachedYaml,
907
- this.sessionInstance,
908
- );
909
-
910
- // Execute the commands from the first step
911
- if (parsed.steps && parsed.steps.length > 0) {
912
- const step = parsed.steps[0];
913
- if (step.commands) {
914
- await this.executeCommands(
915
- step.commands,
916
- 0,
917
- false,
918
- dry,
919
- shouldSave,
920
- );
921
- }
922
- }
923
- } catch (err) {
924
- this.emitter.emit(
925
- events.log.debug,
926
- `Error loading cached YAML: ${err.message}, falling back to API`,
927
- );
928
- // Fall through to make API call if cache is invalid
929
- }
930
-
931
- return;
932
- }
933
-
934
- // Cache miss - call the API
935
895
  this.emitter.emit(events.log.narration, theme.dim("thinking..."), true);
936
896
 
937
897
  this.lastScreenshot = await this.system.captureScreenBase64();
938
898
 
939
- const streamId = `input-${Date.now()}`;
940
- this.emitter.emit(events.log.markdown.start, streamId);
941
-
942
899
  let message = await this.sdk.req(
943
900
  "input",
944
901
  {
@@ -946,59 +903,12 @@ commands:
946
903
  mousePosition: await this.system.getMousePosition(),
947
904
  activeWindow: await this.system.activeWin(),
948
905
  image: this.lastScreenshot,
949
- },
950
- (chunk) => {
951
- if (chunk.type === "data") {
952
- this.emitter.emit(events.log.markdown.chunk, streamId, chunk.data);
953
- }
954
- },
906
+ }
955
907
  );
956
908
 
957
- this.emitter.emit(events.log.markdown.end, streamId);
909
+ this.emitter.emit(events.log.log, message.data);
958
910
 
959
911
  if (message && message.data) {
960
- // Save the YAML to cache (if enabled)
961
- if (useCache) {
962
- try {
963
- // Extract YAML code blocks from the markdown response
964
- const codeblocks = await this.parser.findCodeBlocks(message.data);
965
- if (codeblocks && codeblocks.length > 0) {
966
- // Parse commands from all code blocks
967
- const allCommands = [];
968
- for (const block of codeblocks) {
969
- const commands = await this.parser.getCommands(block);
970
- allCommands.push(...commands);
971
- }
972
-
973
- // Create a proper step with prompt
974
- const step = {
975
- prompt: currentTask,
976
- commands: allCommands,
977
- };
978
-
979
- // Use dumpToYML to create a valid testdriver yaml file
980
- const yamlContent = await generator.dumpToYML(
981
- [step],
982
- this.sessionInstance,
983
- );
984
-
985
- const cachePath = promptCache.writeCache(currentTask, yamlContent);
986
- if (cachePath) {
987
- this.emitter.emit(
988
- events.log.debug,
989
- `Cached YAML saved to: ${cachePath}`,
990
- );
991
- }
992
- }
993
- } catch (err) {
994
- // If we can't extract YAML, just skip caching
995
- this.emitter.emit(
996
- events.log.debug,
997
- `Could not cache response: ${err.message}`,
998
- );
999
- }
1000
- }
1001
-
1002
912
  await this.aiExecute(message.data, validateAndLoop, dry, shouldSave);
1003
913
  this.emitter.emit(
1004
914
  events.log.debug,
@@ -1709,49 +1619,35 @@ ${regression}
1709
1619
  this.emitter.emit(events.log.log, `${inputFile} (end)`);
1710
1620
  }
1711
1621
 
1712
- // Returns sandboxId to use (either from file if recent, or null)
1713
- getRecentSandboxId() {
1714
- const lastSandboxFile = path.join(
1715
- os.homedir(),
1716
- ".testdriverai-last-sandbox",
1717
- );
1622
+ // Returns the path to the last sandbox file
1623
+ getLastSandboxFilePath() {
1624
+ const testdriverDir = path.join(process.cwd(), '.testdriver');
1625
+ return path.join(testdriverDir, 'last-sandbox');
1626
+ }
1627
+
1628
+ // Returns full sandbox info from last-sandbox file (no timeout - let API validate)
1629
+ getLastSandboxId() {
1630
+ const lastSandboxFile = this.getLastSandboxFilePath();
1718
1631
 
1719
1632
  if (fs.existsSync(lastSandboxFile)) {
1720
1633
  try {
1721
- const stats = fs.statSync(lastSandboxFile);
1722
- const mtime = new Date(stats.mtime);
1723
- const now = new Date();
1724
- const diffMinutes = (now - mtime) / (1000 * 60);
1725
- if (diffMinutes < 10) {
1726
- const fileContent = fs.readFileSync(lastSandboxFile, "utf-8").trim();
1727
-
1728
- // Parse sandbox info (supports both old format and new format)
1729
- let sandboxInfo;
1730
- try {
1731
- sandboxInfo = JSON.parse(fileContent);
1732
- } catch {
1733
- return fileContent || null;
1734
- }
1634
+ const fileContent = fs.readFileSync(lastSandboxFile, "utf-8").trim();
1735
1635
 
1736
- // Check if AMI and instance type match current requirements
1737
- const currentAmi = this.sandboxAmi || null;
1738
- const currentInstance = this.sandboxInstance || null;
1739
- const storedAmi = sandboxInfo.ami || null;
1740
- const storedInstance = sandboxInfo.instanceType || null;
1741
-
1742
- if (currentAmi === storedAmi && currentInstance === storedInstance) {
1743
- // Return sandboxId (new format) or instanceId (old format for backwards compatibility)
1744
- return sandboxInfo.sandboxId || sandboxInfo.instanceId;
1745
- } else {
1746
- this.emitter.emit(
1747
- events.log.log,
1748
- theme.dim(
1749
- "Recent sandbox found but AMI/instance type doesn't match current requirements",
1750
- ),
1751
- );
1752
- return null;
1753
- }
1636
+ // Parse sandbox info (supports both old format and new format)
1637
+ let sandboxInfo;
1638
+ try {
1639
+ sandboxInfo = JSON.parse(fileContent);
1640
+ } catch {
1641
+ return { sandboxId: fileContent || null };
1754
1642
  }
1643
+
1644
+ return {
1645
+ sandboxId: sandboxInfo.sandboxId || sandboxInfo.instanceId || null,
1646
+ os: sandboxInfo.os || 'linux',
1647
+ ami: sandboxInfo.ami || null,
1648
+ instanceType: sandboxInfo.instanceType || null,
1649
+ timestamp: sandboxInfo.timestamp || null,
1650
+ };
1755
1651
  } catch {
1756
1652
  // ignore errors
1757
1653
  }
@@ -1759,12 +1655,43 @@ ${regression}
1759
1655
  return null;
1760
1656
  }
1761
1657
 
1658
+ // Returns sandboxId to use if AMI/instance type match current requirements
1659
+ getRecentSandboxId() {
1660
+ const sandboxInfo = this.getLastSandboxId();
1661
+
1662
+ if (!sandboxInfo || !sandboxInfo.sandboxId) {
1663
+ return null;
1664
+ }
1665
+
1666
+ // Check if AMI and instance type match current requirements
1667
+ const currentAmi = this.sandboxAmi || null;
1668
+ const currentInstance = this.sandboxInstance || null;
1669
+ const storedAmi = sandboxInfo.ami || null;
1670
+ const storedInstance = sandboxInfo.instanceType || null;
1671
+
1672
+ if (currentAmi === storedAmi && currentInstance === storedInstance) {
1673
+ return sandboxInfo.sandboxId;
1674
+ } else {
1675
+ this.emitter.emit(
1676
+ events.log.log,
1677
+ theme.dim(
1678
+ "Recent sandbox found but AMI/instance type doesn't match current requirements",
1679
+ ),
1680
+ );
1681
+ return null;
1682
+ }
1683
+ }
1684
+
1762
1685
  saveLastSandboxId(sandboxId, osType = "linux") {
1763
- const lastSandboxFile = path.join(
1764
- os.homedir(),
1765
- ".testdriverai-last-sandbox",
1766
- );
1686
+ const lastSandboxFile = this.getLastSandboxFilePath();
1687
+ const testdriverDir = path.dirname(lastSandboxFile);
1688
+
1767
1689
  try {
1690
+ // Ensure .testdriver directory exists
1691
+ if (!fs.existsSync(testdriverDir)) {
1692
+ fs.mkdirSync(testdriverDir, { recursive: true });
1693
+ }
1694
+
1768
1695
  const sandboxInfo = {
1769
1696
  sandboxId: sandboxId,
1770
1697
  os: osType,
@@ -1772,7 +1699,7 @@ ${regression}
1772
1699
  instanceType: this.sandboxInstance || null,
1773
1700
  timestamp: new Date().toISOString(),
1774
1701
  };
1775
- fs.writeFileSync(lastSandboxFile, JSON.stringify(sandboxInfo), {
1702
+ fs.writeFileSync(lastSandboxFile, JSON.stringify(sandboxInfo, null, 2), {
1776
1703
  encoding: "utf-8",
1777
1704
  });
1778
1705
  } catch {
@@ -1781,10 +1708,7 @@ ${regression}
1781
1708
  }
1782
1709
 
1783
1710
  clearRecentSandboxId() {
1784
- const lastSandboxFile = path.join(
1785
- os.homedir(),
1786
- ".testdriverai-last-sandbox",
1787
- );
1711
+ const lastSandboxFile = this.getLastSandboxFilePath();
1788
1712
  try {
1789
1713
  if (fs.existsSync(lastSandboxFile)) {
1790
1714
  fs.unlinkSync(lastSandboxFile);
@@ -1793,6 +1717,7 @@ ${regression}
1793
1717
  // ignore errors
1794
1718
  }
1795
1719
  }
1720
+
1796
1721
  async buildEnv(options = {}) {
1797
1722
  // If instance already exists, do not build environment again
1798
1723
  if (this.instance) {
@@ -1875,6 +1800,7 @@ ${regression}
1875
1800
  let instance = await this.connectToSandboxDirect(
1876
1801
  this.sandboxId,
1877
1802
  true, // always persist by default
1803
+ this.keepAlive, // pass keepAlive TTL
1878
1804
  );
1879
1805
 
1880
1806
  this.instance = instance;
@@ -1889,11 +1815,6 @@ ${regression}
1889
1815
  );
1890
1816
  console.error("Failed to reconnect to sandbox:", error);
1891
1817
  }
1892
- } else if (!createNew && !recentId) {
1893
- this.emitter.emit(
1894
- events.log.narration,
1895
- theme.dim(`no recent sandbox found, creating a new one.`),
1896
- );
1897
1818
  } else if (!createNew && this.sandboxId && !this.config.CI) {
1898
1819
  // Only attempt to connect to existing sandbox if not in CI mode and not creating new
1899
1820
  // Attempt to connect to known instance
@@ -1906,6 +1827,7 @@ ${regression}
1906
1827
  let instance = await this.connectToSandboxDirect(
1907
1828
  this.sandboxId,
1908
1829
  true, // always persist by default
1830
+ this.keepAlive, // pass keepAlive TTL
1909
1831
  );
1910
1832
 
1911
1833
  this.instance = instance;
@@ -1948,6 +1870,7 @@ ${regression}
1948
1870
  let instance = await this.connectToSandboxDirect(
1949
1871
  this.sandboxId,
1950
1872
  true, // always persist by default
1873
+ this.keepAlive, // pass keepAlive TTL
1951
1874
  );
1952
1875
  this.instance = instance;
1953
1876
  await this.renderSandbox(instance, headless);
@@ -2137,10 +2060,10 @@ Please check your network connection, TD_API_KEY, or the service status.`,
2137
2060
  }
2138
2061
  }
2139
2062
 
2140
- async connectToSandboxDirect(sandboxId, persist = false) {
2063
+ async connectToSandboxDirect(sandboxId, persist = false, keepAlive = null) {
2141
2064
  const { formatter } = require("../sdk-log-formatter.js");
2142
2065
  this.emitter.emit(events.log.narration, formatter.getPrefix("connect") + " " + theme.green.bold("Connecting") + " " + theme.cyan(`to sandbox...`));
2143
- let reply = await this.sandbox.connect(sandboxId, persist);
2066
+ let reply = await this.sandbox.connect(sandboxId, persist, keepAlive);
2144
2067
 
2145
2068
  // reply includes { success, url, sandbox: {...} }
2146
2069
  // For renderSandbox, we need the sandbox object with url merged in
@@ -2169,6 +2092,10 @@ Please check your network connection, TD_API_KEY, or the service status.`,
2169
2092
  if (this.sandboxInstance) {
2170
2093
  sandboxConfig.instanceType = this.sandboxInstance;
2171
2094
  }
2095
+ // Add keepAlive TTL if specified
2096
+ if (this.keepAlive !== undefined && this.keepAlive !== null) {
2097
+ sandboxConfig.keepAlive = this.keepAlive;
2098
+ }
2172
2099
 
2173
2100
  let instance = await this.sandbox.send(sandboxConfig, 60000 * 8);
2174
2101
 
@@ -2200,6 +2127,15 @@ Please check your network connection, TD_API_KEY, or the service status.`,
2200
2127
  }
2201
2128
 
2202
2129
  this.session.set(sessionRes.data.id);
2130
+
2131
+ // Set Sentry session trace context for distributed tracing
2132
+ // This links CLI errors/logs to the same trace as API calls
2133
+ try {
2134
+ const sentry = require("../lib/sentry");
2135
+ sentry.setSessionTraceContext(sessionRes.data.id);
2136
+ } catch (e) {
2137
+ // Sentry module may not be available, ignore
2138
+ }
2203
2139
  }
2204
2140
 
2205
2141
  // Helper method to find testdriver directory by traversing up from a file path
@@ -155,11 +155,12 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
155
155
  }
156
156
  }
157
157
 
158
- async connect(sandboxId, persist = false) {
158
+ async connect(sandboxId, persist = false, keepAlive = null) {
159
159
  let reply = await this.send({
160
160
  type: "connect",
161
161
  persist,
162
162
  sandboxId,
163
+ keepAlive,
163
164
  });
164
165
 
165
166
  if (reply.success) {
@@ -227,6 +228,15 @@ const createSandbox = (emitter, analytics, sessionInstance) => {
227
228
  this.socket.on("message", async (raw) => {
228
229
  let message = JSON.parse(raw);
229
230
 
231
+ // Handle progress messages (no requestId needed)
232
+ if (message.type === 'sandbox.progress') {
233
+ emitter.emit(events.sandbox.progress, {
234
+ step: message.step,
235
+ message: message.message,
236
+ });
237
+ return;
238
+ }
239
+
230
240
  if (!this.ps[message.requestId]) {
231
241
  console.warn(
232
242
  "No pending promise found for requestId:",