testdriverai 6.0.28 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/agent/index.js CHANGED
@@ -63,7 +63,11 @@ 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;
@@ -87,6 +91,8 @@ class TestDriverAgent extends EventEmitter2 {
87
91
  }
88
92
  }
89
93
 
94
+
95
+
90
96
  // Create parser instance with this agent's emitter
91
97
  this.parser = createParser(this.emitter);
92
98
 
@@ -887,30 +893,33 @@ commands:
887
893
  // based on the current state of the system (primarily the current screenshot)
888
894
  // it will generate files that contain only "prompts"
889
895
  // @todo revit the generate command
890
- async generate(type, count, baseYaml, skipYaml = false) {
891
- this.emitter.emit(events.log.debug, "generate called, %s", type);
896
+ async generate(count = 1) {
892
897
 
893
- this.emitter.emit(events.log.narration, theme.dim("thinking..."), true);
898
+ console.log('generate being called with count:', count)
894
899
 
895
- if (baseYaml && !skipYaml) {
896
- await this.runLifecycle("prerun");
897
- await this.run(baseYaml, false, false);
898
- await this.runLifecycle("postrun");
899
- }
900
+ this.emitter.emit(events.log.debug, `generate called with count: ${count}`);
901
+
902
+ await this.runLifecycle("prerun");
903
+
904
+ this.emitter.emit(events.log.narration, theme.dim("thinking..."), true);
900
905
 
901
906
  let image = await this.system.captureScreenBase64();
902
907
 
903
908
  const streamId = `generate-${Date.now()}`;
904
909
  this.emitter.emit(events.log.markdown.start, streamId);
905
910
 
911
+ let mouse = await this.system.getMousePosition();
912
+ let activeWindow = await this.system.activeWin();
913
+
906
914
  let message = await this.sdk.req(
907
915
  "generate",
908
916
  {
909
- type,
917
+ prompt: 'make sure to do a spellcheck',
910
918
  image,
911
- mousePosition: await this.system.getMousePosition(),
912
- activeWindow: await this.system.activeWin(),
919
+ mousePosition: mouse,
920
+ activeWindow: activeWindow,
913
921
  count,
922
+ stream: false
914
923
  },
915
924
  (chunk) => {
916
925
  if (chunk.type === "data") {
@@ -923,6 +932,8 @@ commands:
923
932
 
924
933
  let testPrompts = await this.parser.findGenerativePrompts(message.data);
925
934
 
935
+ console.log(testPrompts)
936
+
926
937
  // for each testPrompt
927
938
  for (const testPrompt of testPrompts) {
928
939
  // with the contents of the testPrompt
@@ -933,35 +944,37 @@ commands:
933
944
  .replace(/['"`]/g, "")
934
945
  .replace(/[^a-zA-Z0-9-]/g, "") // remove any non-alphanumeric chars except hyphens
935
946
  .toLowerCase() + ".yaml";
947
+
936
948
  let path1 = path.join(
937
949
  this.workingDir,
938
950
  "testdriver",
939
951
  "generate",
940
952
  fileName,
941
953
  );
942
-
943
954
  // create generate directory if it doesn't exist
944
- if (!fs.existsSync(path.join(this.workingDir, "generate"))) {
945
- fs.mkdirSync(path.join(this.workingDir, "generate"));
955
+ const generateDir = path.join(this.workingDir, "testdriver", "generate");
956
+ if (!fs.existsSync(generateDir)) {
957
+ fs.mkdirSync(generateDir);
958
+ console.log('Created generate directory:', generateDir);
959
+ } else {
960
+ console.log('Generate directory already exists:', generateDir);
946
961
  }
947
962
 
948
963
  let list = testPrompt.steps;
949
964
 
950
- if (baseYaml && fs.existsSync(baseYaml)) {
951
- list.unshift({
952
- step: {
953
- command: "run",
954
- file: baseYaml,
955
- },
956
- });
957
- }
958
965
  let contents = yaml.dump({
959
966
  version: packageJson.version,
960
967
  steps: list,
961
968
  });
969
+
970
+
971
+ console.log('writing file', path1, contents)
972
+
962
973
  fs.writeFileSync(path1, contents);
963
974
  }
964
975
 
976
+ await this.runLifecycle("postrun");
977
+
965
978
  this.exit(false);
966
979
  }
967
980
 
@@ -1813,7 +1826,7 @@ ${regression}
1813
1826
  }
1814
1827
 
1815
1828
  // if the directory for thisFile doesn't exist, create it
1816
- if (this.cliArgs.command !== "sandbox") {
1829
+ if (this.cliArgs.command !== "sandbox" && this.cliArgs.command !== "generate") {
1817
1830
  const dir = path.dirname(this.thisFile);
1818
1831
  if (!fs.existsSync(dir)) {
1819
1832
  fs.mkdirSync(dir, { recursive: true });
@@ -1838,7 +1851,7 @@ ${regression}
1838
1851
  await this.sdk.auth();
1839
1852
  }
1840
1853
 
1841
- if (this.cliArgs.command !== "sandbox") {
1854
+ if (this.cliArgs.command !== "sandbox" && this.cliArgs.command !== "generate") {
1842
1855
  this.emitter.emit(
1843
1856
  events.log.log,
1844
1857
  theme.dim(`Working on ${this.thisFile}`),
@@ -2032,9 +2045,21 @@ Please check your network connection, TD_API_KEY, or the service status.`,
2032
2045
  }
2033
2046
 
2034
2047
  async runLifecycle(lifecycleName) {
2048
+
2049
+ console.log("Running lifecycle:", lifecycleName);
2050
+
2035
2051
  // Use the current file path from sourceMapper to find the lifecycle directory
2036
2052
  // If sourceMapper doesn't have a current file, use thisFile which should be the file being run
2037
2053
  let currentFilePath = this.sourceMapper.currentFilePath || this.thisFile;
2054
+
2055
+ console.log("Current file path:", currentFilePath);
2056
+
2057
+ // If we still don't have a currentFilePath, fall back to the default testdriver directory
2058
+ if (!currentFilePath) {
2059
+ currentFilePath = path.join(this.workingDir, "testdriver", "testdriver.yaml");
2060
+ console.log("No currentFilePath found, using fallback:", currentFilePath);
2061
+ }
2062
+
2038
2063
  // Ensure we have an absolute path
2039
2064
  if (currentFilePath && !path.isAbsolute(currentFilePath)) {
2040
2065
  currentFilePath = path.resolve(this.workingDir, currentFilePath);
@@ -2071,6 +2096,9 @@ Please check your network connection, TD_API_KEY, or the service status.`,
2071
2096
  }
2072
2097
  }
2073
2098
  }
2099
+
2100
+ console.log(lifecycleFile)
2101
+
2074
2102
  if (lifecycleFile) {
2075
2103
  // Store current source mapping state before running lifecycle file
2076
2104
  const previousContext = this.sourceMapper.saveContext();
@@ -2140,7 +2168,7 @@ Please check your network connection, TD_API_KEY, or the service status.`,
2140
2168
  }
2141
2169
 
2142
2170
  // Move environment setup and special handling here
2143
- if (["edit", "run"].includes(commandName)) {
2171
+ if (["edit", "run", "generate"].includes(commandName)) {
2144
2172
  await this.buildEnv(options);
2145
2173
  }
2146
2174
 
@@ -196,6 +196,42 @@ function createCommandDefinitions(agent) {
196
196
  console.log(`TestDriver.ai v${packageJson.version}`);
197
197
  },
198
198
  },
199
+
200
+ generate: {
201
+ description: "Generate test files based on current screen state",
202
+ args: {
203
+ file: Args.string({
204
+ description: "Base test file to run before generating (optional)",
205
+ required: false,
206
+ }),
207
+ },
208
+ flags: {
209
+ count: Flags.integer({
210
+ description: "Number of test files to generate",
211
+ default: 3,
212
+ }),
213
+ headless: Flags.boolean({
214
+ description: "Run in headless mode (no GUI)",
215
+ default: false,
216
+ }),
217
+ new: Flags.boolean({
218
+ description:
219
+ "Create a new sandbox instead of reconnecting to an existing one",
220
+ default: false,
221
+ }),
222
+ "sandbox-ami": Flags.string({
223
+ description: "Specify AMI ID for sandbox instance (e.g., ami-1234)",
224
+ }),
225
+ "sandbox-instance": Flags.string({
226
+ description: "Specify EC2 instance type for sandbox (e.g., i3.metal)",
227
+ }),
228
+ },
229
+ handler: async (args, flags) => {
230
+ // The file argument is already handled by thisFile in the agent constructor
231
+ // Just call generate with the count
232
+ await agent.generate(flags.count || 3);
233
+ },
234
+ },
199
235
  };
200
236
  }
201
237
 
@@ -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)
@@ -1,6 +1,6 @@
1
1
  // parses markdown content to find code blocks, and then extracts yaml from those code blocks
2
2
  const yaml = require("js-yaml");
3
- const package = require("../../package.json");
3
+ const pkg = require("../../package.json");
4
4
  const session = require("./session");
5
5
  const theme = require("./theme");
6
6
  // do the actual parsing
@@ -56,7 +56,7 @@ const jsonToManual = function (json, colors = true) {
56
56
  const dumpToYML = async function (inputArray, sessionInstance = null) {
57
57
  // use yml dump to convert json to yml
58
58
  let yml = await yaml.dump({
59
- version: package.version,
59
+ version: pkg.version,
60
60
  session: sessionInstance ? sessionInstance.get() : session.get(),
61
61
  steps: inputArray,
62
62
  });
@@ -23,7 +23,7 @@ const createSandbox = (emitter, analytics) => {
23
23
  if (this.socket) {
24
24
  this.messageId++;
25
25
  message.requestId = `${this.uniqueId}-${this.messageId}`;
26
-
26
+
27
27
  // Start timing for this message
28
28
  const timingKey = `sandbox-${message.type}`;
29
29
  marky.mark(timingKey);
package/agent/lib/sdk.js CHANGED
@@ -95,7 +95,7 @@ const createSDK = (emitter, config, sessionInstance) => {
95
95
  return token;
96
96
  } catch (error) {
97
97
  outputError(error);
98
- return;
98
+ throw error; // Re-throw the error so calling code can handle it properly
99
99
  }
100
100
  }
101
101
  };
@@ -194,6 +194,7 @@ const createSDK = (emitter, config, sessionInstance) => {
194
194
  return value;
195
195
  } catch (error) {
196
196
  outputError(error);
197
+ throw error; // Re-throw the error so calling code can handle it properly
197
198
  }
198
199
  };
199
200
 
package/index.html ADDED
@@ -0,0 +1,147 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Login</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
16
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
+ min-height: 100vh;
18
+ display: flex;
19
+ align-items: center;
20
+ justify-content: center;
21
+ }
22
+
23
+ .login-container {
24
+ background: white;
25
+ padding: 2rem;
26
+ border-radius: 10px;
27
+ box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
28
+ width: 100%;
29
+ max-width: 400px;
30
+ }
31
+
32
+ .login-header {
33
+ text-align: center;
34
+ margin-bottom: 2rem;
35
+ }
36
+
37
+ .login-header h1 {
38
+ color: #333;
39
+ font-size: 1.8rem;
40
+ margin-bottom: 0.5rem;
41
+ }
42
+
43
+ .login-header p {
44
+ color: #666;
45
+ font-size: 0.9rem;
46
+ }
47
+
48
+ .form-group {
49
+ margin-bottom: 1.5rem;
50
+ }
51
+
52
+ .form-group label {
53
+ display: block;
54
+ margin-bottom: 0.5rem;
55
+ color: #333;
56
+ font-weight: 500;
57
+ }
58
+
59
+ .form-group input {
60
+ width: 100%;
61
+ padding: 0.75rem;
62
+ border: 2px solid #e1e5e9;
63
+ border-radius: 5px;
64
+ font-size: 1rem;
65
+ transition: border-color 0.3s;
66
+ }
67
+
68
+ .form-group input:focus {
69
+ outline: none;
70
+ border-color: #667eea;
71
+ }
72
+
73
+ .login-btn {
74
+ width: 100%;
75
+ padding: 0.75rem;
76
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
77
+ color: white;
78
+ border: none;
79
+ border-radius: 5px;
80
+ font-size: 1rem;
81
+ font-weight: 500;
82
+ cursor: pointer;
83
+ transition: transform 0.2s;
84
+ }
85
+
86
+ .login-btn:hover {
87
+ transform: translateY(-2px);
88
+ }
89
+
90
+ .forgot-password {
91
+ text-align: center;
92
+ margin-top: 1rem;
93
+ }
94
+
95
+ .forgot-password a {
96
+ color: #667eea;
97
+ text-decoration: none;
98
+ font-size: 0.9rem;
99
+ }
100
+
101
+ .forgot-password a:hover {
102
+ text-decoration: underline;
103
+ }
104
+ </style>
105
+ </head>
106
+ <body>
107
+ <div class="login-container">
108
+ <div class="login-header">
109
+ <h1>Welcome Back</h1>
110
+ <p>Please sign in to your account</p>
111
+ </div>
112
+
113
+ <form id="loginForm">
114
+ <div class="form-group">
115
+ <label for="email">Email Address</label>
116
+ <input type="email" id="email" name="email" required>
117
+ </div>
118
+
119
+ <div class="form-group">
120
+ <label for="password">Password</label>
121
+ <input type="password" id="password" name="password" required>
122
+ </div>
123
+
124
+ <button type="submit" class="login-btn">Sign In</button>
125
+ </form>
126
+
127
+ <div class="forgot-password">
128
+ <a href="#" onclick="alert('Password reset functionality would go here')">Forgot your password?</a>
129
+ </div>
130
+ </div>
131
+
132
+ <script>
133
+ document.getElementById('loginForm').addEventListener('submit', function(e) {
134
+ e.preventDefault();
135
+
136
+ const email = document.getElementById('email').value;
137
+ const password = document.getElementById('password').value;
138
+
139
+ if (email && password) {
140
+ alert('Login functionality would be implemented here.\nEmail: ' + email);
141
+ } else {
142
+ alert('Please fill in all fields');
143
+ }
144
+ });
145
+ </script>
146
+ </body>
147
+ </html>
@@ -0,0 +1,3 @@
1
+ const { createOclifCommand } = require("../utils/factory.js");
2
+
3
+ module.exports = createOclifCommand("generate");
@@ -178,7 +178,7 @@ class BaseCommand extends Command {
178
178
  // Prepare CLI args for the agent with all derived options
179
179
  const cliArgs = {
180
180
  command: this.id,
181
- args: [filePath], // Pass the resolved file path as the first argument
181
+ args: filePath ? [filePath] : [], // Only pass file path if it exists
182
182
  options: {
183
183
  ...flags,
184
184
  resultFile:
@@ -28,17 +28,26 @@ function createOclifCommand(commandName) {
28
28
  this.agent.readlineInterface = readlineInterface;
29
29
  await readlineInterface.start();
30
30
  } else {
31
- // For run and sandbox commands, use the unified command system
32
- const fileArg = args.file || args.action || null;
33
- await this.setupAgent(fileArg, flags);
34
-
31
+ // For other commands, use the unified command system
35
32
  if (commandName === "run") {
33
+ const fileArg = args.file || args.action || null;
34
+ await this.setupAgent(fileArg, flags);
36
35
  // Set error limit higher for run command
37
36
  this.agent.errorLimit = 100;
37
+ // Execute through unified command system
38
+ await this.agent.executeUnifiedCommand(commandName, [fileArg], flags);
39
+ } else if (commandName === "generate") {
40
+ // For generate command, pass the file argument if provided
41
+ const fileArg = args.file || null;
42
+ await this.setupAgent(fileArg, flags);
43
+ // Execute through unified command system
44
+ await this.agent.executeUnifiedCommand(commandName, [fileArg], flags);
45
+ } else {
46
+ const primaryArg = args.file || args.action || null;
47
+ await this.setupAgent(primaryArg, flags);
48
+ // Execute through unified command system
49
+ await this.agent.executeUnifiedCommand(commandName, [primaryArg], flags);
38
50
  }
39
-
40
- // Execute through unified command system
41
- await this.agent.executeUnifiedCommand(commandName, [fileArg], flags);
42
51
  }
43
52
  } catch (error) {
44
53
  console.error(`Error executing ${commandName} command:`, error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testdriverai",
3
- "version": "6.0.28",
3
+ "version": "6.1.0",
4
4
  "description": "Next generation autonomous AI agent for end-to-end testing of web & desktop",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -34,6 +34,7 @@
34
34
  "@oclif/plugin-not-found": "^3.2.59",
35
35
  "@oclif/plugin-warn-if-update-available": "^3.1.43",
36
36
  "@stoplight/yaml-ast-parser": "^0.0.50",
37
+ "@trigger.dev/sdk": "^4.0.2",
37
38
  "ajv": "^8.17.1",
38
39
  "arktype": "^2.1.19",
39
40
  "axios": "^1.7.7",
@@ -65,6 +66,7 @@
65
66
  },
66
67
  "devDependencies": {
67
68
  "@eslint/js": "^9.10.0",
69
+ "@trigger.dev/build": "^4.0.2",
68
70
  "chai": "^5.1.2",
69
71
  "esbuild": "0.20.2",
70
72
  "esbuild-plugin-fileloc": "^0.0.6",
@@ -0,0 +1,10 @@
1
+ schema-version: v1
2
+ rulesets:
3
+ - javascript-best-practices
4
+ - javascript-browser-security
5
+ - javascript-code-style
6
+ - javascript-common-security
7
+ - javascript-express
8
+ - javascript-inclusive
9
+ - javascript-node-security
10
+ - jsx-react
@@ -0,0 +1,44 @@
1
+ version: 6.0.23
2
+ session: 68acf25541189ac907c336fe
3
+ steps:
4
+ - prompt: Upload test to Airtable
5
+ commands:
6
+ - command: exec
7
+ lang: pwsh
8
+ timeout: 30000
9
+ code: |
10
+ # Find the test file
11
+ $testFile = Join-Path ([Environment]::GetFolderPath("Desktop")) "pw-test.spec.js"
12
+
13
+ if (!(Test-Path $testFile)) {
14
+ Write-Host "No test file found"
15
+ exit 0
16
+ }
17
+
18
+ # Read file content
19
+ $content = Get-Content -Path $testFile -Raw
20
+ $testName = [System.IO.Path]::GetFileNameWithoutExtension("${TD_THIS_FILE}")
21
+
22
+ # Upload to Airtable
23
+ $data = @{
24
+ records = @(@{
25
+ fields = @{
26
+ "Name" = $testName
27
+ "Content" = $content
28
+ "Created" = (Get-Date -Format "yyyy-MM-dd HH:mm:ss")
29
+ }
30
+ })
31
+ } | ConvertTo-Json -Depth 3
32
+
33
+ $headers = @{
34
+ "Authorization" = "Bearer ${TD_AIRTABLE_TOKEN}"
35
+ "Content-Type" = "application/json"
36
+ }
37
+
38
+ try {
39
+ $response = Invoke-RestMethod -Uri "https://api.airtable.com/v0/${TD_AIRTABLE_BASE_ID}/${TD_AIRTABLE_TABLE_NAME}" -Method Post -Headers $headers -Body $data
40
+ Write-Host "✅ Uploaded: $($response.records[0].id)"
41
+ } catch {
42
+ Write-Host "❌ Upload failed: $($_.Exception.Message)"
43
+ }
44
+
@@ -0,0 +1,43 @@
1
+ version: 6.0.23
2
+ session: 68acf25541189ac907c336fe
3
+ steps:
4
+ - prompt: Download and start playwright codegen
5
+ commands:
6
+ - command: exec
7
+ lang: pwsh
8
+ timeout: 480000
9
+ code: |
10
+ echo "Step 1: Setting up npm environment"
11
+ # Create npm directory if it doesn't exist
12
+ $npmDir = "$env:APPDATA\npm"
13
+ if (!(Test-Path $npmDir)) {
14
+ New-Item -ItemType Directory -Force -Path $npmDir
15
+ echo "Created npm directory: $npmDir"
16
+ }
17
+
18
+ echo "Step 2: Installing Node.js and npm (if needed)"
19
+ # Check if node is available, if not try to install it
20
+ try {
21
+ node --version
22
+ npm --version
23
+ } catch {
24
+ echo "Node.js/npm not found, attempting to install..."
25
+ # Try to install Node.js via winget if available
26
+ try {
27
+ winget install OpenJS.NodeJS
28
+ } catch {
29
+ echo "Could not install Node.js automatically. Please install Node.js manually."
30
+ exit 1
31
+ }
32
+ }
33
+
34
+ echo "Step 3: Installing Playwright dependencies"
35
+ npm install -g playwright
36
+ npx playwright install --with-deps chromium
37
+
38
+ echo "Step 4: Generating Playwright script (non-blocking)"
39
+ $desktopPath = [Environment]::GetFolderPath("Desktop")
40
+ $outputFile = Join-Path $desktopPath "pw-test.spec.js"
41
+ Start-Process cmd.exe -ArgumentList "/c npx playwright codegen --target playwright-test -o `"$outputFile`" https://airbnb.com"
42
+ echo "Step 5: Completed"
43
+ exit
@@ -0,0 +1,7 @@
1
+ version: 6.0.0
2
+ steps:
3
+ - prompt: stop dashcam
4
+ commands:
5
+ - command: exec
6
+ lang: pwsh
7
+ code: dashcam -t 'Web Test Recording' -p
@@ -2,6 +2,12 @@ version: 6.0.0
2
2
  steps:
3
3
  - prompt: launch chrome
4
4
  commands:
5
+ - command: exec
6
+ lang: pwsh
7
+ code: dashcam track --name=TestDriver --type=application --pattern="C:\Users\testdriver\Documents\testdriver.log"
8
+ - command: exec
9
+ lang: pwsh
10
+ code: dashcam start
5
11
  - command: exec
6
12
  lang: pwsh
7
13
  code: |