testdriverai 7.8.0-canary.6 → 7.8.0-test.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.
Files changed (41) hide show
  1. package/agent/index.js +5 -6
  2. package/agent/lib/commands.js +2 -3
  3. package/agent/lib/sandbox.js +102 -117
  4. package/agent/lib/sdk.js +2 -4
  5. package/agent/lib/system.js +65 -25
  6. package/ai/skills/testdriver-running-tests/SKILL.md +1 -1
  7. package/docs/changelog.mdx +2 -2
  8. package/docs/docs.json +30 -31
  9. package/docs/v7/{hosted.mdx → cloud.mdx} +5 -43
  10. package/docs/v7/enterprise.mdx +110 -3
  11. package/docs/v7/quickstart.mdx +2 -30
  12. package/docs/v7/running-tests.mdx +1 -1
  13. package/docs/v7/self-hosted.mdx +44 -127
  14. package/{manual → examples}/drag-and-drop.test.mjs +1 -1
  15. package/interfaces/logger.js +12 -0
  16. package/interfaces/vitest-plugin.mjs +0 -3
  17. package/lib/core/Dashcam.js +7 -11
  18. package/lib/resolve-channel.js +3 -4
  19. package/package.json +1 -1
  20. package/sdk.js +3 -3
  21. package/vitest.config.mjs +32 -20
  22. package/agent/lib/http.js +0 -144
  23. package/ai/skills/testdriver-mcp/SKILL.md +0 -7
  24. package/docs/v7/copilot/auto-healing.mdx +0 -265
  25. package/docs/v7/copilot/creating-tests.mdx +0 -156
  26. package/docs/v7/copilot/github.mdx +0 -143
  27. package/docs/v7/copilot/running-tests.mdx +0 -149
  28. package/docs/v7/copilot/setup.mdx +0 -143
  29. package/docs/v7/mcp.mdx +0 -9
  30. package/lib/environments.json +0 -18
  31. /package/{manual → examples}/flake-diffthreshold-001.test.mjs +0 -0
  32. /package/{manual → examples}/flake-diffthreshold-01.test.mjs +0 -0
  33. /package/{manual → examples}/flake-diffthreshold-05.test.mjs +0 -0
  34. /package/{manual → examples}/flake-noredraw-cache.test.mjs +0 -0
  35. /package/{manual → examples}/flake-noredraw-nocache.test.mjs +0 -0
  36. /package/{manual → examples}/flake-redraw-cache.test.mjs +0 -0
  37. /package/{manual → examples}/flake-redraw-nocache.test.mjs +0 -0
  38. /package/{manual → examples}/flake-rocket-match.test.mjs +0 -0
  39. /package/{manual → examples}/flake-shared.mjs +0 -0
  40. /package/{manual → examples}/no-provision.test.mjs +0 -0
  41. /package/{manual → examples}/scroll-until-text.test.mjs +0 -0
package/agent/index.js CHANGED
@@ -1941,19 +1941,18 @@ ${regression}
1941
1941
  // Allow explicit override via env (e.g. VITE_DOMAIN from .env)
1942
1942
  if (process.env.VITE_DOMAIN) return process.env.VITE_DOMAIN;
1943
1943
 
1944
- const environments = require("../lib/environments.json");
1945
1944
  const mapping = {
1946
- "https://v6.testdriver.ai": environments.stable.consoleUrl,
1945
+ "https://api.testdriver.ai": "https://console.testdriver.ai",
1946
+ "https://v6.testdriver.ai": "https://console.testdriver.ai",
1947
+ "https://api-canary.testdriver.ai": "https://console-canary.testdriver.ai",
1948
+ "https://api-test.testdriver.ai": "https://console-test.testdriver.ai",
1947
1949
  };
1948
- for (const env of Object.values(environments)) {
1949
- mapping[env.apiRoot] = env.consoleUrl;
1950
- }
1951
1950
  if (mapping[apiRoot]) return mapping[apiRoot];
1952
1951
  // Local dev: API on localhost:1337 -> Web on localhost:3001
1953
1952
  if (apiRoot.includes("localhost:1337") || apiRoot.includes("127.0.0.1:1337")) {
1954
1953
  return "http://localhost:3001";
1955
1954
  }
1956
- return environments.stable.consoleUrl;
1955
+ return "https://console.testdriver.ai";
1957
1956
  }
1958
1957
 
1959
1958
  // Write session file for IDE preview (VSCode extension watches for these)
@@ -580,12 +580,11 @@ const createCommands = (
580
580
  } else if (action === "mouseDown") {
581
581
  await sandbox.send({ type: "mousePress", button: "left", x, y, ...elementData });
582
582
  } else if (action === "mouseUp") {
583
- // Move first to create drag motion, then release
584
- // (pyautogui.mouseUp with x/y teleports instead of dragging)
585
- await sandbox.send({ type: "moveMouse", x, y, ...elementData });
586
583
  await sandbox.send({
587
584
  type: "mouseRelease",
588
585
  button: "left",
586
+ x,
587
+ y,
589
588
  ...elementData
590
589
  });
591
590
  }
@@ -1,9 +1,83 @@
1
+ const crypto = require("crypto");
1
2
  const Ably = require("ably");
2
- const axios = require("axios");
3
3
  const { events } = require("../events");
4
4
  const logger = require("./logger");
5
5
  const { version } = require("../../package.json");
6
- const { withRetry, getSentryTraceHeaders } = require("./sdk");
6
+
7
+ function getSentryTraceHeaders(sessionId) {
8
+ if (!sessionId) return {};
9
+ const traceId = crypto.createHash("md5").update(sessionId).digest("hex");
10
+ const spanId = crypto.randomBytes(8).toString("hex");
11
+ return {
12
+ "sentry-trace": traceId + "-" + spanId + "-1",
13
+ baggage:
14
+ "sentry-trace_id=" +
15
+ traceId +
16
+ ",sentry-sample_rate=1.0,sentry-sampled=true",
17
+ };
18
+ }
19
+
20
+ function httpPost(apiRoot, path, body, timeout) {
21
+ const http = require("http");
22
+ const https = require("https");
23
+ const url = new URL(apiRoot + path);
24
+ const transport = url.protocol === "https:" ? https : http;
25
+ const bodyStr = JSON.stringify(body);
26
+
27
+ return new Promise(function (resolve, reject) {
28
+ var timeoutId = timeout
29
+ ? setTimeout(function () {
30
+ req.destroy();
31
+ reject(
32
+ new Error("HTTP request timed out after " + timeout + "ms"),
33
+ );
34
+ }, timeout)
35
+ : null;
36
+
37
+ var req = transport.request(
38
+ url,
39
+ {
40
+ method: "POST",
41
+ headers: {
42
+ "Content-Type": "application/json",
43
+ "Content-Length": Buffer.byteLength(bodyStr),
44
+ "Connection": "close",
45
+ },
46
+ },
47
+ function (res) {
48
+ var data = "";
49
+ res.on("data", function (chunk) {
50
+ data += chunk;
51
+ });
52
+ res.on("end", function () {
53
+ if (timeoutId) clearTimeout(timeoutId);
54
+ try {
55
+ var parsed = JSON.parse(data);
56
+ if (res.statusCode >= 400) {
57
+ var err = new Error(
58
+ parsed.errorMessage ||
59
+ parsed.message ||
60
+ "HTTP " + res.statusCode,
61
+ );
62
+ err.responseData = parsed;
63
+ reject(err);
64
+ } else {
65
+ resolve(parsed);
66
+ }
67
+ } catch (e) {
68
+ reject(new Error("Failed to parse API response: " + data));
69
+ }
70
+ });
71
+ },
72
+ );
73
+ req.on("error", function (err) {
74
+ if (timeoutId) clearTimeout(timeoutId);
75
+ reject(err);
76
+ });
77
+ req.write(bodyStr);
78
+ req.end();
79
+ });
80
+ }
7
81
 
8
82
  const createSandbox = function (emitter, analytics, sessionInstance) {
9
83
  class Sandbox {
@@ -31,12 +105,6 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
31
105
  this._lastConnectParams = null;
32
106
  this._teamId = null;
33
107
  this._sandboxId = null;
34
-
35
- // Rate limiting state for Ably publishes (Ably limits to 50 msg/sec per connection)
36
- this._publishLastTime = 0;
37
- this._publishMinIntervalMs = 25; // 40 msg/sec max, safely under Ably's 50 limit
38
- this._publishCount = 0;
39
- this._publishWindowStart = Date.now();
40
108
  }
41
109
 
42
110
  getTraceId() {
@@ -230,79 +298,40 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
230
298
  }
231
299
 
232
300
  /**
233
- * POST to the API with retry for transient network errors (via withRetry)
234
- * and infinite polling for CONCURRENCY_LIMIT_EXCEEDED (until vitest's
235
- * testTimeout kills the test).
301
+ * Wrapper around httpPost that retries when the server responds with
302
+ * CONCURRENCY_LIMIT_EXCEEDED (HTTP 429). Instead of failing the test
303
+ * immediately, we wait for a slot to become available — polling every
304
+ * 10 s until vitest's testTimeout kills the test.
236
305
  */
237
306
  async _httpPostWithConcurrencyRetry(path, body, timeout) {
238
- var concurrencyRetryInterval = 10000; // 10 seconds between concurrency retries
307
+ var retryInterval = 10000; // 10 seconds between retries
239
308
  var startTime = Date.now();
240
- var sessionId = this.sessionInstance ? this.sessionInstance.get() : null;
241
-
242
- var self = this;
243
- var makeRequest = function () {
244
- return axios({
245
- method: "post",
246
- url: self.apiRoot + path,
247
- data: body,
248
- headers: {
249
- "Content-Type": "application/json",
250
- "User-Agent": "TestDriverSDK/" + version + " (Node.js " + process.version + ")",
251
- ...getSentryTraceHeaders(sessionId),
252
- },
253
- timeout: timeout || 120000,
254
- });
255
- };
256
309
 
257
310
  while (true) {
258
311
  try {
259
- var response = await withRetry(makeRequest, {
260
- retryConfig: {
261
- maxRetries: 3,
262
- baseDelayMs: 2000,
263
- retryableStatusCodes: [500, 502, 503, 504], // Don't retry 429 — handled below
264
- },
265
- onRetry: function (attempt, error, delayMs) {
266
- var elapsed = Date.now() - startTime;
267
- logger.warn(
268
- "Transient network error: " + (error.message || error.code) +
269
- " — POST " + path +
270
- " — retry " + attempt + "/3" +
271
- " in " + (delayMs / 1000).toFixed(1) + "s" +
272
- " (" + Math.round(elapsed / 1000) + "s elapsed)...",
273
- );
274
- },
275
- });
276
- return response.data;
312
+ return await httpPost(this.apiRoot, path, body, timeout);
277
313
  } catch (err) {
278
- // Concurrency limit — poll forever until a slot opens
279
- var responseData = err.response && err.response.data;
280
- if (responseData && responseData.errorCode === "CONCURRENCY_LIMIT_EXCEEDED") {
281
- var elapsed = Date.now() - startTime;
282
- logger.log(
283
- "Concurrency limit reached — waiting " +
284
- concurrencyRetryInterval / 1000 +
285
- "s for a slot to become available (" +
286
- Math.round(elapsed / 1000) +
287
- "s elapsed)...",
288
- );
289
- await new Promise(function (resolve) {
290
- var t = setTimeout(resolve, concurrencyRetryInterval);
291
- if (t.unref) t.unref();
292
- });
293
- continue;
294
- }
314
+ var isConcurrencyLimit =
315
+ err.responseData &&
316
+ err.responseData.errorCode === "CONCURRENCY_LIMIT_EXCEEDED";
295
317
 
296
- // Non-retryable HTTP error — preserve responseData for callers
297
- if (responseData) {
298
- var httpErr = new Error(
299
- responseData.errorMessage || responseData.message || "HTTP " + err.response.status,
300
- );
301
- httpErr.responseData = responseData;
302
- throw httpErr;
318
+ if (!isConcurrencyLimit) {
319
+ throw err;
303
320
  }
304
321
 
305
- throw err;
322
+ var elapsed = Date.now() - startTime;
323
+
324
+ logger.log(
325
+ "Concurrency limit reached — waiting " +
326
+ retryInterval / 1000 +
327
+ "s for a slot to become available (" +
328
+ Math.round(elapsed / 1000) +
329
+ "s elapsed)...",
330
+ );
331
+ await new Promise(function (resolve) {
332
+ var t = setTimeout(resolve, retryInterval);
333
+ if (t.unref) t.unref();
334
+ });
306
335
  }
307
336
  }
308
337
  }
@@ -730,7 +759,8 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
730
759
  p.catch(function () {});
731
760
  }
732
761
 
733
- this._throttledPublish(this._cmdChannel, "command", message)
762
+ this._cmdChannel
763
+ .publish("command", message)
734
764
  .then(function () {
735
765
  emitter.emit(events.sandbox.sent, message);
736
766
  })
@@ -747,51 +777,6 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
747
777
  return p;
748
778
  }
749
779
 
750
- /**
751
- * Throttled publish to stay under Ably's 50 msg/sec per-connection limit.
752
- * Also tracks and logs the current publish rate for debugging.
753
- * @param {Object} channel - Ably channel to publish on
754
- * @param {string} eventName - Event name for the publish
755
- * @param {Object} message - Message payload
756
- * @returns {Promise} - Resolves when publish completes
757
- */
758
- async _throttledPublish(channel, eventName, message) {
759
- var self = this;
760
- var now = Date.now();
761
-
762
- // Rate limiting: wait if too soon since last publish
763
- var elapsed = now - this._publishLastTime;
764
- if (elapsed < this._publishMinIntervalMs) {
765
- var waitMs = this._publishMinIntervalMs - elapsed;
766
- await new Promise(function (resolve) {
767
- var timer = setTimeout(resolve, waitMs);
768
- if (timer.unref) timer.unref();
769
- });
770
- }
771
- this._publishLastTime = Date.now();
772
-
773
- // Metrics: track messages per second
774
- this._publishCount++;
775
- var windowElapsed = Date.now() - this._publishWindowStart;
776
- if (windowElapsed >= 1000) {
777
- var rate = (this._publishCount / windowElapsed) * 1000;
778
- var rateStr = rate.toFixed(1);
779
-
780
- // Log rate - warning if approaching limit, debug otherwise
781
- if (rate > 45) {
782
- logger.warn("Ably publish rate: " + rateStr + " msg/sec (approaching 50/sec limit)");
783
- } else if (process.env.VERBOSE || process.env.TD_DEBUG) {
784
- logger.log("Ably publish rate: " + rateStr + " msg/sec");
785
- }
786
-
787
- // Reset window
788
- this._publishCount = 0;
789
- this._publishWindowStart = Date.now();
790
- }
791
-
792
- return channel.publish(eventName, message);
793
- }
794
-
795
780
  async auth(apiKey) {
796
781
  this.apiKey = apiKey;
797
782
  var sessionId = this.sessionInstance
package/agent/lib/sdk.js CHANGED
@@ -231,7 +231,6 @@ const createSDK = (emitter, config, sessionInstance) => {
231
231
  let res = await withRetry(
232
232
  () => axios(url, c),
233
233
  {
234
- retryConfig: { maxRetries: 2 },
235
234
  onRetry: (attempt, error, delayMs) => {
236
235
  emitter.emit(events.sdk.retry, {
237
236
  path: 'auth/exchange-api-key',
@@ -382,7 +381,6 @@ const createSDK = (emitter, config, sessionInstance) => {
382
381
  "Content-Type": "application/json",
383
382
  "User-Agent": `TestDriverSDK/${version} (Node.js ${process.version})`,
384
383
  ...(token && { Authorization: `Bearer ${token}` }),
385
- ...getSentryTraceHeaders(sessionInstance.get()),
386
384
  },
387
385
  timeout: 15000,
388
386
  data: {
@@ -629,5 +627,5 @@ const createSDK = (emitter, config, sessionInstance) => {
629
627
  return { req, auth };
630
628
  };
631
629
 
632
- // Export the factory function and shared utilities
633
- module.exports = { createSDK, withRetry, getSentryTraceHeaders, sleep };
630
+ // Export the factory function
631
+ module.exports = { createSDK };
@@ -4,8 +4,6 @@ const os = require("os");
4
4
  const path = require("path");
5
5
  const { randomUUID } = require("crypto");
6
6
  const Jimp = require("jimp");
7
- const axios = require("axios");
8
- const { withRetry } = require("./sdk");
9
7
  const { events } = require("../events.js");
10
8
 
11
9
  const createSystem = (emitter, sandbox, config) => {
@@ -13,37 +11,79 @@ const createSystem = (emitter, sandbox, config) => {
13
11
  // Download a screenshot from S3 when the runner returns an s3Key
14
12
  // (screenshots exceed Ably's 64KB message limit)
15
13
  const downloadFromS3 = async (s3Key) => {
14
+ const https = require("https");
15
+ const http = require("http");
16
16
  const apiRoot = config["TD_API_ROOT"] || sandbox.apiRoot;
17
17
  const apiKey = sandbox.apiKey;
18
18
 
19
- // Step 1: Get presigned download URL from API (with retry)
20
- const response = await withRetry(
21
- () => axios({
22
- method: "post",
23
- url: apiRoot + "/api/v7/runner/download-url",
24
- data: { apiKey, s3Key },
25
- headers: { "Content-Type": "application/json" },
26
- timeout: 15000,
27
- }),
28
- {
29
- retryConfig: { maxRetries: 3, baseDelayMs: 1000 },
30
- },
31
- );
32
-
33
- const downloadUrl = response.data.downloadUrl;
34
- if (!downloadUrl) {
35
- throw new Error("No downloadUrl in response: " + JSON.stringify(response.data));
19
+ // Step 1: Get presigned download URL from API (with retry on rate-limit)
20
+ const body = JSON.stringify({ apiKey, s3Key });
21
+ const url = new URL(apiRoot + "/api/v7/runner/download-url");
22
+ const transport = url.protocol === "https:" ? https : http;
23
+
24
+ const MAX_RETRIES = 3;
25
+ let downloadUrl;
26
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
27
+ try {
28
+ downloadUrl = await new Promise((resolve, reject) => {
29
+ const req = transport.request(url, {
30
+ method: "POST",
31
+ headers: {
32
+ "Content-Type": "application/json",
33
+ "Content-Length": Buffer.byteLength(body),
34
+ "Connection": "close",
35
+ },
36
+ }, (res) => {
37
+ let data = "";
38
+ res.on("data", (chunk) => { data += chunk; });
39
+ res.on("end", () => {
40
+ if (res.statusCode === 429) {
41
+ return reject({ retryable: true, message: "Rate limited (429) from download-url endpoint" });
42
+ }
43
+ if (res.statusCode >= 400) {
44
+ return reject(new Error(`download-url request failed (HTTP ${res.statusCode}): ${data}`));
45
+ }
46
+ try {
47
+ const parsed = JSON.parse(data);
48
+ if (parsed.downloadUrl) {
49
+ resolve(parsed.downloadUrl);
50
+ } else {
51
+ reject(new Error("No downloadUrl in response: " + data));
52
+ }
53
+ } catch (e) {
54
+ reject({ retryable: true, message: "Failed to parse download-url response: " + data });
55
+ }
56
+ });
57
+ });
58
+ req.on("error", reject);
59
+ req.write(body);
60
+ req.end();
61
+ });
62
+ break; // success — exit retry loop
63
+ } catch (err) {
64
+ if (err && err.retryable && attempt < MAX_RETRIES) {
65
+ const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
66
+ await new Promise((r) => setTimeout(r, delay));
67
+ continue;
68
+ }
69
+ throw err instanceof Error ? err : new Error(err.message || String(err));
70
+ }
36
71
  }
37
72
 
38
73
  // Step 2: Download the image from S3
39
- const imageResponse = await axios({
40
- method: "get",
41
- url: downloadUrl,
42
- responseType: "arraybuffer",
43
- timeout: 30000,
74
+ const imageUrl = new URL(downloadUrl);
75
+ const s3Transport = imageUrl.protocol === "https:" ? https : http;
76
+
77
+ const imageBuffer = await new Promise((resolve, reject) => {
78
+ s3Transport.get(downloadUrl, { headers: { "Connection": "close" } }, (res) => {
79
+ const chunks = [];
80
+ res.on("data", (chunk) => chunks.push(chunk));
81
+ res.on("end", () => resolve(Buffer.concat(chunks)));
82
+ res.on("error", reject);
83
+ }).on("error", reject);
44
84
  });
45
85
 
46
- return Buffer.from(imageResponse.data).toString("base64");
86
+ return imageBuffer.toString("base64");
47
87
  };
48
88
 
49
89
  const screenshot = async (options) => {
@@ -133,7 +133,7 @@ export default defineConfig({
133
133
  <Card
134
134
  title="View Plans & Pricing"
135
135
  icon="credit-card"
136
- href="/v7/hosted"
136
+ href="/v7/cloud"
137
137
  >
138
138
  Compare plans and find the right level of parallelization for your team.
139
139
  </Card>
@@ -5,8 +5,8 @@ rss: true
5
5
  icon: "code-compare"
6
6
  ---
7
7
 
8
- <Update label="v7.8.0-test.6" description="March 2026" tags={["test"]}>
9
- 🔧 Maintenance
8
+ <Update label="v7.8.0-canary.5" description="March 2026" tags={["canary"]}>
9
+ This release includes all changes from v7.8.0-canary.4 with version bumps.
10
10
 
11
11
  ✨ New features
12
12
 
package/docs/docs.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://mintlify.com/docs.json",
3
- "theme": "almond",
3
+ "theme": "palm",
4
4
  "name": "TestDriver",
5
5
  "colors": {
6
6
  "primary": "#b3d334",
@@ -48,20 +48,20 @@
48
48
  "/v7/examples/windows-installer"
49
49
  ]
50
50
  },
51
-
52
51
  {
53
- "group": "Deployment",
52
+ "group": "Plans & Pricing",
54
53
  "icon": "server",
55
54
  "pages": [
56
- "/v7/hosted",
57
- "/v7/self-hosted"
55
+ "/v7/cloud",
56
+ "/v7/self-hosted",
57
+ "/v7/enterprise"
58
58
  ]
59
59
  },
60
60
  "/changelog"
61
61
  ]
62
62
  },
63
63
  {
64
- "group": "Guide",
64
+ "group": "Creating Tests",
65
65
  "pages": [
66
66
  "/v7/generating-tests",
67
67
  "/v7/device-config",
@@ -95,33 +95,32 @@
95
95
  ]
96
96
  },
97
97
  {
98
- "group": "SDK Reference",
98
+ "group": "Actions",
99
+ "pages": [
100
+ "/v7/ai",
101
+ "/v7/assert",
102
+ "/v7/captcha",
103
+ "/v7/click",
104
+ "/v7/double-click",
105
+ "/v7/exec",
106
+ "/v7/find",
107
+ "/v7/focus-application",
108
+ "/v7/hover",
109
+ "/v7/mouse-down",
110
+ "/v7/mouse-up",
111
+ "/v7/parse",
112
+ "/v7/press-keys",
113
+ "/v7/right-click",
114
+ "/v7/screenshot",
115
+ "/v7/type",
116
+ "/v7/scroll"
117
+ ]
118
+ },
119
+ {
120
+ "group": "SDK",
99
121
  "pages": [
100
- "/v7/client",
101
122
  "/v7/elements",
102
- {
103
- "group": "Actions",
104
- "icon": "bolt",
105
- "pages": [
106
- "/v7/ai",
107
- "/v7/assert",
108
- "/v7/captcha",
109
- "/v7/click",
110
- "/v7/double-click",
111
- "/v7/exec",
112
- "/v7/find",
113
- "/v7/focus-application",
114
- "/v7/hover",
115
- "/v7/mouse-down",
116
- "/v7/mouse-up",
117
- "/v7/parse",
118
- "/v7/press-keys",
119
- "/v7/right-click",
120
- "/v7/screenshot",
121
- "/v7/scroll",
122
- "/v7/type"
123
- ]
124
- },
123
+ "/v7/client",
125
124
  "/v7/dashcam"
126
125
  ]
127
126
  } ]
@@ -1,56 +1,18 @@
1
1
  ---
2
- title: "Hosted"
3
- sidebarTitle: "Hosted"
2
+ title: "Cloud Plan"
3
+ sidebarTitle: "Cloud"
4
4
  description: "The fastest way to get started with TestDriver. Just set your API key and start testing."
5
5
  icon: "cloud"
6
- mode: "wide"
7
6
  ---
8
7
 
9
- Hosted pricing is based on **device-seconds**: the amount of time your tests run on **our infrastructure**.
8
+ Cloud pricing is based on **device-seconds**: the amount of time your tests run on **our infrastructure**.
10
9
 
11
10
  - **Zero Setup** — Start testing immediately. No DevOps required.
12
11
  - **Free Tier** — Get started with a limited preview at no cost.
13
12
  - **Pay As You Go** — Only pay for the device-seconds you use.
14
13
 
15
- ## Hosted Plans
16
-
17
- <CardGroup cols={3}>
18
- <Card title="Free Trial" icon="gift" href="https://docs.testdriver.ai">
19
- **$0/month**
20
-
21
- - 1 Concurrent Sandbox
22
- - 60 Minutes Included
23
- - 1 Team User
24
- - Community Support
25
- </Card>
26
-
27
- <Card title="Pro" icon="rocket" href="https://console.testdriver.ai/checkout/pro">
28
- **$20/month**
29
-
30
- - 2 Concurrent Sandboxes
31
- - 600 Minutes Included
32
- - Overage: $0.002/second
33
- - 1 Team User
34
- - Test Recordings
35
- - Community Support
36
- </Card>
37
-
38
- <Card title="Team" icon="users" href="https://console.testdriver.ai/checkout/team">
39
- **$600/month**
40
-
41
- - 8 Concurrent Sandboxes
42
- - 10,000 Minutes Included
43
- - Overage: $0.001/second
44
- - 5 Team Users
45
- - Test Recordings
46
- - Private Support
47
- - Test Analytics
48
- - CPU, RAM, & Network Profiles
49
- </Card>
50
- </CardGroup>
51
-
52
14
  ## Get Started
53
- Hosted is the default when you follow the Quickstart guide.
15
+ Cloud is the default when you follow the Quickstart guide.
54
16
  <Card
55
17
  title="Try the Quickstart"
56
18
  icon="play"
@@ -140,7 +102,7 @@ To prevent tests from failing due to exceeding your license slot limit, we recom
140
102
 
141
103
  ## When to Consider Self-Hosted
142
104
 
143
- Hosted is perfect for getting started and for teams that want zero infrastructure management. However, you might consider [Self-Hosted](/v7/self-hosted) if you:
105
+ Cloud is perfect for getting started and for teams that want zero infrastructure management. However, you might consider [Self-Hosted](/v7/self-hosted) if you:
144
106
 
145
107
  - Want to escape per-second billing with a flat license fee
146
108
  - Require greater concurrency than offered in Cloud plans