testdriverai 4.0.81 → 4.1.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.
package/eslint.config.js CHANGED
@@ -16,6 +16,6 @@ module.exports = [
16
16
  {
17
17
  // this needs to be it's own object for some reason
18
18
  // https://github.com/eslint/eslint/issues/17400
19
- ignores: ["lib/subimage/opencv.js", "node_modules/**", ".git"],
19
+ ignores: ["lib/subimage/opencv.js", "node_modules/**", ".git", "electron/terminal/xterm*"],
20
20
  },
21
21
  ];
package/index.js CHANGED
@@ -46,6 +46,7 @@ const { showTerminal, hideTerminal } = require("./lib/focus-application");
46
46
  const isValidVersion = require("./lib/valid-version");
47
47
  const session = require("./lib/session");
48
48
  const notify = require("./lib/notify");
49
+ const { emitter, events } = require("./lib/events.js");
49
50
 
50
51
  let lastPrompt = "";
51
52
  let terminalApp = "";
@@ -54,7 +55,7 @@ let executionHistory = [];
54
55
  let errorCounts = {};
55
56
  let errorLimit = 3;
56
57
  let checkCount = 0;
57
- let checkLimit = 3;
58
+ let checkLimit = 7;
58
59
  let lastScreenshot = null;
59
60
  let rl;
60
61
 
@@ -160,8 +161,9 @@ function fileCompleter(line) {
160
161
  }
161
162
 
162
163
  function completer(line) {
163
- let completions =
164
- "/summarize /save /run /quit /assert /undo /manual".split(" ");
164
+ let completions = "/summarize /save /run /quit /assert /undo /manual".split(
165
+ " ",
166
+ );
165
167
  if (line.startsWith("/run ")) {
166
168
  return fileCompleter(line);
167
169
  } else {
@@ -256,8 +258,8 @@ const haveAIResolveError = async (error, markdown, depth = 0, undo = false) => {
256
258
  image,
257
259
  });
258
260
 
259
- if (response) {
260
- return await actOnMarkdown(response, depth, true);
261
+ if (response?.data) {
262
+ return await actOnMarkdown(response.data, depth, true);
261
263
  }
262
264
  };
263
265
 
@@ -275,19 +277,33 @@ const check = async () => {
275
277
 
276
278
  await delay(3000);
277
279
 
278
- console.log("");
279
280
  log.log("info", chalk.dim("checking..."), "testdriver");
280
281
 
281
- let thisScreenshot =await system.captureScreenBase64();
282
+ let thisScreenshot = await system.captureScreenBase64();
282
283
  let images = [lastScreenshot, thisScreenshot];
283
284
  let mousePosition = await system.getMousePosition();
284
285
  let activeWindow = await system.activeWin();
285
286
 
286
- let response = await sdk.req("check", { tasks, images, mousePosition, activeWindow });
287
+ const mdStream = log.createMarkdownStreamLogger();
288
+ let response = await sdk.req(
289
+ "check",
290
+ {
291
+ tasks,
292
+ images,
293
+ mousePosition,
294
+ activeWindow,
295
+ },
296
+ (chunk) => {
297
+ if (chunk.type === "data") {
298
+ mdStream.log(chunk.data);
299
+ }
300
+ },
301
+ );
302
+ mdStream.end();
287
303
 
288
304
  lastScreenshot = thisScreenshot;
289
305
 
290
- return response;
306
+ return response.data;
291
307
  };
292
308
 
293
309
  // command is transformed from a single yml entry generated by the AI into a JSON object
@@ -395,9 +411,6 @@ const aiExecute = async (message, validateAndLoop = false) => {
395
411
 
396
412
  let response = await check();
397
413
 
398
- console.log("");
399
- log.prettyMarkdown(response);
400
-
401
414
  let checkCodeblocks = [];
402
415
  try {
403
416
  checkCodeblocks = await parser.findCodeBlocks(response);
@@ -473,11 +486,15 @@ const humanInput = async (currentTask, validateAndLoop = false) => {
473
486
  activeWindow: await system.activeWin(),
474
487
  image: lastScreenshot,
475
488
  },
476
- (chunk) => mdStream.log(chunk),
489
+ (chunk) => {
490
+ if (chunk.type === "data") {
491
+ mdStream.log(chunk.data);
492
+ }
493
+ },
477
494
  );
478
495
  mdStream.end();
479
496
 
480
- await aiExecute(message, validateAndLoop);
497
+ await aiExecute(message.data, validateAndLoop);
481
498
 
482
499
  log.log("debug", "showing prompt from humanInput response check");
483
500
 
@@ -505,11 +522,15 @@ const generate = async (type, count) => {
505
522
  activeWindow: await system.activeWin(),
506
523
  count,
507
524
  },
508
- (chunk) => mdStream.log(chunk),
525
+ (chunk) => {
526
+ if (chunk.type === "data") {
527
+ mdStream.log(chunk.data);
528
+ }
529
+ },
509
530
  );
510
531
  mdStream.end();
511
532
 
512
- let testPrompts = await parser.findGenerativePrompts(message);
533
+ let testPrompts = await parser.findGenerativePrompts(message.data);
513
534
 
514
535
  // for each testPrompt
515
536
  for (const testPrompt of testPrompts) {
@@ -619,6 +640,7 @@ const firstPrompt = async () => {
619
640
  // this is how we parse user input
620
641
  // notice that the AI is only called if the input is not a command
621
642
  rl.on("line", async (input) => {
643
+ emitter.emit(events.interactive, false);
622
644
  await setTerminalApp();
623
645
 
624
646
  setTerminalWindowTransparency(true);
@@ -762,7 +784,11 @@ let summarize = async (error = null) => {
762
784
  error: error?.toString(),
763
785
  tasks,
764
786
  },
765
- (chunk) => mdStream.log(chunk),
787
+ (chunk) => {
788
+ if (chunk.type === "data") {
789
+ mdStream.log(chunk.data);
790
+ }
791
+ },
766
792
  );
767
793
  mdStream.end();
768
794
 
@@ -771,7 +797,7 @@ let summarize = async (error = null) => {
771
797
  resultFile = "/Windows/Temp/oiResult.log.log";
772
798
  }
773
799
  // write reply to /tmp/oiResult.log.log
774
- fs.writeFileSync(resultFile, reply);
800
+ fs.writeFileSync(resultFile, reply.data);
775
801
  };
776
802
 
777
803
  // this function is responsible for saving the regression test script to a file
@@ -900,6 +926,7 @@ ${yaml.dump(step)}
900
926
  };
901
927
 
902
928
  const promptUser = () => {
929
+ emitter.emit(events.interactive, true);
903
930
  rl.prompt(true);
904
931
  };
905
932
 
@@ -964,7 +991,18 @@ const embed = async (file, depth) => {
964
991
  log.log("info", `${file} (end)`);
965
992
  };
966
993
 
994
+ emitter.on(events.interactive, (data) => {
995
+ if (!terminalApp) return;
996
+ if (data) {
997
+ showTerminal(terminalApp);
998
+ } else {
999
+ hideTerminal(terminalApp);
1000
+ }
1001
+ });
1002
+
967
1003
  (async () => {
1004
+ // console.log(await system.getPrimaryDisplay());
1005
+
968
1006
  // @todo add-auth
969
1007
  // if (!process.env.DASHCAM_API_KEY) {
970
1008
  // log.log('info', chalk.red(`You must supply an API key`), 'system')
@@ -1009,13 +1047,13 @@ const embed = async (file, depth) => {
1009
1047
  await setTerminalApp();
1010
1048
 
1011
1049
  // should be start of new session
1012
- let sessionRes = await sdk.req("session/start", {
1050
+ const sessionRes = await sdk.req("session/start", {
1013
1051
  systemInformationOsInfo: await system.getSystemInformationOsInfo(),
1014
1052
  mousePosition: await system.getMousePosition(),
1015
1053
  activeWindow: await system.activeWin(),
1016
1054
  });
1017
1055
 
1018
- session.set(sessionRes);
1056
+ session.set(sessionRes.data);
1019
1057
 
1020
1058
  analytics.track("command", { command: thisCommand, file: thisFile });
1021
1059
 
package/lib/commander.js CHANGED
@@ -11,6 +11,9 @@ const marky = require("marky");
11
11
  // object is a json representation of the individual yml command
12
12
  // the process turns markdown -> yml -> json -> js function execution
13
13
  const run = async (object, depth) => {
14
+
15
+ log("debug", { object, depth });
16
+
14
17
  // success returns null
15
18
  // if this is set, it means that we need to take more action and a new thread is spawned
16
19
  let response = null;
package/lib/commands.js CHANGED
@@ -20,6 +20,7 @@ const os = require("os");
20
20
  const cliProgress = require("cli-progress");
21
21
  const redraw = require("./redraw");
22
22
  const logger = require("./logger");
23
+ const { emitter, events } = require("./events.js");
23
24
 
24
25
  const niceSeconds = (ms) => {
25
26
  return Math.round(ms / 1000);
@@ -138,7 +139,6 @@ const assert = async (assertion, shouldThrow = false, async = false) => {
138
139
  // take a screenshot
139
140
  log("info", "");
140
141
  log("info", chalk.dim("thinking..."), true);
141
- log("info", "");
142
142
 
143
143
  if (async) {
144
144
  await sdk
@@ -147,7 +147,7 @@ const assert = async (assertion, shouldThrow = false, async = false) => {
147
147
  image: await captureScreenBase64(),
148
148
  })
149
149
  .then((response) => {
150
- return handleAssertResponse(response);
150
+ return handleAssertResponse(response.data);
151
151
  });
152
152
 
153
153
  return true;
@@ -156,7 +156,7 @@ const assert = async (assertion, shouldThrow = false, async = false) => {
156
156
  expect: assertion,
157
157
  image: await captureScreenBase64(),
158
158
  });
159
- return handleAssertResponse(response);
159
+ return handleAssertResponse(response.data);
160
160
  }
161
161
  };
162
162
  const scroll = async (direction = "down", amount = 300) => {
@@ -167,19 +167,19 @@ const scroll = async (direction = "down", amount = 300) => {
167
167
  switch (direction) {
168
168
  case "up":
169
169
  await robot.keyTap("pageup");
170
- await redraw.wait(10000);
170
+ await redraw.wait(2500);
171
171
  break;
172
172
  case "down":
173
173
  await robot.keyTap("pagedown");
174
- await redraw.wait(10000);
174
+ await redraw.wait(2500);
175
175
  break;
176
176
  case "left":
177
177
  await robot.scrollMouse(amount * -1, 0);
178
- await redraw.wait(10000);
178
+ await redraw.wait(2500);
179
179
  break;
180
180
  case "right":
181
181
  await robot.scrollMouse(amount, 0);
182
- await redraw.wait(10000);
182
+ await redraw.wait(2500);
183
183
  break;
184
184
  default:
185
185
  throw new AiError("Direction not found");
@@ -199,7 +199,8 @@ const click = async (x, y, button = "left", click = "single") => {
199
199
  robot.moveMouseSmooth(x, y, 0.1);
200
200
  await delay(1000); // wait for the mouse to move
201
201
  robot.mouseClick(button, double);
202
- await redraw.wait(10000);
202
+ emitter.emit(events.mouseClick, { x, y, button, click });
203
+ await redraw.wait(5000);
203
204
  return;
204
205
  };
205
206
 
@@ -210,7 +211,7 @@ const hover = async (x, y) => {
210
211
  y = parseInt(y);
211
212
 
212
213
  await robot.moveMouseSmooth(x, y, 0.1);
213
- await redraw.wait(10000);
214
+ await redraw.wait(2500);
214
215
 
215
216
  return;
216
217
  };
@@ -229,14 +230,13 @@ let commands = {
229
230
  action = "click",
230
231
  button = "left",
231
232
  clickType = "single",
232
- method = "ai",
233
+ method = "turbo",
233
234
  ) => {
234
- text = text.toString();
235
+ text = text ? text.toString() : null;
235
236
  description = description ? description.toString() : null;
236
237
 
237
238
  log("info", "");
238
239
  log("info", chalk.dim("thinking..."), true);
239
- log("info", "");
240
240
 
241
241
  const mdStream = logger.createMarkdownStreamLogger();
242
242
  let response = await sdk.req(
@@ -251,14 +251,20 @@ let commands = {
251
251
  description,
252
252
  displayMultiple: 1,
253
253
  },
254
- (chunk) => mdStream.log(chunk),
254
+ (chunk) => {
255
+ if (chunk.type === "data") {
256
+ mdStream.log(chunk.data);
257
+ } else if (chunk.type === "closeMatches") {
258
+ emitter.emit(events.matches.show, chunk.data);
259
+ }
260
+ },
255
261
  );
256
262
  mdStream.end();
257
263
 
258
- if (!response) {
264
+ if (!response.data) {
259
265
  throw new AiError("No text on screen matches description", true);
260
266
  } else {
261
- return response;
267
+ return response.data;
262
268
  }
263
269
  },
264
270
  // uses our api to find all images on screen
@@ -271,22 +277,32 @@ let commands = {
271
277
  // take a screenshot
272
278
  log("info", "");
273
279
  log("info", chalk.dim("thinking..."), true);
274
- log("info", "");
275
280
 
276
- let response = await sdk.req("hover/image", {
277
- needle: description,
278
- image: await captureScreenBase64(),
279
- intent: action,
280
- button,
281
- clickType,
282
- displayMultiple: 1,
283
- });
281
+ const mdStream = logger.createMarkdownStreamLogger();
282
+ let response = await sdk.req(
283
+ "hover/image",
284
+ {
285
+ needle: description,
286
+ image: await captureScreenBase64(),
287
+ intent: action,
288
+ button,
289
+ clickType,
290
+ displayMultiple: 1,
291
+ },
292
+ (chunk) => {
293
+
294
+ if (chunk.type === "data") {
295
+ mdStream.log(chunk.data);
296
+ } else if (chunk.type === "closeMatches") {
297
+ emitter.emit(events.matches.show, chunk.data);
298
+ }
299
+ },
300
+ );
301
+ mdStream.end();
284
302
 
285
303
  if (!response?.data) {
286
304
  throw new AiError("No text on screen matches description", true);
287
305
  } else {
288
- log("info", "");
289
- logger.prettyMarkdown(response.data);
290
306
  return response.data;
291
307
  }
292
308
  },
@@ -315,7 +331,7 @@ let commands = {
315
331
  await redraw.start();
316
332
  string = string.toString();
317
333
  await robot.typeString(string);
318
- await redraw.wait(10000);
334
+ await redraw.wait(5000);
319
335
  return;
320
336
  },
321
337
  // press keys
@@ -373,7 +389,7 @@ let commands = {
373
389
  // finally, press the keys
374
390
  robot.keyTap(keysPressed[0], modsToPress);
375
391
 
376
- await redraw.wait(10000);
392
+ await redraw.wait(5000);
377
393
 
378
394
  // keyTap will release the normal keys, but will not release modifier keys
379
395
  // so we need to release the modifier keys manually
@@ -402,10 +418,11 @@ let commands = {
402
418
  let passed = false;
403
419
 
404
420
  while (durationPassed < timeout && !passed) {
405
- passed = await sdk.req("assert", {
421
+ const response = await sdk.req("assert", {
406
422
  needle: description,
407
423
  image: await captureScreenBase64(),
408
424
  });
425
+ passed = response.data;
409
426
 
410
427
  durationPassed = new Date().getTime() - startTime;
411
428
  if (!passed) {
@@ -433,7 +450,7 @@ let commands = {
433
450
  );
434
451
  }
435
452
  },
436
- "wait-for-text": async (text, timeout = 5000, method = "ai") => {
453
+ "wait-for-text": async (text, timeout = 5000, method = "turbo") => {
437
454
  await redraw.start();
438
455
 
439
456
  log("info", chalk.dim(`waiting for text: "${text}"...`), true);
@@ -444,12 +461,21 @@ let commands = {
444
461
  let passed = false;
445
462
 
446
463
  while (durationPassed < timeout && !passed) {
447
- passed = await sdk.req("assert/text", {
448
- needle: text,
449
- method: method,
450
- image: await captureScreenBase64(),
451
- });
464
+ const response = await sdk.req(
465
+ "assert/text",
466
+ {
467
+ needle: text,
468
+ method: method,
469
+ image: await captureScreenBase64(),
470
+ },
471
+ (chunk) => {
472
+ if (chunk.type === "closeMatches") {
473
+ emitter.emit(events.matches.show, chunk.data);
474
+ }
475
+ },
476
+ );
452
477
 
478
+ passed = response.data;
453
479
  durationPassed = new Date().getTime() - startTime;
454
480
  if (!passed && durationPassed > timeout) {
455
481
  log(
@@ -459,7 +485,7 @@ let commands = {
459
485
  ),
460
486
  true,
461
487
  );
462
- await redraw.wait(10000);
488
+ await redraw.wait(5000);
463
489
  }
464
490
  }
465
491
 
@@ -477,7 +503,7 @@ let commands = {
477
503
  text,
478
504
  direction = "down",
479
505
  maxDistance = 1200,
480
- method = "ai",
506
+ method = "turbo",
481
507
  ) => {
482
508
  await redraw.start();
483
509
 
@@ -488,7 +514,7 @@ let commands = {
488
514
  await robot.keyTap("f", commandOrControl);
489
515
  // type the text
490
516
  await robot.typeString(text);
491
- await redraw.wait(10000);
517
+ await redraw.wait(5000);
492
518
  await robot.keyTap("escape");
493
519
  } catch (e) {
494
520
  console.log(e);
@@ -503,12 +529,21 @@ let commands = {
503
529
  let passed = false;
504
530
 
505
531
  while (scrollDistance <= maxDistance && !passed) {
506
- passed = await sdk.req("assert/text", {
507
- needle: text,
508
- method: method,
509
- image: await captureScreenBase64(),
510
- });
532
+ const response = await sdk.req(
533
+ "assert/text",
534
+ {
535
+ needle: text,
536
+ method: method,
537
+ image: await captureScreenBase64(),
538
+ },
539
+ (chunk) => {
540
+ if (chunk.type === "closeMatches") {
541
+ emitter.emit(events.matches.show, chunk.data);
542
+ }
543
+ },
544
+ );
511
545
 
546
+ passed = response.data;
512
547
  if (!passed) {
513
548
  log(
514
549
  "info",
@@ -548,11 +583,12 @@ let commands = {
548
583
  let passed = false;
549
584
 
550
585
  while (scrollDistance < maxDistance && !passed) {
551
- passed = await sdk.req("assert", {
586
+ const response = await sdk.req("assert", {
552
587
  needle: `An image matching the description "${description}" appears on screen.`,
553
588
  image: await captureScreenBase64(),
554
589
  });
555
590
 
591
+ passed = response.data;
556
592
  if (!passed) {
557
593
  log(
558
594
  "info",
package/lib/config.js CHANGED
@@ -9,12 +9,11 @@ require("dotenv").config();
9
9
 
10
10
  // Parse out true and false string values
11
11
  function parseValue(value) {
12
- if (value === "false") {
13
- return false;
14
- }
15
-
16
- if (value === "true") {
17
- return true;
12
+ if (typeof value === "string") {
13
+ const normalizedValue = value.toLowerCase().trim();
14
+ if (["true", "false"].includes(normalizedValue)) {
15
+ return JSON.parse(normalizedValue);
16
+ }
18
17
  }
19
18
 
20
19
  return value;
@@ -29,6 +28,7 @@ const config = {
29
28
  TD_API_ROOT: "https://api.testdriver.ai",
30
29
  TD_DEV: parseValue(process.env["DEV"]),
31
30
  TD_PROFILE: false,
31
+ TD_OVERLAY: true,
32
32
  };
33
33
 
34
34
  // Find all env vars starting with TD_
package/lib/events.js ADDED
@@ -0,0 +1,38 @@
1
+ const { EventEmitter } = require("events");
2
+
3
+ const emitter = new EventEmitter();
4
+
5
+ const events = {
6
+ mouseClick: "mouse-click",
7
+ screenCapture: {
8
+ start: "screen-capture:start",
9
+ end: "screen-capture:end",
10
+ error: "screen-capture:error",
11
+ },
12
+ interactive: "interactive",
13
+ terminal: {
14
+ stdout: "terminal:stdout",
15
+ stderr: "terminal:stderr",
16
+ },
17
+ matches: {
18
+ show: "matches:show",
19
+ },
20
+ };
21
+
22
+ const getValues = (obj) => {
23
+ if (["string", "number"].includes(typeof obj)) {
24
+ return obj;
25
+ }
26
+ if (Array.isArray(obj)) {
27
+ return obj.map(getValues);
28
+ }
29
+ if ([undefined, null].includes(obj)) {
30
+ return [];
31
+ }
32
+
33
+ return Object.values(obj).map(getValues).flat();
34
+ };
35
+
36
+ const eventsArray = getValues(events);
37
+
38
+ module.exports = { events, emitter, eventsArray };
package/lib/overlay.js ADDED
@@ -0,0 +1,36 @@
1
+ const path = require("path");
2
+ const ipc = require("node-ipc").default;
3
+ const { fork } = require("child_process");
4
+
5
+ const { emitter, eventsArray } = require("./events.js");
6
+
7
+ ipc.config.id = "testdriverai";
8
+ ipc.config.retry = 50;
9
+ ipc.config.silent = true;
10
+
11
+ const electronProcess = fork(
12
+ path.join(__dirname, "../node_modules/electron/cli.js"),
13
+ [path.join(__dirname, "../electron/overlay.js")],
14
+ { stdio: "ignore" },
15
+ );
16
+
17
+ electronProcess.on("exit", (code) => {
18
+ process.exit(code);
19
+ });
20
+
21
+ process.on("exit", () => electronProcess.kill("SIGTERM"));
22
+ process.on("SIGINT", () => electronProcess.kill("SIGTERM")); // Ctrl+C
23
+ process.on("SIGTERM", () => electronProcess.kill("SIGTERM")); // Termination signal
24
+ process.on("uncaughtException", () => electronProcess.kill("SIGTERM"));
25
+
26
+ module.exports.electronProcessPromise = new Promise((resolve) => {
27
+ ipc.connectTo("testdriverai_overlay");
28
+ ipc.of.testdriverai_overlay.on("connect", () => {
29
+ eventsArray.forEach((event) =>
30
+ emitter.on(event, (data) =>
31
+ ipc.of.testdriverai_overlay.emit(event, data),
32
+ ),
33
+ );
34
+ resolve(electronProcess);
35
+ });
36
+ });