testdriverai 7.4.5 → 7.5.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/CHANGELOG.md +4 -0
- package/agent/events.js +7 -0
- package/agent/index.js +24 -17
- package/agent/lib/sandbox.js +727 -244
- package/agent/lib/system.js +70 -1
- package/docs/_data/examples-manifest.json +68 -68
- package/docs/v7/examples/ai.mdx +1 -1
- package/docs/v7/examples/assert.mdx +1 -1
- package/docs/v7/examples/chrome-extension.mdx +1 -1
- package/docs/v7/examples/drag-and-drop.mdx +1 -1
- package/docs/v7/examples/element-not-found.mdx +1 -1
- package/docs/v7/examples/hover-image.mdx +1 -1
- package/docs/v7/examples/hover-text.mdx +1 -1
- package/docs/v7/examples/installer.mdx +1 -1
- package/docs/v7/examples/launch-vscode-linux.mdx +1 -1
- package/docs/v7/examples/match-image.mdx +1 -1
- package/docs/v7/examples/press-keys.mdx +1 -1
- package/docs/v7/examples/scroll-keyboard.mdx +1 -1
- package/docs/v7/examples/scroll-until-image.mdx +1 -1
- package/docs/v7/examples/scroll-until-text.mdx +1 -1
- package/docs/v7/examples/scroll.mdx +1 -1
- package/docs/v7/examples/type.mdx +1 -1
- package/docs/v7/examples/windows-installer.mdx +1 -1
- package/lib/vitest/hooks.mjs +9 -0
- package/lib/vitest/setup-aws.mjs +1 -0
- package/manual/packer-hover-image.test.mjs +176 -0
- package/package.json +2 -1
- package/sdk.js +10 -0
- package/setup/aws/cloudformation.yaml +1 -8
- package/setup/aws/spawn-runner.sh +2 -37
- package/vitest.config.mjs +1 -1
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Packer AMI Integration Test
|
|
3
|
+
*
|
|
4
|
+
* Builds a fresh AMI via `packer build`, then runs the hover-image test suite
|
|
5
|
+
* against it. This is an end-to-end validation that a newly built runner image
|
|
6
|
+
* can provision a sandbox, execute commands, and interact with a browser.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* TD_API_ROOT=http://localhost:1337 \
|
|
10
|
+
* TD_API_KEY=<key> \
|
|
11
|
+
* npx vitest run examples/packer-hover-image.test.mjs
|
|
12
|
+
*
|
|
13
|
+
* The packer build takes ~25 minutes, and the hover-image test ~5-10 minutes,
|
|
14
|
+
* so the overall test timeout is set to 60 minutes.
|
|
15
|
+
*
|
|
16
|
+
* Set TD_SANDBOX_AMI to skip the packer build and use an existing AMI.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { describe, expect, it, beforeAll, afterAll } from "vitest";
|
|
20
|
+
import { TestDriver } from "../lib/vitest/hooks.mjs";
|
|
21
|
+
import { getDefaults } from "../examples/config.mjs";
|
|
22
|
+
import { execSync } from "child_process";
|
|
23
|
+
import path from "path";
|
|
24
|
+
import { fileURLToPath } from "url";
|
|
25
|
+
|
|
26
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
27
|
+
const __dirname = path.dirname(__filename);
|
|
28
|
+
const PACKER_DIR = path.resolve(__dirname, "../../runner/packer");
|
|
29
|
+
|
|
30
|
+
// 60 minute timeout for the entire suite (packer build + test)
|
|
31
|
+
const SUITE_TIMEOUT = 60 * 60 * 1000;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Build a fresh AMI using packer.
|
|
35
|
+
* Returns the AMI ID string (e.g. "ami-0337d8cd7cff854a4").
|
|
36
|
+
*/
|
|
37
|
+
function buildAmi() {
|
|
38
|
+
console.log("[packer] Starting AMI build — this takes ~25 minutes...");
|
|
39
|
+
const startTime = Date.now();
|
|
40
|
+
|
|
41
|
+
// packer build outputs "us-east-2: ami-XXXX" on the last relevant line
|
|
42
|
+
// Use -machine-readable for reliable parsing
|
|
43
|
+
const output = execSync("packer build -machine-readable .", {
|
|
44
|
+
cwd: PACKER_DIR,
|
|
45
|
+
encoding: "utf-8",
|
|
46
|
+
timeout: 45 * 60 * 1000, // 45 minute hard limit on packer
|
|
47
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
48
|
+
env: { ...process.env },
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Machine-readable output has lines like:
|
|
52
|
+
// timestamp,target,type,data
|
|
53
|
+
// ...,artifact,0,id,us-east-2:ami-XXXXXXXXXXXX
|
|
54
|
+
const amiMatch = output.match(/,artifact,\d+,id,[\w-]+:(ami-[a-f0-9]+)/);
|
|
55
|
+
if (!amiMatch) {
|
|
56
|
+
// Fallback: try human-readable format
|
|
57
|
+
const humanMatch = output.match(/[\w-]+:\s*(ami-[a-f0-9]+)/);
|
|
58
|
+
if (!humanMatch) {
|
|
59
|
+
console.error("[packer] Build output (last 2000 chars):", output.slice(-2000));
|
|
60
|
+
throw new Error("Failed to extract AMI ID from packer build output");
|
|
61
|
+
}
|
|
62
|
+
const elapsed = ((Date.now() - startTime) / 1000 / 60).toFixed(1);
|
|
63
|
+
console.log(`[packer] AMI built: ${humanMatch[1]} (${elapsed} min)`);
|
|
64
|
+
return humanMatch[1];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const amiId = amiMatch[1];
|
|
68
|
+
const elapsed = ((Date.now() - startTime) / 1000 / 60).toFixed(1);
|
|
69
|
+
console.log(`[packer] AMI built: ${amiId} (${elapsed} min)`);
|
|
70
|
+
return amiId;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Deregister an AMI and delete its backing snapshot.
|
|
75
|
+
* Best-effort — failures are logged but don't fail the test.
|
|
76
|
+
*/
|
|
77
|
+
function cleanupAmi(amiId) {
|
|
78
|
+
if (!amiId) return;
|
|
79
|
+
try {
|
|
80
|
+
console.log(`[cleanup] Deregistering AMI ${amiId}...`);
|
|
81
|
+
// Get snapshot IDs before deregistering
|
|
82
|
+
const describeOutput = execSync(
|
|
83
|
+
`aws ec2 describe-images --image-ids ${amiId} --query "Images[0].BlockDeviceMappings[*].Ebs.SnapshotId" --output text`,
|
|
84
|
+
{ encoding: "utf-8", timeout: 15000 },
|
|
85
|
+
).trim();
|
|
86
|
+
|
|
87
|
+
execSync(`aws ec2 deregister-image --image-id ${amiId}`, {
|
|
88
|
+
timeout: 15000,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Delete backing snapshots
|
|
92
|
+
if (describeOutput && describeOutput !== "None") {
|
|
93
|
+
for (const snapId of describeOutput.split(/\s+/)) {
|
|
94
|
+
if (snapId.startsWith("snap-")) {
|
|
95
|
+
console.log(`[cleanup] Deleting snapshot ${snapId}...`);
|
|
96
|
+
execSync(`aws ec2 delete-snapshot --snapshot-id ${snapId}`, {
|
|
97
|
+
timeout: 15000,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
console.log(`[cleanup] AMI ${amiId} cleaned up`);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
console.warn(`[cleanup] Failed to clean up AMI ${amiId}: ${err.message}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── Hover-Image login helper ─────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
async function performLogin(client, username = "standard_user") {
|
|
111
|
+
await client.focusApplication("Google Chrome");
|
|
112
|
+
const password = await client.extract("the password");
|
|
113
|
+
const usernameField = await client.find("username input");
|
|
114
|
+
await usernameField.click();
|
|
115
|
+
await client.type(username);
|
|
116
|
+
await client.pressKeys(["tab"]);
|
|
117
|
+
await client.type(password, { secret: true });
|
|
118
|
+
await client.pressKeys(["tab"]);
|
|
119
|
+
await client.pressKeys(["enter"]);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── Test Suite ───────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
describe("Packer AMI → Hover Image", { timeout: SUITE_TIMEOUT }, () => {
|
|
125
|
+
let amiId;
|
|
126
|
+
let builtAmi = false;
|
|
127
|
+
|
|
128
|
+
beforeAll(() => {
|
|
129
|
+
// If TD_SANDBOX_AMI is set, skip the packer build
|
|
130
|
+
if (process.env.TD_SANDBOX_AMI) {
|
|
131
|
+
amiId = process.env.TD_SANDBOX_AMI;
|
|
132
|
+
console.log(`[packer] Using existing AMI: ${amiId}`);
|
|
133
|
+
} else {
|
|
134
|
+
amiId = buildAmi();
|
|
135
|
+
builtAmi = true;
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
afterAll(() => {
|
|
140
|
+
// Only clean up AMIs we built (don't delete pre-existing ones)
|
|
141
|
+
if (builtAmi && amiId && process.env.TD_PACKER_CLEANUP !== "false") {
|
|
142
|
+
cleanupAmi(amiId);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it(
|
|
147
|
+
"should click on shopping cart icon and verify empty cart",
|
|
148
|
+
{ timeout: SUITE_TIMEOUT },
|
|
149
|
+
async (context) => {
|
|
150
|
+
const testdriver = TestDriver(context, {
|
|
151
|
+
...getDefaults(context),
|
|
152
|
+
os: "windows",
|
|
153
|
+
sandboxAmi: amiId,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Provision Chrome on the freshly-built AMI
|
|
157
|
+
await testdriver.provision.chrome({
|
|
158
|
+
url: "http://testdriver-sandbox.vercel.app/login",
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Perform login
|
|
162
|
+
await performLogin(testdriver);
|
|
163
|
+
|
|
164
|
+
// Click on the shopping cart icon
|
|
165
|
+
await testdriver.focusApplication("Google Chrome");
|
|
166
|
+
const cartIcon = await testdriver.find(
|
|
167
|
+
"shopping cart icon next to the Cart text in the top right corner",
|
|
168
|
+
);
|
|
169
|
+
await cartIcon.click();
|
|
170
|
+
|
|
171
|
+
// Assert that you see an empty shopping cart
|
|
172
|
+
const result = await testdriver.assert("Your cart is empty");
|
|
173
|
+
expect(result).toBeTruthy();
|
|
174
|
+
},
|
|
175
|
+
);
|
|
176
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "testdriverai",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.5.0",
|
|
4
4
|
"description": "Next generation autonomous AI agent for end-to-end testing of web & desktop",
|
|
5
5
|
"main": "sdk.js",
|
|
6
6
|
"types": "sdk.d.ts",
|
|
@@ -82,6 +82,7 @@
|
|
|
82
82
|
"@sentry/node": "^9.47.1",
|
|
83
83
|
"@stoplight/yaml-ast-parser": "^0.0.50",
|
|
84
84
|
"ajv": "^8.17.1",
|
|
85
|
+
"ably": "^2.6.0",
|
|
85
86
|
"arktype": "^2.1.19",
|
|
86
87
|
"axios": "^1.7.7",
|
|
87
88
|
"chalk": "^4.1.2",
|
package/sdk.js
CHANGED
|
@@ -1485,6 +1485,9 @@ class TestDriverSDK {
|
|
|
1485
1485
|
// Store IP address if provided for direct connection
|
|
1486
1486
|
this.ip = options.ip || null;
|
|
1487
1487
|
|
|
1488
|
+
// Store EC2 instance ID for direct connections (used to provision Ably credentials via SSM)
|
|
1489
|
+
this.instanceId = options.instanceId || null;
|
|
1490
|
+
|
|
1488
1491
|
// Store sandbox configuration options
|
|
1489
1492
|
this.sandboxAmi = options.sandboxAmi || null;
|
|
1490
1493
|
this.sandboxInstance = options.sandboxInstance || null;
|
|
@@ -2770,6 +2773,13 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
2770
2773
|
} else if (this.ip) {
|
|
2771
2774
|
this.agent.ip = this.ip;
|
|
2772
2775
|
}
|
|
2776
|
+
// Use instanceId from connectOptions if provided, otherwise fall back to constructor value
|
|
2777
|
+
// This allows the API to provision Ably credentials via SSM for direct connections
|
|
2778
|
+
if (connectOptions.instanceId !== undefined) {
|
|
2779
|
+
this.agent.instanceId = connectOptions.instanceId;
|
|
2780
|
+
} else if (this.instanceId) {
|
|
2781
|
+
this.agent.instanceId = this.instanceId;
|
|
2782
|
+
}
|
|
2773
2783
|
// Use sandboxAmi from connectOptions if provided, otherwise fall back to constructor value
|
|
2774
2784
|
if (connectOptions.sandboxAmi !== undefined) {
|
|
2775
2785
|
this.agent.sandboxAmi = connectOptions.sandboxAmi;
|
|
@@ -161,16 +161,9 @@ Resources:
|
|
|
161
161
|
SecurityGroup:
|
|
162
162
|
Type: AWS::EC2::SecurityGroup
|
|
163
163
|
Properties:
|
|
164
|
-
GroupDescription: SG for QA desktop testing (RDP/HTTPS/NGINX/
|
|
164
|
+
GroupDescription: SG for QA desktop testing (RDP/HTTPS/NGINX/VNC)
|
|
165
165
|
VpcId: !Ref TestDriverVpc
|
|
166
166
|
SecurityGroupIngress:
|
|
167
|
-
- {
|
|
168
|
-
IpProtocol: tcp,
|
|
169
|
-
FromPort: 8765,
|
|
170
|
-
ToPort: 8765,
|
|
171
|
-
CidrIp: !Ref AllowedIngressCidr,
|
|
172
|
-
Description: "pyautogui-cli WebSockets"
|
|
173
|
-
}
|
|
174
167
|
- {
|
|
175
168
|
IpProtocol: tcp,
|
|
176
169
|
FromPort: 5800,
|
|
@@ -11,7 +11,6 @@ set -euo pipefail
|
|
|
11
11
|
: "${RESOLUTION:=1440x900}"
|
|
12
12
|
|
|
13
13
|
TAG_NAME="${AWS_TAG_PREFIX}-"$(date +%s)
|
|
14
|
-
WS_CONFIG_PATH='C:\Windows\Temp\pyautogui-ws.json'
|
|
15
14
|
|
|
16
15
|
echo "Launching AWS Instance..."
|
|
17
16
|
|
|
@@ -144,46 +143,12 @@ done
|
|
|
144
143
|
|
|
145
144
|
echo "Getting Public IP..."
|
|
146
145
|
|
|
147
|
-
#
|
|
146
|
+
# --- 5) Get instance Public IP ---
|
|
148
147
|
DESC_JSON=$(aws ec2 describe-instances --region "$AWS_REGION" --instance-ids "$INSTANCE_ID" --output json)
|
|
149
148
|
PUBLIC_IP=$(jq -r '.Reservations[0].Instances[0].PublicIpAddress // empty' <<<"$DESC_JSON")
|
|
150
149
|
[ -n "$PUBLIC_IP" ] || PUBLIC_IP="No public IP assigned"
|
|
151
150
|
|
|
152
|
-
#
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
# --- 6) Read WebSocket config JSON ---
|
|
156
|
-
echo "Reading WebSocket configuration from: $WS_CONFIG_PATH"
|
|
157
|
-
READ_JSON=$(aws ssm send-command \
|
|
158
|
-
--region "$AWS_REGION" \
|
|
159
|
-
--instance-ids "$INSTANCE_ID" \
|
|
160
|
-
--document-name "AWS-RunPowerShellScript" \
|
|
161
|
-
--parameters "commands=[\"if (Test-Path '${WS_CONFIG_PATH}') { Get-Content -Raw '${WS_CONFIG_PATH}' } else { Write-Output 'Config file not found at ${WS_CONFIG_PATH}' }\"]" \
|
|
162
|
-
--output json)
|
|
163
|
-
|
|
164
|
-
READ_CMD_ID=$(jq -r '.Command.CommandId' <<<"$READ_JSON")
|
|
165
|
-
echo "WebSocket config read command ID: $READ_CMD_ID"
|
|
166
|
-
|
|
167
|
-
echo "Waiting for WebSocket config command to complete..."
|
|
168
|
-
aws ssm wait command-executed --region "$AWS_REGION" --command-id "$READ_CMD_ID" --instance-id "$INSTANCE_ID"
|
|
169
|
-
|
|
170
|
-
INVOC=$(aws ssm get-command-invocation \
|
|
171
|
-
--region "$AWS_REGION" \
|
|
172
|
-
--command-id "$READ_CMD_ID" \
|
|
173
|
-
--instance-id "$INSTANCE_ID" \
|
|
174
|
-
--output json)
|
|
175
|
-
|
|
176
|
-
STDOUT=$(jq -r '.StandardOutputContent // ""' <<<"$INVOC")
|
|
177
|
-
STDERR=$(jq -r '.StandardErrorContent // ""' <<<"$INVOC")
|
|
178
|
-
CMD_STATUS=$(jq -r '.Status // ""' <<<"$INVOC")
|
|
179
|
-
|
|
180
|
-
echo "WebSocket config command status: $CMD_STATUS"
|
|
181
|
-
if [ -n "$STDERR" ] && [ "$STDERR" != "null" ]; then
|
|
182
|
-
echo "WebSocket config stderr: $STDERR"
|
|
183
|
-
fi
|
|
184
|
-
echo "WebSocket config raw output: $STDOUT"
|
|
185
|
-
|
|
186
|
-
# --- 7) Output results ---
|
|
151
|
+
# --- 6) Output results ---
|
|
187
152
|
echo "Setup complete!"
|
|
188
153
|
echo "PUBLIC_IP=$PUBLIC_IP"
|
|
189
154
|
echo "INSTANCE_ID=$INSTANCE_ID"
|