testdriverai 7.2.91 → 7.3.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.
- package/agent/index.js +139 -60
- package/agent/lib/analytics.js +4 -1
- package/agent/lib/commands.js +252 -252
- package/agent/lib/redraw.js +3 -14
- package/agent/lib/sandbox.js +1 -1
- package/ai/skills/testdriver:aws-setup/SKILL.md +4 -4
- package/ai/skills/testdriver:captcha/SKILL.md +1 -1
- package/ai/skills/testdriver:ci-cd/SKILL.md +23 -23
- package/ai/skills/testdriver:cloud/SKILL.md +1 -1
- package/ai/skills/testdriver:customizing-devices/SKILL.md +5 -5
- package/ai/skills/testdriver:running-tests/SKILL.md +11 -11
- package/ai/skills/testdriver:secrets/SKILL.md +1 -1
- package/ai/skills/testdriver:testdriver/SKILL.md +5 -5
- package/ai/skills/testdriver:variables/SKILL.md +3 -3
- package/debugger/index.html +0 -36
- package/package.json +1 -1
- package/sdk-log-formatter.js +8 -1
- package/sdk.d.ts +86 -2
- package/sdk.js +123 -24
package/agent/index.js
CHANGED
|
@@ -214,9 +214,6 @@ class TestDriverAgent extends EventEmitter2 {
|
|
|
214
214
|
// Ignore sandbox close errors during exit
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
|
-
|
|
218
|
-
// Clean up IDE session file
|
|
219
|
-
this.cleanupIdeSessionFile();
|
|
220
217
|
|
|
221
218
|
shouldRunPostrun =
|
|
222
219
|
!this.hasRunPostrun &&
|
|
@@ -440,10 +437,13 @@ class TestDriverAgent extends EventEmitter2 {
|
|
|
440
437
|
this.emitter.emit(events.log.narration, theme.dim("checking..."));
|
|
441
438
|
|
|
442
439
|
// check asks the ai if the task is complete
|
|
443
|
-
|
|
440
|
+
// Parallelize system calls for better performance
|
|
441
|
+
const [thisScreenshot, mousePosition, activeWindow] = await Promise.all([
|
|
442
|
+
this.system.captureScreenBase64(1, false, true),
|
|
443
|
+
this.system.getMousePosition(),
|
|
444
|
+
this.system.activeWin(),
|
|
445
|
+
]);
|
|
444
446
|
let images = [this.lastScreenshot, thisScreenshot];
|
|
445
|
-
let mousePosition = await this.system.getMousePosition();
|
|
446
|
-
let activeWindow = await this.system.activeWin();
|
|
447
447
|
|
|
448
448
|
let response = await this.sdk.req("check", {
|
|
449
449
|
tasks: this.tasks,
|
|
@@ -905,12 +905,18 @@ commands:
|
|
|
905
905
|
|
|
906
906
|
this.emitter.emit(events.log.narration, theme.dim("thinking..."), true);
|
|
907
907
|
|
|
908
|
-
|
|
908
|
+
// Parallelize system calls for better performance
|
|
909
|
+
const [screenshot, mousePosition, activeWindow] = await Promise.all([
|
|
910
|
+
this.system.captureScreenBase64(),
|
|
911
|
+
this.system.getMousePosition(),
|
|
912
|
+
this.system.activeWin(),
|
|
913
|
+
]);
|
|
914
|
+
this.lastScreenshot = screenshot;
|
|
909
915
|
|
|
910
916
|
let message = await this.sdk.req("input", {
|
|
911
917
|
input: currentTask,
|
|
912
|
-
mousePosition
|
|
913
|
-
activeWindow
|
|
918
|
+
mousePosition,
|
|
919
|
+
activeWindow,
|
|
914
920
|
image: this.lastScreenshot,
|
|
915
921
|
});
|
|
916
922
|
|
|
@@ -941,13 +947,15 @@ commands:
|
|
|
941
947
|
|
|
942
948
|
this.emitter.emit(events.log.narration, theme.dim("thinking..."), true);
|
|
943
949
|
|
|
944
|
-
let image = await this.system.captureScreenBase64();
|
|
945
|
-
|
|
946
950
|
const streamId = `generate-${Date.now()}`;
|
|
947
951
|
this.emitter.emit(events.log.markdown.start, streamId);
|
|
948
952
|
|
|
949
|
-
|
|
950
|
-
|
|
953
|
+
// Parallelize system calls for better performance
|
|
954
|
+
const [image, mouse, activeWindow] = await Promise.all([
|
|
955
|
+
this.system.captureScreenBase64(),
|
|
956
|
+
this.system.getMousePosition(),
|
|
957
|
+
this.system.activeWin(),
|
|
958
|
+
]);
|
|
951
959
|
|
|
952
960
|
let message = await this.sdk.req(
|
|
953
961
|
"generate",
|
|
@@ -2042,8 +2050,8 @@ ${regression}
|
|
|
2042
2050
|
const previewMode = this.config.TD_PREVIEW || "browser";
|
|
2043
2051
|
|
|
2044
2052
|
if (previewMode === "ide") {
|
|
2045
|
-
//
|
|
2046
|
-
this.
|
|
2053
|
+
// Send session to VS Code extension via HTTP
|
|
2054
|
+
this.sendIdeSessionNotification(urlToOpen, data);
|
|
2047
2055
|
} else if (previewMode !== "none") {
|
|
2048
2056
|
// Open in browser (default behavior)
|
|
2049
2057
|
this.emitter.emit(events.showWindow, urlToOpen);
|
|
@@ -2052,64 +2060,135 @@ ${regression}
|
|
|
2052
2060
|
}
|
|
2053
2061
|
}
|
|
2054
2062
|
|
|
2055
|
-
//
|
|
2056
|
-
|
|
2063
|
+
// Find the VS Code instance that contains the test file
|
|
2064
|
+
findTargetIdeInstance(testFilePath) {
|
|
2057
2065
|
const fs = require("fs");
|
|
2058
2066
|
const os = require("os");
|
|
2059
2067
|
const path = require("path");
|
|
2060
2068
|
|
|
2061
|
-
const
|
|
2062
|
-
const sessionsDir = path.join(sessionDir, "ide-sessions");
|
|
2069
|
+
const instancesDir = path.join(os.homedir(), ".testdriver", "ide-instances");
|
|
2063
2070
|
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
.replace(/\.[^/.]+$/, ""); // Remove file extension
|
|
2068
|
-
const sessionId = `${testFileName}-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
|
|
2069
|
-
const sessionFile = path.join(sessionsDir, `${sessionId}.json`);
|
|
2070
|
-
|
|
2071
|
-
try {
|
|
2072
|
-
// Ensure directories exist
|
|
2073
|
-
if (!fs.existsSync(sessionDir)) {
|
|
2074
|
-
fs.mkdirSync(sessionDir, { recursive: true });
|
|
2075
|
-
}
|
|
2076
|
-
if (!fs.existsSync(sessionsDir)) {
|
|
2077
|
-
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
2078
|
-
}
|
|
2071
|
+
if (!fs.existsSync(instancesDir)) {
|
|
2072
|
+
return null;
|
|
2073
|
+
}
|
|
2079
2074
|
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
os: data.os || this.sandboxOs || "linux",
|
|
2086
|
-
timestamp: Date.now(),
|
|
2087
|
-
};
|
|
2075
|
+
const files = fs.readdirSync(instancesDir);
|
|
2076
|
+
const normalizedTestPath = testFilePath ? path.normalize(testFilePath) : null;
|
|
2077
|
+
|
|
2078
|
+
let matchingInstance = null;
|
|
2079
|
+
let longestMatchLength = 0;
|
|
2088
2080
|
|
|
2089
|
-
|
|
2090
|
-
|
|
2081
|
+
for (const file of files) {
|
|
2082
|
+
if (!file.endsWith('.json')) continue;
|
|
2091
2083
|
|
|
2092
|
-
// Store session file path for cleanup on exit
|
|
2093
|
-
this._ideSessionFile = sessionFile;
|
|
2094
|
-
} catch (error) {
|
|
2095
|
-
logger.warn(`Failed to write IDE session file: ${error.message}`);
|
|
2096
|
-
}
|
|
2097
|
-
}
|
|
2098
|
-
|
|
2099
|
-
// Clean up IDE session file when test completes
|
|
2100
|
-
cleanupIdeSessionFile() {
|
|
2101
|
-
if (this._ideSessionFile) {
|
|
2102
|
-
const fs = require("fs");
|
|
2103
2084
|
try {
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2085
|
+
const registrationPath = path.join(instancesDir, file);
|
|
2086
|
+
const registration = JSON.parse(fs.readFileSync(registrationPath, 'utf-8'));
|
|
2087
|
+
|
|
2088
|
+
// Check if this instance is still alive (registration within last 60 seconds or process exists)
|
|
2089
|
+
const isRecent = Date.now() - registration.timestamp < 60000;
|
|
2090
|
+
|
|
2091
|
+
// Skip stale registrations
|
|
2092
|
+
if (!isRecent) {
|
|
2093
|
+
// Try to clean up stale file
|
|
2094
|
+
try { fs.unlinkSync(registrationPath); } catch {}
|
|
2095
|
+
continue;
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
// If we have a test file path, find the best matching workspace
|
|
2099
|
+
if (normalizedTestPath && registration.workspacePaths) {
|
|
2100
|
+
for (const workspacePath of registration.workspacePaths) {
|
|
2101
|
+
const normalizedWorkspace = path.normalize(workspacePath);
|
|
2102
|
+
if (normalizedTestPath.startsWith(normalizedWorkspace + path.sep) ||
|
|
2103
|
+
normalizedTestPath === normalizedWorkspace) {
|
|
2104
|
+
// Prefer longest match (most specific workspace)
|
|
2105
|
+
if (normalizedWorkspace.length > longestMatchLength) {
|
|
2106
|
+
longestMatchLength = normalizedWorkspace.length;
|
|
2107
|
+
matchingInstance = registration;
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
} else if (!matchingInstance) {
|
|
2112
|
+
// If no test file path, just use the first available instance
|
|
2113
|
+
matchingInstance = registration;
|
|
2107
2114
|
}
|
|
2108
2115
|
} catch (error) {
|
|
2109
|
-
// Ignore
|
|
2116
|
+
// Ignore malformed registration files
|
|
2110
2117
|
}
|
|
2111
|
-
this._ideSessionFile = null;
|
|
2112
2118
|
}
|
|
2119
|
+
|
|
2120
|
+
return matchingInstance;
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
// Send session notification to VS Code extension via HTTP
|
|
2124
|
+
sendIdeSessionNotification(debuggerUrl, data) {
|
|
2125
|
+
const http = require("http");
|
|
2126
|
+
const path = require("path");
|
|
2127
|
+
|
|
2128
|
+
const testFilePath = data.testFile || this.thisFile;
|
|
2129
|
+
const targetInstance = this.findTargetIdeInstance(testFilePath);
|
|
2130
|
+
|
|
2131
|
+
if (!targetInstance) {
|
|
2132
|
+
logger.warn("No VS Code instance found for IDE preview. Make sure VS Code with TestDriver extension is open.");
|
|
2133
|
+
// Fall back to browser
|
|
2134
|
+
this.emitter.emit(events.showWindow, debuggerUrl);
|
|
2135
|
+
return;
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
// Generate a unique session ID
|
|
2139
|
+
const testFileName = (testFilePath || "test")
|
|
2140
|
+
.split(path.sep).pop()
|
|
2141
|
+
.replace(/\.[^/.]+$/, "");
|
|
2142
|
+
const sessionId = `${testFileName}-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
|
|
2143
|
+
|
|
2144
|
+
const sessionData = {
|
|
2145
|
+
sessionId: sessionId,
|
|
2146
|
+
debuggerUrl: debuggerUrl,
|
|
2147
|
+
resolution: data.resolution || this.config.TD_RESOLUTION,
|
|
2148
|
+
testFile: testFilePath,
|
|
2149
|
+
os: data.os || this.sandboxOs || "linux",
|
|
2150
|
+
timestamp: Date.now(),
|
|
2151
|
+
};
|
|
2152
|
+
|
|
2153
|
+
const postData = JSON.stringify(sessionData);
|
|
2154
|
+
|
|
2155
|
+
const options = {
|
|
2156
|
+
hostname: '127.0.0.1',
|
|
2157
|
+
port: targetInstance.port,
|
|
2158
|
+
path: '/session',
|
|
2159
|
+
method: 'POST',
|
|
2160
|
+
headers: {
|
|
2161
|
+
'Content-Type': 'application/json',
|
|
2162
|
+
'Content-Length': Buffer.byteLength(postData)
|
|
2163
|
+
},
|
|
2164
|
+
timeout: 5000
|
|
2165
|
+
};
|
|
2166
|
+
|
|
2167
|
+
const req = http.request(options, (res) => {
|
|
2168
|
+
if (res.statusCode === 200) {
|
|
2169
|
+
logger.log(`IDE session notification sent to port ${targetInstance.port}`);
|
|
2170
|
+
} else {
|
|
2171
|
+
logger.warn(`IDE session notification failed with status ${res.statusCode}`);
|
|
2172
|
+
// Fall back to browser on failure
|
|
2173
|
+
this.emitter.emit(events.showWindow, debuggerUrl);
|
|
2174
|
+
}
|
|
2175
|
+
});
|
|
2176
|
+
|
|
2177
|
+
req.on('error', (error) => {
|
|
2178
|
+
logger.warn(`Failed to send IDE session notification: ${error.message}`);
|
|
2179
|
+
// Fall back to browser on error
|
|
2180
|
+
this.emitter.emit(events.showWindow, debuggerUrl);
|
|
2181
|
+
});
|
|
2182
|
+
|
|
2183
|
+
req.on('timeout', () => {
|
|
2184
|
+
req.destroy();
|
|
2185
|
+
logger.warn('IDE session notification timed out');
|
|
2186
|
+
// Fall back to browser on timeout
|
|
2187
|
+
this.emitter.emit(events.showWindow, debuggerUrl);
|
|
2188
|
+
});
|
|
2189
|
+
|
|
2190
|
+
req.write(postData);
|
|
2191
|
+
req.end();
|
|
2113
2192
|
}
|
|
2114
2193
|
|
|
2115
2194
|
async connectToSandboxService() {
|
package/agent/lib/analytics.js
CHANGED
|
@@ -10,7 +10,10 @@ const createAnalytics = (emitter, config, sessionInstance) => {
|
|
|
10
10
|
return;
|
|
11
11
|
}
|
|
12
12
|
if (Math.random() <= 0.01) {
|
|
13
|
-
|
|
13
|
+
// Fire-and-forget: don't await analytics calls
|
|
14
|
+
sdk.req("analytics", { event, data }).catch((err) => {
|
|
15
|
+
console.warn("Analytics track failed:", err.message);
|
|
16
|
+
});
|
|
14
17
|
}
|
|
15
18
|
},
|
|
16
19
|
};
|