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.
@@ -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.4.5",
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/pyautogui + VNC)
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
- # # --- 5) Get instance Public IP ---
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
- # echo "Getting Websocket Port..."
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"
package/vitest.config.mjs CHANGED
@@ -15,7 +15,7 @@ export default defineConfig({
15
15
  hookTimeout: 900000,
16
16
  disableConsoleIntercept: true,
17
17
  maxConcurrency: 100,
18
- maxWorkers: 16,
18
+ maxWorkers: 4,
19
19
  reporters: [
20
20
  "default",
21
21
  TestDriver(),