testdriverai 7.10.0-canary.0 → 7.10.1-test

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 (196) hide show
  1. package/.claude-plugin/marketplace.json +17 -0
  2. package/agent/index.js +116 -71
  3. package/agent/interface.js +13 -1
  4. package/agent/lib/commands.js +35 -15
  5. package/agent/lib/provision-commands.js +92 -11
  6. package/agent/lib/sandbox.js +263 -30
  7. package/agent/lib/sdk.js +16 -12
  8. package/agent/lib/system.js +107 -31
  9. package/ai/.claude-plugin/plugin.json +89 -4
  10. package/ai/agents/testdriver.md +4 -8
  11. package/ai/skills/testdriver-assert/SKILL.md +0 -1
  12. package/ai/skills/testdriver-aws-setup/SKILL.md +1 -1
  13. package/ai/skills/testdriver-caching/SKILL.md +13 -6
  14. package/ai/skills/testdriver-ci-cd/SKILL.md +57 -14
  15. package/ai/skills/testdriver-client/SKILL.md +10 -6
  16. package/ai/skills/testdriver-customizing-devices/SKILL.md +7 -5
  17. package/ai/skills/testdriver-dashcam/SKILL.md +36 -3
  18. package/ai/skills/testdriver-debugging-with-screenshots/SKILL.md +28 -4
  19. package/ai/skills/testdriver-device-config/SKILL.md +5 -5
  20. package/ai/skills/testdriver-find/SKILL.md +50 -11
  21. package/ai/skills/testdriver-generating-tests/SKILL.md +163 -8
  22. package/ai/skills/testdriver-hosted/SKILL.md +1 -1
  23. package/ai/skills/testdriver-machine-setup/SKILL.md +331 -0
  24. package/ai/skills/testdriver-making-assertions/SKILL.md +78 -2
  25. package/ai/skills/testdriver-mcp-workflow/SKILL.md +2 -2
  26. package/ai/skills/testdriver-parse/SKILL.md +1 -1
  27. package/ai/skills/testdriver-performing-actions/SKILL.md +97 -3
  28. package/ai/skills/testdriver-provision/SKILL.md +0 -20
  29. package/ai/skills/testdriver-quickstart/SKILL.md +299 -21
  30. package/ai/skills/testdriver-reusable-code/SKILL.md +3 -3
  31. package/ai/skills/testdriver-running-tests/SKILL.md +1 -1
  32. package/ai/skills/testdriver-screenshot/SKILL.md +5 -5
  33. package/ai/skills/testdriver-scroll/SKILL.md +30 -160
  34. package/ai/skills/testdriver-test-writer/SKILL.md +3 -5
  35. package/ai/skills/testdriver-testdriver/SKILL.md +3 -7
  36. package/ai/skills/testdriver-wait/SKILL.md +1 -1
  37. package/{docs/v7/ai.mdx → ai/skills/testdriver:ai/SKILL.md} +3 -4
  38. package/ai/skills/testdriver:assert/SKILL.md +315 -0
  39. package/ai/skills/testdriver:aws-setup/SKILL.md +448 -0
  40. package/ai/skills/testdriver:cache/SKILL.md +221 -0
  41. package/ai/skills/testdriver:caching/SKILL.md +124 -0
  42. package/ai/skills/testdriver:captcha/SKILL.md +158 -0
  43. package/ai/skills/testdriver:ci-cd/SKILL.md +659 -0
  44. package/ai/skills/testdriver:click/SKILL.md +286 -0
  45. package/ai/skills/testdriver:client/SKILL.md +477 -0
  46. package/ai/skills/testdriver:customizing-devices/SKILL.md +319 -0
  47. package/ai/skills/testdriver:dashcam/SKILL.md +451 -0
  48. package/ai/skills/testdriver:debugging-with-screenshots/SKILL.md +415 -0
  49. package/{docs/v7/device-config.mdx → ai/skills/testdriver:device-config/SKILL.md} +8 -8
  50. package/ai/skills/testdriver:double-click/SKILL.md +102 -0
  51. package/ai/skills/testdriver:elements/SKILL.md +605 -0
  52. package/ai/skills/testdriver:enterprise/SKILL.md +7 -0
  53. package/ai/skills/testdriver:errors/SKILL.md +246 -0
  54. package/ai/skills/testdriver:events/SKILL.md +356 -0
  55. package/ai/skills/testdriver:exec/SKILL.md +317 -0
  56. package/ai/skills/testdriver:find/SKILL.md +860 -0
  57. package/ai/skills/testdriver:focus-application/SKILL.md +293 -0
  58. package/ai/skills/testdriver:generating-tests/SKILL.md +25 -0
  59. package/ai/skills/testdriver:hosted/SKILL.md +156 -0
  60. package/ai/skills/testdriver:hover/SKILL.md +278 -0
  61. package/{docs/v7/locating-elements.mdx → ai/skills/testdriver:locating-elements/SKILL.md} +3 -3
  62. package/ai/skills/testdriver:making-assertions/SKILL.md +32 -0
  63. package/ai/skills/testdriver:mcp/SKILL.md +7 -0
  64. package/ai/skills/testdriver:mouse-down/SKILL.md +161 -0
  65. package/ai/skills/testdriver:mouse-up/SKILL.md +164 -0
  66. package/ai/skills/testdriver:parse/SKILL.md +236 -0
  67. package/ai/skills/testdriver:performing-actions/SKILL.md +53 -0
  68. package/ai/skills/testdriver:press-keys/SKILL.md +348 -0
  69. package/ai/skills/testdriver:provision/SKILL.md +331 -0
  70. package/ai/skills/testdriver:quickstart/SKILL.md +172 -0
  71. package/ai/skills/testdriver:redraw/SKILL.md +214 -0
  72. package/ai/skills/testdriver:reusable-code/SKILL.md +249 -0
  73. package/ai/skills/testdriver:right-click/SKILL.md +123 -0
  74. package/{docs/v7/running-tests.mdx → ai/skills/testdriver:running-tests/SKILL.md} +4 -4
  75. package/ai/skills/testdriver:screenshot/SKILL.md +248 -0
  76. package/ai/skills/testdriver:screenshots/SKILL.md +184 -0
  77. package/ai/skills/testdriver:scroll/SKILL.md +205 -0
  78. package/ai/skills/testdriver:secrets/SKILL.md +115 -0
  79. package/ai/skills/testdriver:self-hosted/SKILL.md +147 -0
  80. package/ai/skills/testdriver:test-results-json/SKILL.md +257 -0
  81. package/ai/skills/testdriver:testdriver/SKILL.md +624 -0
  82. package/ai/skills/testdriver:type/SKILL.md +357 -0
  83. package/ai/skills/testdriver:variables/SKILL.md +111 -0
  84. package/ai/skills/testdriver:wait/SKILL.md +50 -0
  85. package/{docs/v7/waiting-for-elements.mdx → ai/skills/testdriver:waiting-for-elements/SKILL.md} +3 -3
  86. package/ai/skills/testdriver:what-is-testdriver/SKILL.md +54 -0
  87. package/debugger/index.html +12 -0
  88. package/docs/_data/examples-manifest.json +54 -46
  89. package/docs/_data/examples-manifest.schema.json +12 -2
  90. package/docs/_scripts/extract-example-urls.js +21 -3
  91. package/docs/_scripts/generate-examples.js +64 -16
  92. package/docs/changelog.mdx +29 -2
  93. package/docs/claude-mcp-plugin.mdx +65 -18
  94. package/docs/docs.json +16 -33
  95. package/docs/snippets/tests/scroll-yaml.mdx +2 -2
  96. package/docs/v6/commands/scroll-until-image.mdx +1 -1
  97. package/docs/v6/commands/scroll-until-text.mdx +2 -2
  98. package/docs/v6/commands/scroll.mdx +2 -2
  99. package/docs/v7/_drafts/agents.mdx +0 -4
  100. package/docs/v7/_drafts/best-practices.mdx +0 -12
  101. package/docs/v7/_drafts/commands/scroll-until-image.mdx +1 -1
  102. package/docs/v7/_drafts/commands/scroll-until-text.mdx +2 -2
  103. package/docs/v7/_drafts/commands/scroll.mdx +1 -1
  104. package/docs/v7/_drafts/configuration.mdx +0 -12
  105. package/docs/v7/_drafts/faq.mdx +2 -10
  106. package/docs/v7/_drafts/readme.mdx +0 -1
  107. package/docs/v7/_drafts/troubleshooting.mdx +1 -4
  108. package/docs/v7/assert.mdx +0 -1
  109. package/docs/v7/aws-setup.mdx +1 -1
  110. package/docs/v7/caching.mdx +14 -10
  111. package/docs/v7/ci-cd.mdx +98 -14
  112. package/docs/v7/client.mdx +10 -6
  113. package/docs/v7/copilot/auto-healing.mdx +167 -18
  114. package/docs/v7/copilot/running-tests.mdx +915 -54
  115. package/docs/v7/customizing-devices.mdx +7 -5
  116. package/docs/v7/dashcam.mdx +36 -3
  117. package/docs/v7/debugging-with-screenshots.mdx +31 -7
  118. package/docs/v7/examples/ai.mdx +13 -8
  119. package/docs/v7/examples/assert.mdx +1 -1
  120. package/docs/v7/examples/chrome-extension.mdx +30 -13
  121. package/docs/v7/examples/element-not-found.mdx +1 -1
  122. package/docs/v7/examples/findall-coffee-icons.mdx +3 -2
  123. package/docs/v7/examples/formatted-logging.mdx +74 -0
  124. package/docs/v7/examples/hover-image.mdx +1 -1
  125. package/docs/v7/examples/hover-text-with-description.mdx +2 -2
  126. package/docs/v7/examples/hover-text.mdx +1 -1
  127. package/docs/v7/examples/installer.mdx +1 -1
  128. package/docs/v7/examples/launch-vscode-linux.mdx +27 -27
  129. package/docs/v7/examples/match-image.mdx +1 -1
  130. package/docs/v7/examples/parse.mdx +3 -2
  131. package/docs/v7/examples/press-keys.mdx +2 -2
  132. package/docs/v7/examples/scroll-keyboard.mdx +2 -2
  133. package/docs/v7/examples/scroll-until-text.mdx +1 -1
  134. package/docs/v7/examples/scroll.mdx +1 -1
  135. package/docs/v7/examples/type.mdx +1 -1
  136. package/docs/v7/find.mdx +50 -11
  137. package/docs/v7/generating-tests.mdx +166 -10
  138. package/docs/v7/hosted.mdx +1 -1
  139. package/docs/v7/making-assertions.mdx +81 -4
  140. package/docs/v7/parse.mdx +1 -1
  141. package/docs/v7/performing-actions.mdx +100 -5
  142. package/docs/v7/provision.mdx +0 -20
  143. package/docs/v7/quickstart.mdx +299 -21
  144. package/docs/v7/reusable-code.mdx +3 -3
  145. package/docs/v7/screenshot.mdx +5 -5
  146. package/docs/v7/scroll.mdx +30 -160
  147. package/docs/v7/wait.mdx +1 -1
  148. package/examples/chrome-extension.test.mjs +29 -12
  149. package/examples/config.mjs +1 -1
  150. package/examples/launch-vscode-linux.test.mjs +25 -25
  151. package/interfaces/cli/commands/init.js +84 -2
  152. package/interfaces/vitest-plugin.mjs +51 -61
  153. package/lib/core/Dashcam.js +43 -2
  154. package/lib/core/index.d.ts +13 -1
  155. package/lib/github-comment.mjs +11 -5
  156. package/lib/init-project.js +98 -44
  157. package/lib/install-clients.js +470 -0
  158. package/lib/provision.js +749 -0
  159. package/lib/resolve-channel.js +2 -2
  160. package/lib/sentry.js +15 -2
  161. package/lib/vitest/hooks.mjs +44 -34
  162. package/mcp-server/README.md +32 -0
  163. package/mcp-server/dist/core/actions.d.ts +176 -0
  164. package/mcp-server/dist/core/actions.js +753 -0
  165. package/mcp-server/dist/env-utils.d.ts +45 -0
  166. package/mcp-server/dist/env-utils.js +63 -0
  167. package/mcp-server/dist/provision-types.d.ts +6 -2
  168. package/mcp-server/dist/provision-types.js +3 -1
  169. package/mcp-server/dist/server.mjs +449 -751
  170. package/mcp-server/package-lock.json +384 -1
  171. package/mcp-server/package.json +5 -2
  172. package/mcp-server/src/core/actions.ts +912 -0
  173. package/mcp-server/src/env-utils.test.ts +82 -0
  174. package/mcp-server/src/env-utils.ts +77 -0
  175. package/mcp-server/src/provision-types.ts +4 -1
  176. package/mcp-server/src/server.ts +533 -954
  177. package/mcp-server/tsconfig.json +1 -1
  178. package/mcp-server/vitest.config.ts +7 -0
  179. package/package.json +15 -3
  180. package/sdk.d.ts +11 -9
  181. package/sdk.js +116 -746
  182. package/setup/aws/spawn-runner.sh +24 -3
  183. package/docs/v7/_drafts/caching-ai.mdx +0 -215
  184. package/docs/v7/_drafts/caching.mdx +0 -366
  185. package/docs/v7/_drafts/prompt-cache.mdx +0 -200
  186. package/docs/v7/copilot/creating-tests.mdx +0 -156
  187. package/docs/v7/copilot/github.mdx +0 -143
  188. package/docs/v7/copilot/setup.mdx +0 -143
  189. package/docs/v7/examples/exec-output.mdx +0 -84
  190. package/docs/v7/examples/exec-pwsh.mdx +0 -82
  191. package/docs/v7/examples/focus-window.mdx +0 -61
  192. package/docs/v7/examples/scroll-until-image.mdx +0 -82
  193. package/docs/v7/examples/windows-installer.mdx +0 -95
  194. package/examples/formatted-logging.test.mjs +0 -27
  195. package/examples/match-image.test.mjs +0 -55
  196. package/manual/exec-stream-logs.test.mjs +0 -25
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "testdriver",
3
+ "version": "7.9.54-test",
4
+ "description": "TestDriver.ai plugins for Claude Code — MCP server, agent, and skills for building natural-language end-to-end tests.",
5
+ "repository": "https://github.com/testdriverai/testdriverai",
6
+ "owner": {
7
+ "name": "TestDriver.ai",
8
+ "url": "https://testdriver.ai"
9
+ },
10
+ "plugins": [
11
+ {
12
+ "name": "testdriver",
13
+ "source": "./ai",
14
+ "description": "TestDriver.ai MCP server, TestDriver expert agent, and skills for authoring computer-use end-to-end tests with Vitest."
15
+ }
16
+ ]
17
+ }
package/agent/index.js CHANGED
@@ -494,65 +494,89 @@ class TestDriverAgent extends EventEmitter2 {
494
494
  );
495
495
  }
496
496
 
497
- try {
498
- let response;
499
-
500
- // "run" and "if" commands are special meta commands
501
- // that change the flow of execution
502
- if (command.command == "run") {
503
- response = await this.embed(command.file, depth, pushToHistory);
504
- } else if (command.command == "if") {
505
- response = await this.iffy(
506
- command.condition,
507
- command.then,
508
- command.else,
509
- depth,
510
- );
511
- } else {
512
- response = await this.commander.run(command, depth);
513
- }
497
+ const sentry = require("../lib/sentry");
498
+ return sentry.withSpan(
499
+ {
500
+ name: `command.${commandName}`,
501
+ op: "command",
502
+ attributes: { "command.name": commandName, "command.depth": depth },
503
+ },
504
+ async (span) => {
505
+ try {
506
+ let response;
507
+
508
+ // "run" and "if" commands are special meta commands
509
+ // that change the flow of execution
510
+ if (command.command == "run") {
511
+ response = await this.embed(command.file, depth, pushToHistory);
512
+ } else if (command.command == "if") {
513
+ response = await this.iffy(
514
+ command.condition,
515
+ command.then,
516
+ command.else,
517
+ depth,
518
+ );
519
+ } else {
520
+ response = await this.commander.run(command, depth);
521
+ }
514
522
 
515
- const endTime = Date.now();
516
- const duration = endTime - startTime;
523
+ const endTime = Date.now();
524
+ const duration = endTime - startTime;
517
525
 
518
- // Emit command success event with source mapping
519
- this.emitter.emit(events.command.success, {
520
- command: commandName,
521
- depth,
522
- data: command,
523
- duration,
524
- response,
525
- timestamp: endTime,
526
- sourcePosition: sourcePosition,
527
- });
526
+ if (span) {
527
+ span.setStatus({ code: 1 }); // OK
528
+ }
528
529
 
529
- // if the result of a command contains more commands, we perform the process again
530
- if (response && typeof response === "string") {
531
- return await this.actOnMarkdown(response, depth, false, false, false);
532
- }
533
- } catch (error) {
534
- const endTime = Date.now();
535
- const duration = endTime - startTime;
530
+ // Emit command success event with source mapping
531
+ this.emitter.emit(events.command.success, {
532
+ command: commandName,
533
+ depth,
534
+ data: command,
535
+ duration,
536
+ response,
537
+ timestamp: endTime,
538
+ sourcePosition: sourcePosition,
539
+ });
536
540
 
537
- // Emit command error event with source mapping
538
- this.emitter.emit(events.command.error, {
539
- command: commandName,
540
- depth,
541
- data: command,
542
- error: error.message,
543
- duration,
544
- timestamp: endTime,
545
- sourcePosition: sourcePosition,
546
- });
541
+ // if the result of a command contains more commands, we perform the process again
542
+ if (response && typeof response === "string") {
543
+ return await this.actOnMarkdown(
544
+ response,
545
+ depth,
546
+ false,
547
+ false,
548
+ false,
549
+ );
550
+ }
551
+ } catch (error) {
552
+ const endTime = Date.now();
553
+ const duration = endTime - startTime;
547
554
 
548
- return await this.haveAIResolveError(
549
- error,
550
- yaml.dump({ commands: [yml] }),
551
- depth,
552
- true,
553
- shouldSave,
554
- );
555
- }
555
+ if (span) {
556
+ span.setStatus({ code: 2, message: error.message });
557
+ }
558
+
559
+ // Emit command error event with source mapping
560
+ this.emitter.emit(events.command.error, {
561
+ command: commandName,
562
+ depth,
563
+ data: command,
564
+ error: error.message,
565
+ duration,
566
+ timestamp: endTime,
567
+ sourcePosition: sourcePosition,
568
+ });
569
+
570
+ return await this.haveAIResolveError(
571
+ error,
572
+ yaml.dump({ commands: [yml] }),
573
+ depth,
574
+ true,
575
+ shouldSave,
576
+ );
577
+ }
578
+ },
579
+ );
556
580
  }
557
581
 
558
582
  async executeCommands(
@@ -1968,29 +1992,50 @@ ${regression}
1968
1992
  // Write session file for IDE preview (VSCode extension watches for these)
1969
1993
  writeIdeSessionFile(debuggerUrl, data) {
1970
1994
  const fs = require("fs");
1995
+ const os = require("os");
1971
1996
  const path = require("path");
1972
1997
 
1973
- const sessionId = `${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
1974
- const previewsDir = path.join(process.cwd(), ".testdriver", ".previews");
1998
+ // Base dir for the preview file. `process.cwd()` is read-only in some hosts
1999
+ // (e.g. Vercel/Lambda serverless, where cwd is /var/task), which makes the
2000
+ // mkdir below throw. Allow an explicit override and otherwise fall back to a
2001
+ // writable temp dir, so a preview write never blocks the session.
2002
+ const baseDir =
2003
+ process.env.TD_PREVIEWS_DIR ||
2004
+ (this.config && this.config.TD_PREVIEWS_DIR) ||
2005
+ process.cwd();
2006
+ const previewsDir = path.join(baseDir, ".testdriver", ".previews");
1975
2007
 
1976
- // Create the previews directory if it doesn't exist
1977
- if (!fs.existsSync(previewsDir)) {
1978
- fs.mkdirSync(previewsDir, { recursive: true });
1979
- }
2008
+ const sessionId = `${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
1980
2009
 
1981
- const sessionData = {
1982
- sessionId,
1983
- debuggerUrl,
1984
- resolution: Array.isArray(data.resolution) ? data.resolution : (data.resolution ? data.resolution.split("x").map(Number) : [1920, 1080]),
1985
- testFile: data.testFile || this.testFile || null,
1986
- os: data.os || this.sandboxOs || "linux",
1987
- timestamp: Date.now(),
1988
- };
2010
+ // The IDE preview file is a best-effort convenience (the VSCode extension
2011
+ // watches for it). It must never break session start — if the target dir
2012
+ // isn't writable, fall back to os.tmpdir(), and if that also fails, log and
2013
+ // move on rather than throwing out of the provisioning path.
2014
+ try {
2015
+ const sessionData = {
2016
+ sessionId,
2017
+ debuggerUrl,
2018
+ resolution: Array.isArray(data.resolution) ? data.resolution : (data.resolution ? data.resolution.split("x").map(Number) : [1920, 1080]),
2019
+ testFile: data.testFile || this.testFile || null,
2020
+ os: data.os || this.sandboxOs || "linux",
2021
+ timestamp: Date.now(),
2022
+ };
1989
2023
 
1990
- const filePath = path.join(previewsDir, `${sessionId}.json`);
1991
- fs.writeFileSync(filePath, JSON.stringify(sessionData, null, 2));
2024
+ let dir = previewsDir;
2025
+ try {
2026
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
2027
+ } catch {
2028
+ // cwd/base wasn't writable — retry under the OS temp dir.
2029
+ dir = path.join(os.tmpdir(), ".testdriver", ".previews");
2030
+ fs.mkdirSync(dir, { recursive: true });
2031
+ }
1992
2032
 
1993
- logger.log(`IDE preview session written to ${filePath}`);
2033
+ const filePath = path.join(dir, `${sessionId}.json`);
2034
+ fs.writeFileSync(filePath, JSON.stringify(sessionData, null, 2));
2035
+ logger.log(`IDE preview session written to ${filePath}`);
2036
+ } catch (error) {
2037
+ logger.log(`Skipped IDE preview session file (non-fatal): ${error.message}`);
2038
+ }
1994
2039
  }
1995
2040
 
1996
2041
  // Find the VS Code instance that contains the test file
@@ -10,7 +10,19 @@ function createCommandDefinitions(agent) {
10
10
  init: {
11
11
  description: "Initialize a new TestDriver project with Vitest SDK examples",
12
12
  args: {},
13
- flags: {},
13
+ flags: {
14
+ client: Flags.string({
15
+ description:
16
+ "AI client(s) to install into (comma-separated, or 'all'). e.g. --client claude-code,cursor. Omit for an interactive picker.",
17
+ multiple: false,
18
+ }),
19
+ "sample-test": Flags.boolean({
20
+ description:
21
+ "Scaffold the example test files (tests/example.test.js + tests/login.js). Use --no-sample-test to skip them (e.g. when an agent writes its own tests).",
22
+ default: true,
23
+ allowNo: true,
24
+ }),
25
+ },
14
26
  handler: async () => {
15
27
  // This handler is special - it doesn't need an agent instance
16
28
  // It just scaffolds files, so it will be handled by the CLI command
@@ -302,10 +302,12 @@ const createCommands = (
302
302
  `🔍 assert() threshold: ${threshold} (cache ${threshold < 0 ? "DISABLED" : "ENABLED"}${cacheKey ? `, cacheKey: ${cacheKey.substring(0, 8)}...` : ""})`,
303
303
  );
304
304
 
305
- // Use v7 endpoint for assert with caching support
305
+ // Use v7 endpoint for assert with caching support.
306
+ // captureScreenImage returns { imageKey } (fast S3-key path) or { image }
307
+ // (base64 fallback) — see system.captureScreenImage.
306
308
  let response = await sdk.req("assert", {
307
309
  expect: assertion,
308
- image: await system.captureScreenBase64(),
310
+ ...(await system.captureScreenImage()),
309
311
  threshold,
310
312
  cacheKey,
311
313
  os,
@@ -815,7 +817,7 @@ const createCommands = (
815
817
 
816
818
  let response = await sdk.req("find", {
817
819
  element: description,
818
- image: await system.captureScreenBase64(),
820
+ ...(await system.captureScreenImage()),
819
821
  });
820
822
 
821
823
  if (!response || !response.coordinates) {
@@ -855,7 +857,7 @@ const createCommands = (
855
857
 
856
858
  let response = await sdk.req("find", {
857
859
  element: description,
858
- image: await system.captureScreenBase64(),
860
+ ...(await system.captureScreenImage()),
859
861
  });
860
862
 
861
863
  if (!response || !response.coordinates) {
@@ -1211,7 +1213,7 @@ const createCommands = (
1211
1213
  while (durationPassed < timeout && !passed) {
1212
1214
  const response = await sdk.req("find", {
1213
1215
  element: text,
1214
- image: await system.captureScreenBase64(),
1216
+ ...(await system.captureScreenImage()),
1215
1217
  });
1216
1218
 
1217
1219
  passed = !!(response && response.coordinates);
@@ -1304,7 +1306,7 @@ const createCommands = (
1304
1306
  while (scrollDistance <= maxDistance && !passed) {
1305
1307
  const response = await sdk.req("find", {
1306
1308
  element: text,
1307
- image: await system.captureScreenBase64(),
1309
+ ...(await system.captureScreenImage()),
1308
1310
  });
1309
1311
 
1310
1312
  passed = !!(response && response.coordinates);
@@ -1531,9 +1533,11 @@ const createCommands = (
1531
1533
  emitter.emit(events.log.narration, formatter.getPrefix("action") + " " + theme.cyan.bold("Exec") + " " + theme.magenta(`[${language}]`), true);
1532
1534
 
1533
1535
  // Log nested command details (truncate to first line)
1534
- const firstLine = code.split('\n')[0];
1535
- const codeDisplay = code.includes('\n') ? firstLine + '...' : firstLine;
1536
- emitter.emit(events.log.log, formatter.formatCodeLine(codeDisplay));
1536
+ if (!silent) {
1537
+ const firstLine = code.split('\n')[0];
1538
+ const codeDisplay = code.includes('\n') ? firstLine + '...' : firstLine;
1539
+ emitter.emit(events.log.log, formatter.formatCodeLine(codeDisplay));
1540
+ }
1537
1541
 
1538
1542
  let plat = system.platform();
1539
1543
 
@@ -1565,11 +1569,26 @@ const createCommands = (
1565
1569
  let result = null;
1566
1570
 
1567
1571
  const execTimeout = timeout || 300000;
1568
- result = await sandbox.send({
1569
- type: "commands.run",
1570
- command: code,
1571
- timeout: execTimeout,
1572
- }, execTimeout);
1572
+
1573
+ // Stream output chunks in real-time as they arrive from the runner
1574
+ let streamedOutput = false;
1575
+ const onExecOutput = ({ chunk }) => {
1576
+ if (!silent && chunk) {
1577
+ emitter.emit(events.log.log, theme.dim(chunk), true);
1578
+ streamedOutput = true;
1579
+ }
1580
+ };
1581
+ emitter.on(events.exec.output, onExecOutput);
1582
+
1583
+ try {
1584
+ result = await sandbox.send({
1585
+ type: "commands.run",
1586
+ command: code,
1587
+ timeout: execTimeout,
1588
+ }, execTimeout);
1589
+ } finally {
1590
+ emitter.off(events.exec.output, onExecOutput);
1591
+ }
1573
1592
 
1574
1593
  const execActionEndTime = Date.now();
1575
1594
  const execDuration = execActionEndTime - execActionLogStart;
@@ -1595,7 +1614,8 @@ const createCommands = (
1595
1614
  true,
1596
1615
  );
1597
1616
 
1598
- if (!silent && result.out?.stdout) {
1617
+ // Skip stdout log if already streamed in real-time to avoid duplication
1618
+ if (!silent && !streamedOutput && result.out?.stdout) {
1599
1619
  emitter.emit(events.log.log, theme.dim(` stdout:`), true);
1600
1620
  emitter.emit(events.log.log, theme.dim(` ${result.out.stdout}`), true);
1601
1621
  }
@@ -13,6 +13,24 @@
13
13
 
14
14
  'use strict';
15
15
 
16
+ /**
17
+ * Extract the major.minor portion of a semver-ish version string.
18
+ *
19
+ * Examples:
20
+ * '7.9.59' -> '7.9'
21
+ * '7.9.59-test' -> '7.9'
22
+ * '7.10.0-canary.3'-> '7.10'
23
+ * '' -> ''
24
+ *
25
+ * @param {string} version
26
+ * @returns {string}
27
+ */
28
+ function majorMinor(version) {
29
+ if (!version || typeof version !== 'string') return '';
30
+ var m = version.match(/^(\d+)\.(\d+)/);
31
+ return m ? m[1] + '.' + m[2] : '';
32
+ }
33
+
16
34
  /**
17
35
  * Build the agent config object written to the sandbox.
18
36
  *
@@ -25,9 +43,10 @@
25
43
  * @param {string} [opts.sentryChannel]
26
44
  * @param {Object} opts.ablyToken - Ably token object
27
45
  * @param {string} opts.channelName - Ably channel name
46
+ * @param {string} [opts.sessionId] - Session ID for distributed tracing (traceId = MD5(sessionId))
28
47
  * @returns {Object} Agent config to serialize as JSON
29
48
  */
30
- function buildAgentConfig({ sandboxId, apiRoot, apiKey, sentryDsn, sentryEnvironment, sentryChannel, ablyToken, channelName }) {
49
+ function buildAgentConfig({ sandboxId, apiRoot, apiKey, sentryDsn, sentryEnvironment, sentryChannel, ablyToken, channelName, sessionId }) {
31
50
  return {
32
51
  sandboxId,
33
52
  apiRoot,
@@ -35,6 +54,7 @@ function buildAgentConfig({ sandboxId, apiRoot, apiKey, sentryDsn, sentryEnviron
35
54
  sentryDsn: sentryDsn || undefined,
36
55
  sentryEnvironment: sentryEnvironment || 'production',
37
56
  sentryChannel: sentryChannel || undefined,
57
+ sessionId: sessionId || undefined,
38
58
  ably: {
39
59
  token: ablyToken,
40
60
  channel: channelName,
@@ -54,9 +74,10 @@ function buildAgentConfig({ sandboxId, apiRoot, apiKey, sentryDsn, sentryEnviron
54
74
  * @param {string} opts.configJson - JSON.stringify'd agent config
55
75
  * @param {string} opts.sandboxId - For logging
56
76
  * @param {string} [opts.s3DownloadUrl] - S3 pre-signed URL for dev/test (omit for npm install)
77
+ * @param {string} [opts.imageVersion] - Version baked into the AMI (skip npm install when it matches)
57
78
  * @returns {string[]} Array of PowerShell command strings
58
79
  */
59
- function windowsProvisionCommands({ channel, configJson, sandboxId, s3DownloadUrl }) {
80
+ function windowsProvisionCommands({ channel, configJson, sandboxId, s3DownloadUrl, imageVersion }) {
60
81
  var useS3 = (channel === 'dev' || channel === 'test') && s3DownloadUrl;
61
82
  var commands = [];
62
83
 
@@ -92,11 +113,40 @@ function windowsProvisionCommands({ channel, configJson, sandboxId, s3DownloadUr
92
113
  // Canary/stable (or dev/test without S3 URL): npm install by dist-tag
93
114
  agentScript = 'node_modules/@testdriverai/runner/sandbox-agent.js';
94
115
  var runnerTag = channel === 'stable' ? 'latest' : channel;
95
- commands.push(
96
- "Write-Host 'Installing @testdriverai/runner@" + runnerTag + "...'",
97
- 'npm install @testdriverai/runner@' + runnerTag + ' --omit=dev 2>&1 | Write-Host',
98
- "Write-Host 'Runner install complete'"
99
- );
116
+
117
+ // If we know the image version, check whether the baked-in runner already
118
+ // matches — skip npm install entirely when it does (saves 5-15s per launch).
119
+ // Only the major.minor portion is compared (patch & prerelease suffix are
120
+ // ignored) so trivial patch bumps don't trigger an unnecessary npm install.
121
+ // Emits a structured RUNNER_VERSION_CHECK line so SSM output / runner logs
122
+ // capture the outcome for monitoring.
123
+ if (imageVersion) {
124
+ var expectedMinor = majorMinor(imageVersion);
125
+ commands.push(
126
+ "Write-Host 'Checking installed runner version...'",
127
+ "$installedVersion = ''",
128
+ "try { $pkg = Get-Content 'node_modules/@testdriverai/runner/package.json' -Raw | ConvertFrom-Json; $installedVersion = $pkg.version } catch {}",
129
+ "$installedMinor = if ($installedVersion) { ($installedVersion -split '[.-]')[0..1] -join '.' } else { '' }",
130
+ "if (('" + expectedMinor + "' -ne '') -and ($installedMinor -eq '" + expectedMinor + "')) {",
131
+ " Write-Host \"Runner already at v$installedVersion (minor " + expectedMinor + "), skipping update\"",
132
+ " Write-Host (\"RUNNER_VERSION_CHECK:\" + (ConvertTo-Json -Compress @{ action='skipped'; installedVersion=$installedVersion; expectedVersion='" + imageVersion + "'; expectedMinor='" + expectedMinor + "'; channel='" + channel + "'; sandboxId='" + sandboxId + "' }))",
133
+ "} else {",
134
+ " Write-Host \"Installed minor: $installedMinor, expected: " + expectedMinor + " — updating...\"",
135
+ " Write-Host 'Installing @testdriverai/runner@" + runnerTag + "...'",
136
+ ' npm install @testdriverai/runner@' + runnerTag + ' --omit=dev 2>&1 | Write-Host',
137
+ " $newVersion = ''",
138
+ " try { $newPkg = Get-Content 'node_modules/@testdriverai/runner/package.json' -Raw | ConvertFrom-Json; $newVersion = $newPkg.version } catch {}",
139
+ " Write-Host (\"RUNNER_VERSION_CHECK:\" + (ConvertTo-Json -Compress @{ action='updated'; previousVersion=$installedVersion; expectedVersion='" + imageVersion + "'; expectedMinor='" + expectedMinor + "'; newVersion=$newVersion; channel='" + channel + "'; sandboxId='" + sandboxId + "' }))",
140
+ " Write-Host 'Runner install complete'",
141
+ "}"
142
+ );
143
+ } else {
144
+ commands.push(
145
+ "Write-Host 'Installing @testdriverai/runner@" + runnerTag + "...'",
146
+ 'npm install @testdriverai/runner@' + runnerTag + ' --omit=dev 2>&1 | Write-Host',
147
+ "Write-Host 'Runner install complete'"
148
+ );
149
+ }
100
150
  }
101
151
 
102
152
  // ── 3. Regenerate run_testdriver.ps1 ──────────────────────────────
@@ -138,16 +188,23 @@ function windowsProvisionCommands({ channel, configJson, sandboxId, s3DownloadUr
138
188
  /**
139
189
  * Generate the bash command to install/update the runner on Linux (E2B).
140
190
  *
191
+ * When `imageVersion` is provided with the non-S3 (npm) path, the generated
192
+ * command checks the installed version first and skips the install when it
193
+ * matches — mirroring the Windows version-check behaviour.
194
+ *
141
195
  * @param {Object} opts
142
- * @param {string} opts.channel - Release channel
143
- * @param {string} [opts.s3DownloadUrl] - S3 pre-signed URL for dev/test
144
- * @param {string} [opts.runnerPath] - Default '/opt/testdriver-runner'
196
+ * @param {string} opts.channel - Release channel
197
+ * @param {string} [opts.s3DownloadUrl] - S3 pre-signed URL for dev/test
198
+ * @param {string} [opts.runnerPath] - Default '/opt/testdriver-runner'
199
+ * @param {string} [opts.imageVersion] - Version baked into the E2B template (skip install when it matches)
200
+ * @param {string} [opts.sandboxId] - For structured logging
145
201
  * @returns {string} Single bash command (steps joined with &&)
146
202
  */
147
- function linuxRunnerInstallCommand({ channel, s3DownloadUrl, runnerPath }) {
203
+ function linuxRunnerInstallCommand({ channel, s3DownloadUrl, runnerPath, imageVersion, sandboxId }) {
148
204
  var rp = runnerPath || '/opt/testdriver-runner';
149
205
  var useS3 = (channel === 'dev' || channel === 'test') && s3DownloadUrl;
150
206
  var runnerTag = channel === 'stable' ? 'latest' : channel;
207
+ var sid = sandboxId || 'unknown';
151
208
 
152
209
  if (useS3) {
153
210
  return [
@@ -162,6 +219,29 @@ function linuxRunnerInstallCommand({ channel, s3DownloadUrl, runnerPath }) {
162
219
  ].join(' && ');
163
220
  }
164
221
 
222
+ // If we know the image version, wrap the install in a version check.
223
+ // Only the major.minor portion is compared (patch & prerelease suffix are
224
+ // ignored) so trivial patch bumps don't trigger an unnecessary npm install.
225
+ if (imageVersion) {
226
+ var expectedMinor = majorMinor(imageVersion);
227
+ return [
228
+ 'INSTALLED_VERSION=$(node -p "try{require(\'' + rp + '/package.json\').version}catch(e){\'\'}" 2>/dev/null || echo "")',
229
+ 'INSTALLED_MINOR=$(echo "$INSTALLED_VERSION" | sed -E "s/^([0-9]+\\.[0-9]+).*/\\1/")',
230
+ 'if [ -n "' + expectedMinor + '" ] && [ "$INSTALLED_MINOR" = "' + expectedMinor + '" ]; then',
231
+ ' echo "RUNNER_VERSION_CHECK:{\\"action\\":\\"skipped\\",\\"installedVersion\\":\\"$INSTALLED_VERSION\\",\\"expectedVersion\\":\\"' + imageVersion + '\\",\\"expectedMinor\\":\\"' + expectedMinor + '\\",\\"channel\\":\\"' + channel + '\\",\\"sandboxId\\":\\"' + sid + '\\"}"',
232
+ ' echo "Runner already at v$INSTALLED_VERSION (minor ' + expectedMinor + '), skipping update"',
233
+ 'else',
234
+ ' echo "Installed minor: $INSTALLED_MINOR, expected: ' + expectedMinor + ' — updating..."',
235
+ ' sudo npm install -g @testdriverai/runner@' + runnerTag + ' --omit=dev --no-audit --no-fund --loglevel=error',
236
+ ' sudo rm -rf ' + rp,
237
+ ' sudo ln -sf $(npm root -g)/@testdriverai/runner ' + rp,
238
+ ' NEW_VERSION=$(node -p "try{require(\'' + rp + '/package.json\').version}catch(e){\'\'}" 2>/dev/null || echo "")',
239
+ ' echo "RUNNER_VERSION_CHECK:{\\"action\\":\\"updated\\",\\"previousVersion\\":\\"$INSTALLED_VERSION\\",\\"expectedVersion\\":\\"' + imageVersion + '\\",\\"expectedMinor\\":\\"' + expectedMinor + '\\",\\"newVersion\\":\\"$NEW_VERSION\\",\\"channel\\":\\"' + channel + '\\",\\"sandboxId\\":\\"' + sid + '\\"}"',
240
+ ' echo "Runner install complete"',
241
+ 'fi',
242
+ ].join(' && ');
243
+ }
244
+
165
245
  return [
166
246
  'sudo npm install -g @testdriverai/runner@' + runnerTag + ' --omit=dev --no-audit --no-fund --loglevel=error',
167
247
  'sudo rm -rf ' + rp,
@@ -173,4 +253,5 @@ module.exports = {
173
253
  buildAgentConfig,
174
254
  windowsProvisionCommands,
175
255
  linuxRunnerInstallCommand,
256
+ majorMinor,
176
257
  };