testdriverai 6.2.0 → 6.2.2
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/.github/workflows/acceptance-tests.yml +2 -0
- package/.github/workflows/acceptance-v6.yml +2 -0
- package/.github/workflows/lint.yml +4 -1
- package/.github/workflows/publish-canary.yml +2 -0
- package/.github/workflows/publish-latest.yml +1 -0
- package/.github/workflows/self-hosted.yml +102 -0
- package/.prettierignore +1 -0
- package/.vscode/settings.json +4 -1
- package/agent/events.js +1 -10
- package/agent/index.js +98 -55
- package/agent/interface.js +43 -6
- package/agent/lib/censorship.js +15 -10
- package/agent/lib/commander.js +31 -18
- package/agent/lib/commands.js +62 -17
- package/agent/lib/debugger-server.js +0 -5
- package/agent/lib/generator.js +2 -2
- package/agent/lib/sdk.js +2 -1
- package/agent/lib/source-mapper.js +1 -1
- package/debugger/index.html +2 -2
- package/docs/account/enterprise.mdx +8 -12
- package/docs/account/pricing.mdx +2 -2
- package/docs/account/projects.mdx +5 -0
- package/docs/apps/tauri-apps.mdx +361 -0
- package/docs/cli/overview.mdx +6 -6
- package/docs/commands/assert.mdx +1 -0
- package/docs/commands/hover-text.mdx +3 -1
- package/docs/commands/match-image.mdx +5 -4
- package/docs/commands/press-keys.mdx +6 -8
- package/docs/commands/scroll-until-image.mdx +8 -7
- package/docs/commands/scroll-until-text.mdx +7 -6
- package/docs/commands/wait-for-image.mdx +5 -4
- package/docs/commands/wait-for-text.mdx +6 -5
- package/docs/docs.json +42 -40
- package/docs/getting-started/playwright.mdx +342 -0
- package/docs/getting-started/self-hosting.mdx +370 -0
- package/docs/getting-started/vscode.mdx +67 -56
- package/docs/guide/dashcam.mdx +118 -0
- package/docs/guide/environment-variables.mdx +5 -5
- package/docs/images/content/self-hosted/launchtemplateid.png +0 -0
- package/docs/images/content/vscode/ide-full.png +0 -0
- package/docs/images/content/vscode/running.png +0 -0
- package/docs/overview/comparison.mdx +22 -39
- package/docs/overview/quickstart.mdx +84 -32
- package/docs/styles.css +10 -1
- package/interfaces/cli/commands/generate.js +3 -0
- package/interfaces/cli/lib/base.js +27 -5
- package/interfaces/cli/utils/factory.js +17 -4
- package/interfaces/logger.js +4 -4
- package/interfaces/readline.js +1 -1
- package/package.json +3 -3
- package/schema.json +21 -0
- package/setup/aws/cloudformation.yaml +463 -0
- package/setup/aws/spawn-runner.sh +190 -0
- package/testdriver/acceptance/hover-text.yaml +2 -1
- package/testdriver/acceptance/prompt.yaml +4 -1
- package/testdriver/acceptance/scroll-until-image.yaml +5 -0
- package/testdriver/edge-cases/js-exception.yaml +8 -0
- package/testdriver/edge-cases/js-promise.yaml +19 -0
- package/testdriver/edge-cases/lifecycle/postrun.yaml +10 -0
- package/testdriver/edge-cases/success-test.yaml +9 -0
- package/testdriver/examples/web/lifecycle/postrun.yaml +7 -0
- package/testdriver/examples/web/lifecycle/{provision.yaml → prerun.yaml} +6 -0
- package/testdriver/lifecycle/postrun.yaml +7 -0
- package/testdriver/lifecycle/prerun.yaml +17 -0
|
@@ -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
package/.vscode/settings.json
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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(
|
|
883
|
-
this.emitter.emit(
|
|
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.
|
|
901
|
+
await this.runLifecycle("prerun");
|
|
886
902
|
|
|
887
|
-
|
|
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
|
-
|
|
916
|
+
prompt: prompt || "make sure to do a spellcheck",
|
|
902
917
|
image,
|
|
903
|
-
mousePosition:
|
|
904
|
-
activeWindow:
|
|
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
|
-
|
|
937
|
-
|
|
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, `${
|
|
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, `${
|
|
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 (
|
|
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 =
|
|
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 (
|
|
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 (
|
|
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
|
|
package/agent/interface.js
CHANGED
|
@@ -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
|
|
package/agent/lib/censorship.js
CHANGED
|
@@ -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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
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)
|