testdriverai 6.2.0 → 6.2.1

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 (64) hide show
  1. package/.github/workflows/acceptance-tests.yml +2 -0
  2. package/.github/workflows/acceptance-v6.yml +2 -0
  3. package/.github/workflows/lint.yml +4 -1
  4. package/.github/workflows/publish-canary.yml +2 -0
  5. package/.github/workflows/publish-latest.yml +1 -0
  6. package/.github/workflows/self-hosted.yml +102 -0
  7. package/.prettierignore +1 -0
  8. package/.vscode/settings.json +4 -1
  9. package/agent/events.js +1 -10
  10. package/agent/index.js +98 -55
  11. package/agent/interface.js +43 -6
  12. package/agent/lib/censorship.js +15 -10
  13. package/agent/lib/commander.js +31 -18
  14. package/agent/lib/commands.js +62 -17
  15. package/agent/lib/debugger-server.js +0 -5
  16. package/agent/lib/generator.js +2 -2
  17. package/agent/lib/sdk.js +2 -1
  18. package/agent/lib/source-mapper.js +1 -1
  19. package/debugger/index.html +1 -1
  20. package/docs/account/enterprise.mdx +8 -12
  21. package/docs/account/pricing.mdx +2 -2
  22. package/docs/account/projects.mdx +5 -0
  23. package/docs/apps/tauri-apps.mdx +361 -0
  24. package/docs/cli/overview.mdx +6 -6
  25. package/docs/commands/assert.mdx +1 -0
  26. package/docs/commands/hover-text.mdx +3 -1
  27. package/docs/commands/match-image.mdx +5 -4
  28. package/docs/commands/press-keys.mdx +6 -8
  29. package/docs/commands/scroll-until-image.mdx +8 -7
  30. package/docs/commands/scroll-until-text.mdx +7 -6
  31. package/docs/commands/wait-for-image.mdx +5 -4
  32. package/docs/commands/wait-for-text.mdx +6 -5
  33. package/docs/docs.json +42 -40
  34. package/docs/getting-started/playwright.mdx +342 -0
  35. package/docs/getting-started/self-hosting.mdx +370 -0
  36. package/docs/getting-started/vscode.mdx +67 -56
  37. package/docs/guide/dashcam.mdx +118 -0
  38. package/docs/guide/environment-variables.mdx +5 -5
  39. package/docs/images/content/self-hosted/launchtemplateid.png +0 -0
  40. package/docs/images/content/vscode/ide-full.png +0 -0
  41. package/docs/images/content/vscode/running.png +0 -0
  42. package/docs/overview/comparison.mdx +22 -39
  43. package/docs/overview/quickstart.mdx +84 -32
  44. package/docs/styles.css +10 -1
  45. package/interfaces/cli/commands/generate.js +3 -0
  46. package/interfaces/cli/lib/base.js +27 -5
  47. package/interfaces/cli/utils/factory.js +17 -4
  48. package/interfaces/logger.js +4 -4
  49. package/interfaces/readline.js +1 -1
  50. package/package.json +3 -3
  51. package/schema.json +21 -0
  52. package/setup/aws/cloudformation.yaml +463 -0
  53. package/setup/aws/spawn-runner.sh +190 -0
  54. package/testdriver/acceptance/hover-text.yaml +2 -1
  55. package/testdriver/acceptance/prompt.yaml +4 -1
  56. package/testdriver/acceptance/scroll-until-image.yaml +5 -0
  57. package/testdriver/edge-cases/js-exception.yaml +8 -0
  58. package/testdriver/edge-cases/js-promise.yaml +19 -0
  59. package/testdriver/edge-cases/lifecycle/postrun.yaml +10 -0
  60. package/testdriver/edge-cases/success-test.yaml +9 -0
  61. package/testdriver/examples/web/lifecycle/postrun.yaml +7 -0
  62. package/testdriver/examples/web/lifecycle/{provision.yaml → prerun.yaml} +6 -0
  63. package/testdriver/lifecycle/postrun.yaml +7 -0
  64. package/testdriver/lifecycle/prerun.yaml +17 -0
@@ -7,6 +7,8 @@ on:
7
7
  # So that we don't do expensive tests until approved
8
8
  push:
9
9
  branches: [main]
10
+ paths-ignore:
11
+ - "docs/**"
10
12
  # So that we can manually trigger tests when there's flake
11
13
  workflow_dispatch:
12
14
 
@@ -5,6 +5,8 @@ on:
5
5
  push:
6
6
  branches:
7
7
  - main
8
+ paths-ignore:
9
+ - "docs/**"
8
10
  pull_request:
9
11
  branches:
10
12
  - main
@@ -1,7 +1,10 @@
1
1
  # Ensure affected code follows standards and is formatted correctly. Otherwise, automatic formatting in future changes will cause larger diffs.
2
2
  name: Lint + Prettier
3
3
 
4
- on: push
4
+ on:
5
+ push:
6
+ paths-ignore:
7
+ - "docs/**"
5
8
 
6
9
  jobs:
7
10
  lint:
@@ -10,6 +10,8 @@ on:
10
10
  # So that we publish for every push to `main`, despite tests
11
11
  push:
12
12
  branches: [main]
13
+ paths-ignore:
14
+ - "docs/**"
13
15
  workflow_dispatch:
14
16
 
15
17
  jobs:
@@ -2,6 +2,7 @@
2
2
  name: Publish @latest to NPM
3
3
 
4
4
  on:
5
+ workflow_dispatch:
5
6
  workflow_run:
6
7
  workflows: ["Acceptance Tests"]
7
8
  branches: [main]
@@ -0,0 +1,102 @@
1
+ name: AWS
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ push:
6
+ paths-ignore:
7
+ - "docs/**"
8
+
9
+ jobs:
10
+ gather:
11
+ name: Gather Test Files
12
+ runs-on: ubuntu-latest
13
+ outputs:
14
+ test_files: ${{ steps.test_list.outputs.files }}
15
+ steps:
16
+ - name: Check out repository
17
+ uses: actions/checkout@v4
18
+
19
+ - name: Find all test files
20
+ id: test_list
21
+ run: |
22
+ FILES=$(ls ./testdriver/acceptance/*.yaml)
23
+ FILENAMES=$(basename -a $FILES)
24
+ FILES_JSON=$(echo "$FILENAMES" | jq -R -s -c 'split("\n")[:-1]')
25
+ echo "files=$FILES_JSON" >> $GITHUB_OUTPUT
26
+
27
+ test:
28
+ needs: gather
29
+ runs-on: ubuntu-latest
30
+ strategy:
31
+ matrix:
32
+ test: ${{ fromJson(needs.gather.outputs.test_files) }}
33
+ fail-fast: false
34
+ steps:
35
+ - name: Checkout repository
36
+ uses: actions/checkout@v4
37
+ with:
38
+ fetch-depth: 0
39
+ # only needed for `act`
40
+ # - name: Install AWS CLI
41
+ # run: |
42
+ # apt-get update
43
+ # apt-get install curl unzip -y
44
+ # curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
45
+ # unzip awscliv2.zip
46
+ # ./aws/install
47
+ - name: Set up Node.js
48
+ uses: actions/setup-node@v4
49
+ with:
50
+ node-version: "20"
51
+ cache: "npm"
52
+ - name: Install dependencies
53
+ run: NODE_ENV=production npm ci
54
+ - name: Setup AWS Instance
55
+ id: aws-setup
56
+ run: |
57
+ OUTPUT=$(./setup/aws/spawn-runner.sh | tee /dev/stderr) # Capture and display output
58
+ echo "$OUTPUT"
59
+ PUBLIC_IP=$(echo "$OUTPUT" | grep "PUBLIC_IP=" | cut -d'=' -f2)
60
+ INSTANCE_ID=$(echo "$OUTPUT" | grep "INSTANCE_ID=" | cut -d'=' -f2)
61
+ AWS_REGION=$(echo "$OUTPUT" | grep "AWS_REGION=" | cut -d'=' -f2)
62
+ echo "public-ip=$PUBLIC_IP" >> $GITHUB_OUTPUT
63
+ echo "instance-id=$INSTANCE_ID" >> $GITHUB_OUTPUT
64
+ echo "aws-region=$AWS_REGION" >> $GITHUB_OUTPUT
65
+ env:
66
+ FORCE_COLOR: 3
67
+ AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
68
+ AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
69
+ AWS_REGION: us-east-2
70
+ AWS_LAUNCH_TEMPLATE_ID: lt-00d02f31cfc602f27
71
+ AMI_ID: ami-085f872ca0cd80fed
72
+ RESOLUTION_WIDTH: 1920
73
+ RESOLUTION_HEIGHT: 1080
74
+ - name: Run TestDriver
75
+ run: node bin/testdriverai.js run testdriver/acceptance/${{ matrix.test }} --ip="${{ steps.aws-setup.outputs.public-ip }}" --junit=out.xml
76
+ env:
77
+ TD_API_KEY: ${{ secrets.TD_API_KEY }}
78
+ TD_WEBSITE: https://testdriver-sandbox.vercel.app
79
+ TD_THIS_FILE: ${{ matrix.test }}
80
+ - name: Upload TestDriver AI CLI logs
81
+ if: always()
82
+ uses: actions/upload-artifact@v4
83
+ with:
84
+ name: testdriverai-cli-logs-${{ matrix.test }}
85
+ path: /tmp/testdriverai-cli-*.log
86
+ if-no-files-found: warn
87
+ retention-days: 30
88
+ - name: Upload test results as artifact
89
+ if: always()
90
+ uses: actions/upload-artifact@v4
91
+ with:
92
+ name: test-results-${{ matrix.test }}
93
+ path: out.xml
94
+ retention-days: 30
95
+ - name: Shutdown AWS Instance
96
+ if: always()
97
+ run: aws ec2 terminate-instances --region "$AWS_REGION" --instance-ids "$INSTANCE_ID"
98
+ env:
99
+ AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
100
+ AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
101
+ AWS_REGION: ${{ steps.aws-setup.outputs.aws-region }}
102
+ INSTANCE_ID: ${{ steps.aws-setup.outputs.instance-id }}
package/.prettierignore CHANGED
@@ -1,3 +1,4 @@
1
1
  agent/lib/subimage/opencv.js
2
2
  node_modules
3
3
  schema.json
4
+ docs
@@ -3,5 +3,8 @@
3
3
  "source.organizeImports": "explicit"
4
4
  },
5
5
  "editor.formatOnSave": true,
6
- "editor.defaultFormatter": "esbenp.prettier-vscode"
6
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
7
+ "yaml.schemas": {
8
+ "https://raw.githubusercontent.com/testdriverai/testdriverai/main/schema.json": "file:///Users/kid/Desktop/td/internal/testdriverai/testdriver.yaml"
9
+ }
7
10
  }
package/agent/events.js CHANGED
@@ -1,5 +1,4 @@
1
1
  const { EventEmitter2 } = require("eventemitter2");
2
- const { censorSensitiveDataDeep } = require("./lib/censorship");
3
2
 
4
3
  // Factory function to create a new emitter instance with censoring middleware
5
4
  const createEmitter = () => {
@@ -13,14 +12,6 @@ const createEmitter = () => {
13
12
  ignoreErrors: false,
14
13
  });
15
14
 
16
- // Override emit to censor sensitive data before emitting
17
- const originalEmit = emitter.emit.bind(emitter);
18
- emitter.emit = function (event, ...args) {
19
- // Censor all arguments passed to emit
20
- const censoredArgs = args.map(censorSensitiveDataDeep);
21
- return originalEmit(event, ...censoredArgs);
22
- };
23
-
24
15
  return emitter;
25
16
  };
26
17
 
@@ -46,7 +37,7 @@ const events = {
46
37
  status: "status",
47
38
  log: {
48
39
  markdown: {
49
- static: "log:markdown:static",
40
+ static: "log:markdown",
50
41
  start: "log:markdown:start",
51
42
  chunk: "log:markdown:chunk",
52
43
  end: "log:markdown:end",
package/agent/index.js CHANGED
@@ -63,13 +63,18 @@ class TestDriverAgent extends EventEmitter2 {
63
63
  // Derive properties from cliArgs
64
64
  const flags = cliArgs.options || {};
65
65
  const firstArg = cliArgs.args && cliArgs.args[0];
66
+
67
+ // All commands (run, edit, generate) use the same pattern:
68
+ // first argument is the main file to work with
66
69
  this.thisFile = firstArg || this.config.TD_DEFAULT_TEST_FILE;
70
+
67
71
  this.resultFile = flags.resultFile || null;
68
72
  this.newSandbox = flags.newSandbox || false;
69
73
  this.healMode = flags.healMode || flags.heal || false;
70
74
  this.sandboxId = flags["sandbox-id"] || null;
71
75
  this.sandboxAmi = flags["sandbox-ami"] || null;
72
76
  this.sandboxInstance = flags["sandbox-instance"] || null;
77
+ this.ip = flags.ip || null;
73
78
  this.workingDir = flags.workingDir || process.cwd();
74
79
 
75
80
  // Resolve thisFile to absolute path with proper extension
@@ -222,7 +227,15 @@ class TestDriverAgent extends EventEmitter2 {
222
227
  if (skipPostrun) {
223
228
  this.exit(true);
224
229
  } else {
225
- await this.summarize(error.message);
230
+ try {
231
+ await this.summarize(error.message);
232
+ } catch (summarizeError) {
233
+ // If summarization fails, log it but don't let it prevent postrun from running
234
+ this.emitter.emit(
235
+ events.log.warn,
236
+ theme.yellow(`Failed to summarize: ${summarizeError.message}`),
237
+ );
238
+ }
226
239
  // Always run postrun lifecycle script, even for fatal errors
227
240
  return await this.exit(true, false, true);
228
241
  }
@@ -419,6 +432,7 @@ class TestDriverAgent extends EventEmitter2 {
419
432
 
420
433
  // Log current execution position for debugging
421
434
  if (this.sourceMapper.currentFileSourceMap) {
435
+ this.emitter.emit(events.log.log, "");
422
436
  this.emitter.emit(
423
437
  events.log.log,
424
438
  theme.dim(`${this.sourceMapper.getCurrentPositionDescription()}`),
@@ -476,14 +490,13 @@ class TestDriverAgent extends EventEmitter2 {
476
490
  sourcePosition: sourcePosition,
477
491
  });
478
492
 
479
- await this.haveAIResolveError(
493
+ return await this.haveAIResolveError(
480
494
  error,
481
495
  yaml.dump({ commands: [yml] }),
482
496
  depth,
483
497
  true,
484
498
  shouldSave,
485
499
  );
486
- throw error;
487
500
  }
488
501
  }
489
502
 
@@ -879,30 +892,33 @@ commands:
879
892
  // based on the current state of the system (primarily the current screenshot)
880
893
  // it will generate files that contain only "prompts"
881
894
  // @todo revit the generate command
882
- async generate(type, count, baseYaml, skipYaml = false) {
883
- this.emitter.emit(events.log.debug, "generate called, %s", type);
895
+ async generate(count = 1, prompt = null) {
896
+ this.emitter.emit(
897
+ events.log.debug,
898
+ `generate called with count: ${count}, prompt: ${prompt}`,
899
+ );
884
900
 
885
- this.emitter.emit(events.log.narration, theme.dim("thinking..."), true);
901
+ await this.runLifecycle("prerun");
886
902
 
887
- if (baseYaml && !skipYaml) {
888
- await this.runLifecycle("prerun");
889
- await this.run(baseYaml, false, false);
890
- await this.runLifecycle("postrun");
891
- }
903
+ this.emitter.emit(events.log.narration, theme.dim("thinking..."), true);
892
904
 
893
905
  let image = await this.system.captureScreenBase64();
894
906
 
895
907
  const streamId = `generate-${Date.now()}`;
896
908
  this.emitter.emit(events.log.markdown.start, streamId);
897
909
 
910
+ let mouse = await this.system.getMousePosition();
911
+ let activeWindow = await this.system.activeWin();
912
+
898
913
  let message = await this.sdk.req(
899
914
  "generate",
900
915
  {
901
- type,
916
+ prompt: prompt || "make sure to do a spellcheck",
902
917
  image,
903
- mousePosition: await this.system.getMousePosition(),
904
- activeWindow: await this.system.activeWin(),
918
+ mousePosition: mouse,
919
+ activeWindow: activeWindow,
905
920
  count,
921
+ stream: false,
906
922
  },
907
923
  (chunk) => {
908
924
  if (chunk.type === "data") {
@@ -925,35 +941,36 @@ commands:
925
941
  .replace(/['"`]/g, "")
926
942
  .replace(/[^a-zA-Z0-9-]/g, "") // remove any non-alphanumeric chars except hyphens
927
943
  .toLowerCase() + ".yaml";
944
+
928
945
  let path1 = path.join(
929
946
  this.workingDir,
930
947
  "testdriver",
931
948
  "generate",
932
949
  fileName,
933
950
  );
934
-
935
951
  // create generate directory if it doesn't exist
936
- if (!fs.existsSync(path.join(this.workingDir, "generate"))) {
937
- fs.mkdirSync(path.join(this.workingDir, "generate"));
952
+ const generateDir = path.join(this.workingDir, "testdriver", "generate");
953
+ if (!fs.existsSync(generateDir)) {
954
+ fs.mkdirSync(generateDir);
955
+ console.log("Created generate directory:", generateDir);
956
+ } else {
957
+ console.log("Generate directory already exists:", generateDir);
938
958
  }
939
959
 
940
960
  let list = testPrompt.steps;
941
961
 
942
- if (baseYaml && fs.existsSync(baseYaml)) {
943
- list.unshift({
944
- step: {
945
- command: "run",
946
- file: baseYaml,
947
- },
948
- });
949
- }
950
962
  let contents = yaml.dump({
951
963
  version: packageJson.version,
952
964
  steps: list,
953
965
  });
966
+
967
+ this.emitter.emit(events.log.debug, `writing file ${path1} ${contents}`);
968
+
954
969
  fs.writeFileSync(path1, contents);
955
970
  }
956
971
 
972
+ await this.runLifecycle("postrun");
973
+
957
974
  this.exit(false);
958
975
  }
959
976
 
@@ -1139,21 +1156,8 @@ ${yml}
1139
1156
 
1140
1157
  // Create diff if file exists and content has changed
1141
1158
  let diffResult = null;
1142
- console.log("Checking for diff. File exists:", fileExists);
1143
- console.log(
1144
- "Content changed:",
1145
- fileExists && existingContent !== regression,
1146
- );
1147
- if (fileExists) {
1148
- console.log(
1149
- "Existing content preview:",
1150
- existingContent.substring(0, 100),
1151
- );
1152
- console.log("New content preview:", regression.substring(0, 100));
1153
- }
1154
1159
 
1155
1160
  if (fileExists && existingContent !== regression) {
1156
- console.log("Creating diff - content has changed");
1157
1161
  const patches = diff.structuredPatch(
1158
1162
  filepath,
1159
1163
  filepath,
@@ -1241,8 +1245,6 @@ ${yml}
1241
1245
  diff: diffResult,
1242
1246
  timestamp: endTime,
1243
1247
  });
1244
- } else {
1245
- console.log("No diff result to emit");
1246
1248
  }
1247
1249
 
1248
1250
  // Emit file save completion event
@@ -1519,6 +1521,8 @@ ${regression}
1519
1521
  }
1520
1522
 
1521
1523
  async embed(file, depth, pushToHistory) {
1524
+ let inputFile = JSON.parse(JSON.stringify(file));
1525
+
1522
1526
  this.analytics.track("embed", { file });
1523
1527
 
1524
1528
  this.emitter.emit(
@@ -1528,7 +1532,7 @@ ${regression}
1528
1532
 
1529
1533
  depth = depth + 1;
1530
1534
 
1531
- this.emitter.emit(events.log.log, `${file} (start)`);
1535
+ this.emitter.emit(events.log.log, `${inputFile} (start)`);
1532
1536
 
1533
1537
  // Use the new helper method to resolve file paths relative to testdriver directory
1534
1538
  const currentFilePath = this.sourceMapper.currentFilePath || this.thisFile;
@@ -1581,7 +1585,7 @@ ${regression}
1581
1585
  this.sourceMapper.restoreContext(previousContext);
1582
1586
  }
1583
1587
 
1584
- this.emitter.emit(events.log.log, `${file} (end)`);
1588
+ this.emitter.emit(events.log.log, `${inputFile} (end)`);
1585
1589
  }
1586
1590
 
1587
1591
  // Returns sandboxId to use (either from file if recent, or null)
@@ -1706,7 +1710,20 @@ ${regression}
1706
1710
  const recentId = createNew ? null : this.getRecentSandboxId();
1707
1711
 
1708
1712
  // Set sandbox ID for reconnection (only if not creating new and recent ID exists)
1709
- if (!createNew && recentId) {
1713
+ if (this.ip) {
1714
+ let instance = await this.sandbox.send({
1715
+ type: "direct",
1716
+ resolution: this.config.TD_RESOLUTION,
1717
+ ci: this.config.CI,
1718
+ ip: this.ip,
1719
+ });
1720
+
1721
+ await this.renderSandbox(instance.instance, headless);
1722
+ await this.newSession();
1723
+ await this.runLifecycle("provision");
1724
+
1725
+ return;
1726
+ } else if (!createNew && recentId) {
1710
1727
  this.emitter.emit(
1711
1728
  events.log.narration,
1712
1729
  theme.dim(`using recent sandbox: ${recentId}`),
@@ -1717,10 +1734,8 @@ ${regression}
1717
1734
  events.log.narration,
1718
1735
  theme.dim(`no recent sandbox found, creating a new one.`),
1719
1736
  );
1720
- }
1721
-
1722
- // Only attempt to connect to existing sandbox if not in CI mode and not creating new
1723
- if (this.sandboxId && !this.config.CI && !createNew) {
1737
+ } else if (this.sandboxId && !this.config.CI) {
1738
+ // Only attempt to connect to existing sandbox if not in CI mode and not creating new
1724
1739
  // Attempt to connect to known instance
1725
1740
  this.emitter.emit(
1726
1741
  events.log.narration,
@@ -1768,7 +1783,7 @@ ${regression}
1768
1783
  url: newSandbox.url,
1769
1784
  };
1770
1785
 
1771
- const encodedData = encodeURIComponent(JSON.stringify(data));
1786
+ const encodedData = Buffer.from(JSON.stringify(data)).toString('base64');
1772
1787
 
1773
1788
  // Use the debugger URL instead of the VNC URL
1774
1789
  const urlToOpen = `${this.debuggerUrl}?data=${encodedData}`;
@@ -1787,13 +1802,16 @@ ${regression}
1787
1802
  events.log.log,
1788
1803
  theme.green(`Howdy! I'm TestDriver v${packageJson.version}`),
1789
1804
  );
1805
+
1806
+ // Emit test start event for the entire test execution
1807
+ this.emitter.emit(events.test.start, {
1808
+ filePath: this.thisFile,
1809
+ timestamp: Date.now(),
1810
+ });
1811
+
1790
1812
  // Start the debugger server as early as possible to ensure event listeners are attached
1791
1813
  if (!debuggerStarted) {
1792
1814
  debuggerStarted = true; // Prevent multiple starts, especially when running test in parallel
1793
- this.emitter.emit(
1794
- events.log.narration,
1795
- theme.green(`Starting debugger server...`),
1796
- );
1797
1815
  debuggerProcess = await createDebuggerProcess(
1798
1816
  this.config,
1799
1817
  this.emitter,
@@ -1801,6 +1819,7 @@ ${regression}
1801
1819
  }
1802
1820
  this.debuggerUrl = debuggerProcess.url || null; // Store the debugger URL
1803
1821
  this.emitter.emit(events.log.log, `This is beta software!`);
1822
+ this.emitter.emit(events.log.log, ``);
1804
1823
  this.emitter.emit(
1805
1824
  events.log.log,
1806
1825
  theme.yellow(`Join our Discord for help`),
@@ -1809,6 +1828,7 @@ ${regression}
1809
1828
  events.log.log,
1810
1829
  `https://discord.com/invite/cWDFW8DzPm`,
1811
1830
  );
1831
+ this.emitter.emit(events.log.log, ``);
1812
1832
 
1813
1833
  // make testdriver directory if it doesn't exist
1814
1834
  let testdriverFolder = path.join(this.workingDir);
@@ -1822,7 +1842,10 @@ ${regression}
1822
1842
  }
1823
1843
 
1824
1844
  // if the directory for thisFile doesn't exist, create it
1825
- if (this.cliArgs.command !== "sandbox") {
1845
+ if (
1846
+ this.cliArgs.command !== "sandbox" &&
1847
+ this.cliArgs.command !== "generate"
1848
+ ) {
1826
1849
  const dir = path.dirname(this.thisFile);
1827
1850
  if (!fs.existsSync(dir)) {
1828
1851
  fs.mkdirSync(dir, { recursive: true });
@@ -1847,7 +1870,10 @@ ${regression}
1847
1870
  await this.sdk.auth();
1848
1871
  }
1849
1872
 
1850
- if (this.cliArgs.command !== "sandbox") {
1873
+ if (
1874
+ this.cliArgs.command !== "sandbox" &&
1875
+ this.cliArgs.command !== "generate"
1876
+ ) {
1851
1877
  this.emitter.emit(
1852
1878
  events.log.log,
1853
1879
  theme.dim(`Working on ${this.thisFile}`),
@@ -2050,6 +2076,20 @@ Please check your network connection, TD_API_KEY, or the service status.`,
2050
2076
  // Use the current file path from sourceMapper to find the lifecycle directory
2051
2077
  // If sourceMapper doesn't have a current file, use thisFile which should be the file being run
2052
2078
  let currentFilePath = this.sourceMapper.currentFilePath || this.thisFile;
2079
+
2080
+ this.emitter.emit(events.log.log, ``);
2081
+ this.emitter.emit(events.log.log, "Running lifecycle: " + lifecycleName);
2082
+
2083
+ // If we still don't have a currentFilePath, fall back to the default testdriver directory
2084
+ if (!currentFilePath) {
2085
+ currentFilePath = path.join(
2086
+ this.workingDir,
2087
+ "testdriver",
2088
+ "testdriver.yaml",
2089
+ );
2090
+ console.log("No currentFilePath found, using fallback:", currentFilePath);
2091
+ }
2092
+
2053
2093
  // Ensure we have an absolute path
2054
2094
  if (currentFilePath && !path.isAbsolute(currentFilePath)) {
2055
2095
  currentFilePath = path.resolve(this.workingDir, currentFilePath);
@@ -2086,6 +2126,9 @@ Please check your network connection, TD_API_KEY, or the service status.`,
2086
2126
  }
2087
2127
  }
2088
2128
  }
2129
+
2130
+ this.emitter.emit(events.log.log, lifecycleFile);
2131
+
2089
2132
  if (lifecycleFile) {
2090
2133
  // Store current source mapping state before running lifecycle file
2091
2134
  const previousContext = this.sourceMapper.saveContext();
@@ -2155,7 +2198,7 @@ Please check your network connection, TD_API_KEY, or the service status.`,
2155
2198
  }
2156
2199
 
2157
2200
  // Move environment setup and special handling here
2158
- if (["edit", "run"].includes(commandName)) {
2201
+ if (["edit", "run", "generate"].includes(commandName)) {
2159
2202
  await this.buildEnv(options);
2160
2203
  }
2161
2204
 
@@ -55,6 +55,10 @@ function createCommandDefinitions(agent) {
55
55
  "sandbox-instance": Flags.string({
56
56
  description: "Specify EC2 instance type for sandbox (e.g., i3.metal)",
57
57
  }),
58
+ ip: Flags.string({
59
+ description:
60
+ "Connect directly to a sandbox at the specified IP address",
61
+ }),
58
62
  summary: Flags.string({
59
63
  description: "Specify output file for summarize results",
60
64
  }),
@@ -68,12 +72,6 @@ function createCommandDefinitions(agent) {
68
72
  const file = normalizeFilePath(args.file);
69
73
  const testStartTime = Date.now();
70
74
 
71
- // Emit test start event for the entire test execution
72
- agent.emitter.emit(events.test.start, {
73
- filePath: file,
74
- timestamp: testStartTime,
75
- });
76
-
77
75
  try {
78
76
  await agent.runLifecycle("prerun");
79
77
  // When run() is called through run.js CLI command, shouldExit should be true
@@ -135,6 +133,10 @@ function createCommandDefinitions(agent) {
135
133
  "sandbox-instance": Flags.string({
136
134
  description: "Specify EC2 instance type for sandbox (e.g., i3.metal)",
137
135
  }),
136
+ ip: Flags.string({
137
+ description:
138
+ "Connect directly to a sandbox at the specified IP address",
139
+ }),
138
140
  summary: Flags.string({
139
141
  description: "Specify output file for summarize results",
140
142
  }),
@@ -202,6 +204,41 @@ function createCommandDefinitions(agent) {
202
204
  console.log(`TestDriver.ai v${packageJson.version}`);
203
205
  },
204
206
  },
207
+
208
+ generate: {
209
+ description: "Generate test files based on current screen state",
210
+ args: {
211
+ prompt: Args.string({
212
+ description: "Multi-line text prompt describing what to generate",
213
+ required: false,
214
+ }),
215
+ },
216
+ flags: {
217
+ count: Flags.integer({
218
+ description: "Number of test files to generate",
219
+ default: 3,
220
+ }),
221
+ headless: Flags.boolean({
222
+ description: "Run in headless mode (no GUI)",
223
+ default: false,
224
+ }),
225
+ new: Flags.boolean({
226
+ description:
227
+ "Create a new sandbox instead of reconnecting to an existing one",
228
+ default: false,
229
+ }),
230
+ "sandbox-ami": Flags.string({
231
+ description: "Specify AMI ID for sandbox instance (e.g., ami-1234)",
232
+ }),
233
+ "sandbox-instance": Flags.string({
234
+ description: "Specify EC2 instance type for sandbox (e.g., i3.metal)",
235
+ }),
236
+ },
237
+ handler: async (args, flags) => {
238
+ // Call generate with the count and prompt
239
+ await agent.generate(flags.count || 3, args.prompt);
240
+ },
241
+ },
205
242
  };
206
243
  }
207
244
 
@@ -38,18 +38,23 @@ const censorSensitiveData = (message) => {
38
38
 
39
39
  // Function to censor sensitive data in any value (recursive for objects/arrays)
40
40
  const censorSensitiveDataDeep = (value) => {
41
- if (typeof value === "string") {
42
- return censorSensitiveData(value);
43
- } else if (Array.isArray(value)) {
44
- return value.map(censorSensitiveDataDeep);
45
- } else if (value && typeof value === "object") {
46
- const result = {};
47
- for (const [key, val] of Object.entries(value)) {
48
- result[key] = censorSensitiveDataDeep(val);
41
+ try {
42
+ if (typeof value === "string") {
43
+ return censorSensitiveData(value);
44
+ } else if (Array.isArray(value)) {
45
+ return value.map(censorSensitiveDataDeep);
46
+ } else if (value && typeof value === "object") {
47
+ const result = {};
48
+ for (const [key, val] of Object.entries(value)) {
49
+ result[key] = censorSensitiveDataDeep(val);
50
+ }
51
+ return result;
49
52
  }
50
- return result;
53
+ return value;
54
+ } catch {
55
+ // If we hit any error (like circular reference), just return a safe placeholder
56
+ return "[Object]";
51
57
  }
52
- return value;
53
58
  };
54
59
 
55
60
  // Function to update interpolation variables (for runtime updates)