testdriverai 7.8.0-test.7 → 7.8.0-test.71
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/agent/index.js +18 -5
- package/agent/lib/commands.js +3 -2
- package/agent/lib/http.js +162 -0
- package/agent/lib/logger.js +15 -0
- package/agent/lib/sandbox.js +554 -209
- package/agent/lib/sdk.js +5 -22
- package/agent/lib/system.js +25 -65
- package/ai/skills/testdriver-cache/SKILL.md +221 -0
- package/ai/skills/testdriver-errors/SKILL.md +246 -0
- package/ai/skills/testdriver-events/SKILL.md +356 -0
- package/ai/skills/testdriver-find/SKILL.md +14 -20
- package/ai/skills/testdriver-mcp/SKILL.md +7 -0
- package/ai/skills/testdriver-provision/SKILL.md +331 -0
- package/ai/skills/testdriver-redraw/SKILL.md +214 -0
- package/ai/skills/testdriver-running-tests/SKILL.md +1 -1
- package/ai/skills/testdriver-screenshots/SKILL.md +184 -0
- package/docs/_data/examples-manifest.json +46 -46
- package/docs/_scripts/extract-example-urls.js +67 -72
- package/docs/changelog.mdx +148 -8
- package/docs/docs.json +46 -38
- package/docs/images/content/vscode/v7-chat.png +0 -0
- package/docs/images/content/vscode/v7-choose-agent.png +0 -0
- package/docs/images/content/vscode/v7-full.png +0 -0
- package/docs/images/content/vscode/v7-onboarding.png +0 -0
- package/docs/v7/cache.mdx +223 -0
- package/docs/v7/copilot/auto-healing.mdx +265 -0
- package/docs/v7/copilot/creating-tests.mdx +156 -0
- package/docs/v7/copilot/github.mdx +143 -0
- package/docs/v7/copilot/running-tests.mdx +149 -0
- package/docs/v7/copilot/setup.mdx +143 -0
- package/docs/v7/enterprise.mdx +3 -110
- package/docs/v7/errors.mdx +248 -0
- package/docs/v7/events.mdx +358 -0
- package/docs/v7/examples/ai.mdx +1 -1
- package/docs/v7/examples/assert.mdx +1 -1
- package/docs/v7/examples/captcha-api.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/exec-output.mdx +85 -0
- package/docs/v7/examples/exec-pwsh.mdx +83 -0
- package/docs/v7/examples/focus-window.mdx +62 -0
- 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/docs/v7/find.mdx +14 -20
- package/docs/v7/{cloud.mdx → hosted.mdx} +43 -5
- package/docs/v7/mcp.mdx +9 -0
- package/docs/v7/provision.mdx +333 -0
- package/docs/v7/quickstart.mdx +30 -2
- package/docs/v7/redraw.mdx +216 -0
- package/docs/v7/running-tests.mdx +1 -1
- package/docs/v7/screenshots.mdx +186 -0
- package/docs/v7/self-hosted.mdx +127 -44
- package/docs/v7/test-results-json.mdx +258 -0
- package/examples/scroll-keyboard.test.mjs +1 -1
- package/examples/scroll.test.mjs +1 -12
- package/interfaces/logger.js +0 -12
- package/interfaces/vitest-plugin.mjs +170 -51
- package/lib/core/Dashcam.js +30 -23
- package/lib/environments.json +22 -0
- package/lib/github-comment.mjs +58 -40
- package/lib/init-project.js +5 -67
- package/lib/resolve-channel.js +42 -12
- package/lib/sentry.js +47 -23
- package/lib/vitest/hooks.mjs +63 -3
- package/{examples → manual}/drag-and-drop.test.mjs +1 -1
- package/manual/exec-stream-logs.test.mjs +25 -0
- package/mcp-server/dist/server.mjs +28 -8
- package/mcp-server/src/server.ts +31 -8
- package/package.json +4 -3
- package/sdk.d.ts +4 -0
- package/sdk.js +45 -15
- package/setup/aws/install-dev-runner.sh +79 -0
- package/setup/aws/spawn-runner.sh +165 -0
- package/test-sentry-span.js +35 -0
- package/vitest.config.mjs +22 -34
- package/vitest.runner.config.mjs +33 -0
- /package/{examples → manual}/flake-diffthreshold-001.test.mjs +0 -0
- /package/{examples → manual}/flake-diffthreshold-01.test.mjs +0 -0
- /package/{examples → manual}/flake-diffthreshold-05.test.mjs +0 -0
- /package/{examples → manual}/flake-noredraw-cache.test.mjs +0 -0
- /package/{examples → manual}/flake-noredraw-nocache.test.mjs +0 -0
- /package/{examples → manual}/flake-redraw-cache.test.mjs +0 -0
- /package/{examples → manual}/flake-redraw-nocache.test.mjs +0 -0
- /package/{examples → manual}/flake-rocket-match.test.mjs +0 -0
- /package/{examples → manual}/flake-shared.mjs +0 -0
- /package/{examples → manual}/no-provision.test.mjs +0 -0
- /package/{examples → manual}/scroll-until-text.test.mjs +0 -0
|
@@ -13,6 +13,7 @@ const channelConfig = require("../lib/resolve-channel.js");
|
|
|
13
13
|
|
|
14
14
|
// Import Sentry for error reporting
|
|
15
15
|
const Sentry = require("@sentry/node");
|
|
16
|
+
const chalk = require("chalk");
|
|
16
17
|
|
|
17
18
|
// Track if Sentry has been initialized
|
|
18
19
|
let sentryInitialized = false;
|
|
@@ -23,7 +24,7 @@ let sentryInitialized = false;
|
|
|
23
24
|
*/
|
|
24
25
|
function initializeSentry() {
|
|
25
26
|
if (sentryInitialized) return;
|
|
26
|
-
|
|
27
|
+
|
|
27
28
|
// Respect telemetry opt-out
|
|
28
29
|
if (process.env.TD_TELEMETRY === "false") {
|
|
29
30
|
return;
|
|
@@ -31,12 +32,12 @@ function initializeSentry() {
|
|
|
31
32
|
|
|
32
33
|
try {
|
|
33
34
|
const version = resolveTestDriverVersion() || "unknown";
|
|
34
|
-
|
|
35
|
+
|
|
35
36
|
Sentry.init({
|
|
36
37
|
dsn:
|
|
37
38
|
process.env.SENTRY_DSN ||
|
|
38
39
|
"https://452bd5a00dbd83a38ee8813e11c57694@o4510262629236736.ingest.us.sentry.io/4510480443637760",
|
|
39
|
-
environment:
|
|
40
|
+
environment: channelConfig.sentryEnvironment,
|
|
40
41
|
release: version,
|
|
41
42
|
sampleRate: 1.0,
|
|
42
43
|
tracesSampleRate: 1.0,
|
|
@@ -53,12 +54,12 @@ function initializeSentry() {
|
|
|
53
54
|
// Filter out events that should not be reported to Sentry
|
|
54
55
|
beforeSend(event, hint) {
|
|
55
56
|
const error = hint.originalException;
|
|
56
|
-
|
|
57
|
+
|
|
57
58
|
// Don't send user-cancelled errors
|
|
58
59
|
if (error && error.message && error.message.includes("User cancelled")) {
|
|
59
60
|
return null;
|
|
60
61
|
}
|
|
61
|
-
|
|
62
|
+
|
|
62
63
|
// Don't send test failures - these are expected behavior, not bugs in the SDK
|
|
63
64
|
// Test failures indicate the test found a problem, which is the intended use case
|
|
64
65
|
if (event.exception?.values) {
|
|
@@ -67,28 +68,33 @@ function initializeSentry() {
|
|
|
67
68
|
if (exception.type === "TestFailure") {
|
|
68
69
|
return null;
|
|
69
70
|
}
|
|
70
|
-
|
|
71
|
+
|
|
71
72
|
// Filter out common user code errors (ReferenceError, TypeError from user tests)
|
|
72
73
|
// Only report if the error originates from TestDriver SDK code, not user test code
|
|
73
74
|
const isUserCodeError = exception.stacktrace?.frames?.some(frame => {
|
|
74
75
|
const filename = frame.filename || frame.abs_path || "";
|
|
75
76
|
// Check if error is from user test files (not from SDK internals)
|
|
76
|
-
return filename.includes("/tests/") ||
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
return filename.includes("/tests/") ||
|
|
78
|
+
filename.includes("/test/") ||
|
|
79
|
+
filename.includes(".test.") ||
|
|
80
|
+
filename.includes(".spec.");
|
|
80
81
|
});
|
|
81
|
-
|
|
82
|
+
|
|
82
83
|
if (isUserCodeError && (exception.type === "ReferenceError" || exception.type === "TypeError")) {
|
|
83
84
|
return null;
|
|
84
85
|
}
|
|
86
|
+
|
|
87
|
+
// Filter out ElementNotFoundError - expected test outcome, not a crash
|
|
88
|
+
if (exception.type === "ElementNotFoundError") {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
85
91
|
}
|
|
86
92
|
}
|
|
87
|
-
|
|
93
|
+
|
|
88
94
|
return event;
|
|
89
95
|
},
|
|
90
96
|
});
|
|
91
|
-
|
|
97
|
+
|
|
92
98
|
sentryInitialized = true;
|
|
93
99
|
logger.debug("Sentry initialized for vitest");
|
|
94
100
|
} catch (err) {
|
|
@@ -118,17 +124,17 @@ async function flushSentry(timeout = 2000) {
|
|
|
118
124
|
function resolveTestDriverVersion() {
|
|
119
125
|
try {
|
|
120
126
|
return require("../package.json").version;
|
|
121
|
-
} catch {}
|
|
127
|
+
} catch { }
|
|
122
128
|
|
|
123
129
|
try {
|
|
124
130
|
const cwdRequire = createRequire(path.join(process.cwd(), "package.json"));
|
|
125
131
|
return cwdRequire("testdriverai/package.json").version;
|
|
126
|
-
} catch {}
|
|
132
|
+
} catch { }
|
|
127
133
|
|
|
128
134
|
try {
|
|
129
135
|
const pkgPath = path.join(process.cwd(), "node_modules", "testdriverai", "package.json");
|
|
130
136
|
return JSON.parse(fs.readFileSync(pkgPath, "utf8")).version;
|
|
131
|
-
} catch {}
|
|
137
|
+
} catch { }
|
|
132
138
|
|
|
133
139
|
return null;
|
|
134
140
|
}
|
|
@@ -149,19 +155,19 @@ function resolveVitestVersion() {
|
|
|
149
155
|
// Strategy 1: createRequire from import.meta.url (standard CJS interop)
|
|
150
156
|
try {
|
|
151
157
|
return require("vitest/package.json").version;
|
|
152
|
-
} catch {}
|
|
158
|
+
} catch { }
|
|
153
159
|
|
|
154
160
|
// Strategy 2: createRequire from process.cwd() (works when import.meta.url is rewritten)
|
|
155
161
|
try {
|
|
156
162
|
const cwdRequire = createRequire(path.join(process.cwd(), "package.json"));
|
|
157
163
|
return cwdRequire("vitest/package.json").version;
|
|
158
|
-
} catch {}
|
|
164
|
+
} catch { }
|
|
159
165
|
|
|
160
166
|
// Strategy 3: read directly from node_modules on disk
|
|
161
167
|
try {
|
|
162
168
|
const vitestPkgPath = path.join(process.cwd(), "node_modules", "vitest", "package.json");
|
|
163
169
|
return JSON.parse(fs.readFileSync(vitestPkgPath, "utf8")).version;
|
|
164
|
-
} catch {}
|
|
170
|
+
} catch { }
|
|
165
171
|
|
|
166
172
|
return null;
|
|
167
173
|
}
|
|
@@ -176,7 +182,7 @@ function checkVitestVersion() {
|
|
|
176
182
|
if (!version) {
|
|
177
183
|
throw new Error(
|
|
178
184
|
"TestDriver requires Vitest to be installed. " +
|
|
179
|
-
|
|
185
|
+
"Please install it: npm install vitest@latest",
|
|
180
186
|
);
|
|
181
187
|
}
|
|
182
188
|
|
|
@@ -184,7 +190,7 @@ function checkVitestVersion() {
|
|
|
184
190
|
if (major < MINIMUM_VITEST_VERSION) {
|
|
185
191
|
throw new Error(
|
|
186
192
|
`TestDriver requires Vitest >= ${MINIMUM_VITEST_VERSION}.0.0, but found ${version}. ` +
|
|
187
|
-
|
|
193
|
+
`Please upgrade Vitest: npm install vitest@latest`,
|
|
188
194
|
);
|
|
189
195
|
}
|
|
190
196
|
}
|
|
@@ -374,7 +380,7 @@ export async function authenticateWithApiKey(apiKey, apiRoot) {
|
|
|
374
380
|
// Network-level error (fetch failed entirely)
|
|
375
381
|
const networkError = new Error(
|
|
376
382
|
`Unable to reach TestDriver API at ${apiRoot}. ` +
|
|
377
|
-
|
|
383
|
+
"Check your internet connection and try again.",
|
|
378
384
|
);
|
|
379
385
|
networkError.code = "NETWORK_ERROR";
|
|
380
386
|
networkError.isNetworkError = true;
|
|
@@ -394,8 +400,8 @@ export async function authenticateWithApiKey(apiKey, apiRoot) {
|
|
|
394
400
|
if (response.status === 401) {
|
|
395
401
|
const authError = new Error(
|
|
396
402
|
data.message ||
|
|
397
|
-
|
|
398
|
-
|
|
403
|
+
"Invalid API key. Please check your TD_API_KEY and try again. " +
|
|
404
|
+
"Get your API key at https://console.testdriver.ai/team",
|
|
399
405
|
);
|
|
400
406
|
authError.code = data.error || "INVALID_API_KEY";
|
|
401
407
|
authError.isAuthError = true;
|
|
@@ -406,7 +412,7 @@ export async function authenticateWithApiKey(apiKey, apiRoot) {
|
|
|
406
412
|
if (response.status >= 500) {
|
|
407
413
|
const serverError = new Error(
|
|
408
414
|
data.message ||
|
|
409
|
-
|
|
415
|
+
`TestDriver API is currently unavailable (HTTP ${response.status}). Please try again later.`,
|
|
410
416
|
);
|
|
411
417
|
serverError.code = data.error || "API_UNAVAILABLE";
|
|
412
418
|
serverError.isServerError = true;
|
|
@@ -426,7 +432,7 @@ export async function authenticateWithApiKey(apiKey, apiRoot) {
|
|
|
426
432
|
// Other HTTP errors
|
|
427
433
|
throw new Error(
|
|
428
434
|
`Authentication failed: ${response.status} ${response.statusText}` +
|
|
429
|
-
|
|
435
|
+
(data.message ? ` - ${data.message}` : ""),
|
|
430
436
|
);
|
|
431
437
|
}
|
|
432
438
|
|
|
@@ -840,6 +846,9 @@ class TestDriverReporter {
|
|
|
840
846
|
logger.debug("API key present:", !!pluginState.apiKey);
|
|
841
847
|
logger.debug("API root:", pluginState.apiRoot);
|
|
842
848
|
|
|
849
|
+
// Environment info is printed by the SDK when each test initializes,
|
|
850
|
+
// so we skip the duplicate banner here in the reporter.
|
|
851
|
+
|
|
843
852
|
// Check if we should enable the reporter
|
|
844
853
|
if (!pluginState.apiKey) {
|
|
845
854
|
logger.warn("No API key provided, skipping test recording");
|
|
@@ -1135,7 +1144,7 @@ class TestDriverReporter {
|
|
|
1135
1144
|
const error = result.errors[0];
|
|
1136
1145
|
errorMessage = error.message;
|
|
1137
1146
|
errorStack = error.stack;
|
|
1138
|
-
|
|
1147
|
+
|
|
1139
1148
|
// Note: We do NOT report test failures to Sentry.
|
|
1140
1149
|
// Test failures are expected behavior (they indicate a test found a bug).
|
|
1141
1150
|
// We only want actual SDK crashes and exceptions reported to Sentry.
|
|
@@ -1205,8 +1214,125 @@ class TestDriverReporter {
|
|
|
1205
1214
|
|
|
1206
1215
|
console.log("");
|
|
1207
1216
|
console.log(
|
|
1208
|
-
`🔗 Test Report: ${consoleUrl}/runs/${testRunDbId}/${testCaseDbId}
|
|
1217
|
+
chalk.cyan(`🔗 Test Report: ${consoleUrl}/runs/${testRunDbId}/${testCaseDbId}`),
|
|
1209
1218
|
);
|
|
1219
|
+
console.log("");
|
|
1220
|
+
|
|
1221
|
+
// Write per-test-case JSON result file
|
|
1222
|
+
{
|
|
1223
|
+
const testResult = meta.testResult || {};
|
|
1224
|
+
|
|
1225
|
+
// Parse replay URL to extract replayId and shareKey for embed links
|
|
1226
|
+
let replayUrl = dashcamUrl || null;
|
|
1227
|
+
let replayGifUrl = null;
|
|
1228
|
+
let replayEmbedUrl = null;
|
|
1229
|
+
let replayMarkdown = null;
|
|
1230
|
+
const replayMatch = dashcamUrl && dashcamUrl.match(/\/replay\/([a-f0-9]+)\?share=([^&\s]+)/);
|
|
1231
|
+
if (replayMatch) {
|
|
1232
|
+
const [, replayId, shareKey] = replayMatch;
|
|
1233
|
+
const apiRoot = pluginState.apiRoot;
|
|
1234
|
+
replayGifUrl = `${apiRoot}/replay/${replayId}/gif?shareKey=${shareKey}`;
|
|
1235
|
+
replayEmbedUrl = `${consoleUrl}/replay/${replayId}?share=${shareKey}&embed=true`;
|
|
1236
|
+
replayMarkdown = `[](${replayUrl})`;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
const resultData = {
|
|
1240
|
+
// Versions
|
|
1241
|
+
versions: {
|
|
1242
|
+
sdk: testResult.sdkVersion || null,
|
|
1243
|
+
vitest: resolveVitestVersion() || null,
|
|
1244
|
+
api: testResult.apiVersion || null,
|
|
1245
|
+
runnerBefore: testResult.runnerVersionBefore || null,
|
|
1246
|
+
runnerAfter: testResult.runnerVersionAfter || null,
|
|
1247
|
+
runnerWasUpdated: testResult.wasUpdated || false,
|
|
1248
|
+
},
|
|
1249
|
+
|
|
1250
|
+
// Test info
|
|
1251
|
+
test: {
|
|
1252
|
+
file: testResult.testFile || null,
|
|
1253
|
+
name: testResult.testName || null,
|
|
1254
|
+
suite: testResult.suiteName || null,
|
|
1255
|
+
passed: status === "passed",
|
|
1256
|
+
caseId: testCaseDbId || null,
|
|
1257
|
+
runId: testRunDbId || null,
|
|
1258
|
+
error: errorMessage || testResult.error || null,
|
|
1259
|
+
errorStack: errorStack || testResult.errorStack || null,
|
|
1260
|
+
},
|
|
1261
|
+
|
|
1262
|
+
// URLs
|
|
1263
|
+
urls: {
|
|
1264
|
+
api: testResult.apiUrl || null,
|
|
1265
|
+
console: consoleUrl || null,
|
|
1266
|
+
vnc: testResult.vncUrl || null,
|
|
1267
|
+
testRun: testCaseDbId ? `${consoleUrl}/runs/${testRunDbId}/${testCaseDbId}` : null,
|
|
1268
|
+
},
|
|
1269
|
+
|
|
1270
|
+
// Recording replay
|
|
1271
|
+
replay: {
|
|
1272
|
+
url: replayUrl,
|
|
1273
|
+
gifUrl: replayGifUrl,
|
|
1274
|
+
embedUrl: replayEmbedUrl,
|
|
1275
|
+
markdown: replayMarkdown,
|
|
1276
|
+
},
|
|
1277
|
+
|
|
1278
|
+
// Timing
|
|
1279
|
+
date: testResult.date || new Date().toISOString(),
|
|
1280
|
+
|
|
1281
|
+
// Team & session
|
|
1282
|
+
team: {
|
|
1283
|
+
id: testResult.teamId || null,
|
|
1284
|
+
sessionId: testResult.sessionId || null,
|
|
1285
|
+
},
|
|
1286
|
+
|
|
1287
|
+
// Infrastructure
|
|
1288
|
+
infrastructure: {
|
|
1289
|
+
sandboxId: testResult.sandboxId || null,
|
|
1290
|
+
instanceId: testResult.instanceId || null,
|
|
1291
|
+
os: testResult.os || null,
|
|
1292
|
+
amiId: testResult.amiId || null,
|
|
1293
|
+
e2bTemplateId: testResult.e2bTemplateId || null,
|
|
1294
|
+
imageVersion: testResult.imageVersion || null,
|
|
1295
|
+
},
|
|
1296
|
+
|
|
1297
|
+
// Realtime
|
|
1298
|
+
realtime: {
|
|
1299
|
+
channel: testResult.realtimeChannel || null,
|
|
1300
|
+
messageCount: testResult.realtimeMessageCount || 0,
|
|
1301
|
+
},
|
|
1302
|
+
|
|
1303
|
+
// Interactions
|
|
1304
|
+
interactions: testResult.interactions || { total: 0, cached: 0, byType: {} },
|
|
1305
|
+
};
|
|
1306
|
+
|
|
1307
|
+
// Sanitize testName for filesystem use
|
|
1308
|
+
const safeName = (test.name || "unknown").replace(/[^a-zA-Z0-9_.-]/g, "_").substring(0, 200);
|
|
1309
|
+
const resultDir = path.join(process.cwd(), ".testdriver", "results", testFile);
|
|
1310
|
+
fs.mkdirSync(resultDir, { recursive: true });
|
|
1311
|
+
|
|
1312
|
+
// Include a stable unique suffix in the filename to avoid collisions
|
|
1313
|
+
// when multiple tests in the same file share the same name.
|
|
1314
|
+
const hashSourceParts = [];
|
|
1315
|
+
if (test.id) {
|
|
1316
|
+
hashSourceParts.push(String(test.id));
|
|
1317
|
+
}
|
|
1318
|
+
if (Array.isArray(test.suitePath)) {
|
|
1319
|
+
hashSourceParts.push(test.suitePath.join(" > "));
|
|
1320
|
+
}
|
|
1321
|
+
if (test.file && (test.file.name || test.file.path)) {
|
|
1322
|
+
hashSourceParts.push(test.file.name || test.file.path);
|
|
1323
|
+
}
|
|
1324
|
+
// Fallback to the test name if no other identifiers are available.
|
|
1325
|
+
if (hashSourceParts.length === 0) {
|
|
1326
|
+
hashSourceParts.push(test.name || "unknown");
|
|
1327
|
+
}
|
|
1328
|
+
const hashSource = hashSourceParts.join(" | ");
|
|
1329
|
+
const uniqueHash = crypto.createHash("sha256").update(hashSource).digest("hex").slice(0, 8);
|
|
1330
|
+
|
|
1331
|
+
fs.writeFileSync(
|
|
1332
|
+
path.join(resultDir, `${safeName}-${uniqueHash}.json`),
|
|
1333
|
+
JSON.stringify(resultData, null, 2),
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1210
1336
|
|
|
1211
1337
|
// If there were retries, list all per-attempt dashcam URLs for debugging
|
|
1212
1338
|
if (hasRetries) {
|
|
@@ -1218,14 +1344,7 @@ class TestDriverReporter {
|
|
|
1218
1344
|
}
|
|
1219
1345
|
}
|
|
1220
1346
|
}
|
|
1221
|
-
|
|
1222
|
-
// Output parseable format for docs generation (examples only)
|
|
1223
|
-
if (testFile.startsWith("examples/")) {
|
|
1224
|
-
const testFileName = path.basename(testFile);
|
|
1225
|
-
console.log(
|
|
1226
|
-
`TESTDRIVER_EXAMPLE_URL::${testFileName}::${consoleUrl}/runs/${testRunDbId}/${testCaseDbId}`,
|
|
1227
|
-
);
|
|
1228
|
-
}
|
|
1347
|
+
|
|
1229
1348
|
} catch (error) {
|
|
1230
1349
|
logger.error("Failed to report test case:", error.message);
|
|
1231
1350
|
}
|
|
@@ -1244,12 +1363,20 @@ class TestDriverReporter {
|
|
|
1244
1363
|
* @returns {string} The corresponding web console URL
|
|
1245
1364
|
*/
|
|
1246
1365
|
function getConsoleUrl(apiRoot) {
|
|
1247
|
-
//
|
|
1248
|
-
if (process.env.
|
|
1366
|
+
// Explicit override — use TD_CONSOLE_URL when deliberately set
|
|
1367
|
+
if (process.env.TD_CONSOLE_URL) return process.env.TD_CONSOLE_URL;
|
|
1249
1368
|
|
|
1250
1369
|
if (!apiRoot) return "https://console.testdriver.ai";
|
|
1251
1370
|
|
|
1252
|
-
//
|
|
1371
|
+
// Fly.io: swap "-api" for "-web" in the hostname
|
|
1372
|
+
// e.g. preview-138-api.fly.dev -> preview-138-web.fly.dev
|
|
1373
|
+
// td-test-api.fly.dev -> td-test-web.fly.dev
|
|
1374
|
+
const flyMatch = apiRoot.match(/https:\/\/([\w-]+)-api\.fly\.dev/);
|
|
1375
|
+
if (flyMatch) {
|
|
1376
|
+
return `https://${flyMatch[1]}-web.fly.dev`;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
// Known channel API URLs -> console equivalents
|
|
1253
1380
|
// e.g. https://api-canary.testdriver.ai -> https://console-canary.testdriver.ai
|
|
1254
1381
|
for (const url of Object.values(channelConfig.channels)) {
|
|
1255
1382
|
if (url === apiRoot) {
|
|
@@ -1257,27 +1384,19 @@ function getConsoleUrl(apiRoot) {
|
|
|
1257
1384
|
}
|
|
1258
1385
|
}
|
|
1259
1386
|
|
|
1260
|
-
// Local development
|
|
1387
|
+
// Local development
|
|
1261
1388
|
if (apiRoot.includes("ngrok.io") || apiRoot.includes("trycloudflare.com") || apiRoot.includes("localhost")) {
|
|
1262
|
-
return
|
|
1389
|
+
return "http://localhost:3001";
|
|
1263
1390
|
}
|
|
1264
1391
|
|
|
1265
|
-
// Render PR previews
|
|
1266
|
-
// canary-api-pr-123.onrender.com -> canary-web-pr-123.onrender.com
|
|
1267
|
-
// testdriver-api-i4m4-pr-123.onrender.com -> web-i4m4-pr-123.onrender.com
|
|
1392
|
+
// Render PR previews (legacy)
|
|
1268
1393
|
const renderPrMatch = apiRoot.match(/https:\/\/([\w-]+)-api(-[\w]+)?(-pr-\d+)?\.onrender\.com/);
|
|
1269
1394
|
if (renderPrMatch) {
|
|
1270
1395
|
const [, prefix, suffix, prSuffix] = renderPrMatch;
|
|
1271
|
-
|
|
1272
|
-
if (prefix === 'testdriver' && suffix) {
|
|
1273
|
-
webPrefix = 'web' + suffix;
|
|
1274
|
-
} else {
|
|
1275
|
-
webPrefix = prefix + '-web';
|
|
1276
|
-
}
|
|
1396
|
+
const webPrefix = (prefix === 'testdriver' && suffix) ? 'web' + suffix : prefix + '-web';
|
|
1277
1397
|
return `https://${webPrefix}${prSuffix || ''}.onrender.com`;
|
|
1278
1398
|
}
|
|
1279
1399
|
|
|
1280
|
-
// Other tunnels or unknown hosts: return as-is
|
|
1281
1400
|
return apiRoot;
|
|
1282
1401
|
}
|
|
1283
1402
|
|
package/lib/core/Dashcam.js
CHANGED
|
@@ -104,7 +104,7 @@ class Dashcam {
|
|
|
104
104
|
return `https://console-${envMatch[1]}.testdriver.ai`;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
// Production: API on
|
|
107
|
+
// Production: API on custom domain or v6 -> Console on testdriver.ai
|
|
108
108
|
if (
|
|
109
109
|
apiRoot.includes("api.testdriver.ai") ||
|
|
110
110
|
apiRoot.includes("v6.testdriver.ai")
|
|
@@ -117,21 +117,31 @@ class Dashcam {
|
|
|
117
117
|
return "http://localhost:3001";
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
// Fly.io PR previews: map API app to Web app
|
|
121
|
+
// pr-123-api.fly.dev -> pr-123-web.fly.dev
|
|
122
|
+
const flyPrMatch = apiRoot.match(/https:\/\/(pr-\d+)-api\.fly\.dev/);
|
|
123
|
+
if (flyPrMatch) {
|
|
124
|
+
const [, prPrefix] = flyPrMatch;
|
|
125
|
+
return `https://${prPrefix}-web.fly.dev`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Fly.io environment apps: test-api.fly.dev -> test-web.fly.dev
|
|
129
|
+
const flyEnvMatch = apiRoot.match(/https:\/\/([\w-]+)-api\.fly\.dev/);
|
|
130
|
+
if (flyEnvMatch) {
|
|
131
|
+
const [, prefix] = flyEnvMatch;
|
|
132
|
+
return `https://${prefix}-web.fly.dev`;
|
|
133
|
+
}
|
|
134
|
+
|
|
120
135
|
// Render PR previews: map API service to Web service
|
|
121
136
|
// canary-api-pr-123.onrender.com -> canary-web-pr-123.onrender.com
|
|
122
137
|
// testdriver-api-i4m4-pr-123.onrender.com -> web-i4m4-pr-123.onrender.com
|
|
123
138
|
const renderPrMatch = apiRoot.match(/https:\/\/([\w-]+)-api(-[\w]+)?(-pr-\d+)?\.onrender\.com/);
|
|
124
139
|
if (renderPrMatch) {
|
|
125
140
|
const [, prefix, suffix, prSuffix] = renderPrMatch;
|
|
126
|
-
// Map API naming to Web naming:
|
|
127
|
-
// canary-api -> canary-web
|
|
128
|
-
// testdriver-api-i4m4 -> web-i4m4
|
|
129
141
|
let webPrefix;
|
|
130
142
|
if (prefix === 'testdriver' && suffix) {
|
|
131
|
-
// testdriver-api-i4m4 -> web-i4m4
|
|
132
143
|
webPrefix = 'web' + suffix;
|
|
133
144
|
} else {
|
|
134
|
-
// canary-api -> canary-web
|
|
135
145
|
webPrefix = prefix + '-web';
|
|
136
146
|
}
|
|
137
147
|
return `https://${webPrefix}${prSuffix || ''}.onrender.com`;
|
|
@@ -147,20 +157,13 @@ class Dashcam {
|
|
|
147
157
|
* @private
|
|
148
158
|
*/
|
|
149
159
|
async _getDashcamPath() {
|
|
150
|
-
const shell = this._getShell();
|
|
151
160
|
|
|
152
161
|
if (this.client.os === "windows") {
|
|
153
162
|
return "C:\\Program Files\\nodejs\\dashcam.cmd";
|
|
163
|
+
} else {
|
|
164
|
+
return "/usr/bin/dashcam";
|
|
154
165
|
}
|
|
155
166
|
|
|
156
|
-
const npmPrefix = await this.client.exec(
|
|
157
|
-
shell,
|
|
158
|
-
"npm prefix -g",
|
|
159
|
-
40000,
|
|
160
|
-
process.env.TD_DEBUG == "true" ? false : true,
|
|
161
|
-
);
|
|
162
|
-
|
|
163
|
-
return npmPrefix.trim() + "/bin/dashcam";
|
|
164
167
|
}
|
|
165
168
|
|
|
166
169
|
/**
|
|
@@ -289,13 +292,17 @@ class Dashcam {
|
|
|
289
292
|
const apiRoot = this._getApiRoot();
|
|
290
293
|
|
|
291
294
|
if (this.client.os === "windows") {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
295
|
+
try {
|
|
296
|
+
const addLogOutput = await this.client.exec(
|
|
297
|
+
shell,
|
|
298
|
+
`$env:TD_API_ROOT="${apiRoot}"; & "${dashcamPath}" logs --add --type=web --pattern="${pattern}" --name="${name}"`,
|
|
299
|
+
120000,
|
|
300
|
+
process.env.TD_DEBUG == "true" ? false : true,
|
|
301
|
+
);
|
|
302
|
+
this._log("debug", "Add web log tracking output:", addLogOutput);
|
|
303
|
+
} catch (err) {
|
|
304
|
+
this._log("warn", "Add web log tracking failed:", err.message);
|
|
305
|
+
}
|
|
299
306
|
} else {
|
|
300
307
|
const addLogOutput = await this.client.exec(
|
|
301
308
|
shell,
|
|
@@ -449,7 +456,7 @@ class Dashcam {
|
|
|
449
456
|
this.recording = false;
|
|
450
457
|
|
|
451
458
|
// Extract the /replay/... path from CLI output and reconstruct the URL
|
|
452
|
-
// using getConsoleUrl(). The CLI may return a wrong domain
|
|
459
|
+
// using getConsoleUrl(). The CLI may return a wrong domain
|
|
453
460
|
// so we always rewrite the base URL to match the current environment.
|
|
454
461
|
if (output) {
|
|
455
462
|
// Match /replay/{id} with optional query params from any URL or broken prefix
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"dev": {
|
|
3
|
+
"apiRoot": "https://api-dev.testdriver.ai",
|
|
4
|
+
"consoleUrl": "https://console-dev.testdriver.ai",
|
|
5
|
+
"tdEnv": "dev"
|
|
6
|
+
},
|
|
7
|
+
"test": {
|
|
8
|
+
"apiRoot": "https://api-test.testdriver.ai",
|
|
9
|
+
"consoleUrl": "https://console-test.testdriver.ai",
|
|
10
|
+
"tdEnv": "staging"
|
|
11
|
+
},
|
|
12
|
+
"canary": {
|
|
13
|
+
"apiRoot": "https://api-canary.testdriver.ai",
|
|
14
|
+
"consoleUrl": "https://console-canary.testdriver.ai",
|
|
15
|
+
"tdEnv": "production"
|
|
16
|
+
},
|
|
17
|
+
"stable": {
|
|
18
|
+
"apiRoot": "https://api.testdriver.ai",
|
|
19
|
+
"consoleUrl": "https://console.testdriver.ai",
|
|
20
|
+
"tdEnv": "production"
|
|
21
|
+
}
|
|
22
|
+
}
|