testdriverai 7.2.2 → 7.2.9
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/.github/workflows/publish.yaml +15 -7
- package/.github/workflows/testdriver.yml +36 -0
- package/agent/index.js +28 -109
- package/bin/testdriverai.js +8 -0
- package/debugger/index.html +37 -0
- package/docs/docs.json +2 -11
- package/docs/v7/_drafts/architecture.mdx +1 -26
- package/docs/v7/_drafts/provision.mdx +251 -188
- package/docs/v7/_drafts/quick-start-test-recording.mdx +0 -1
- package/docs/v7/_drafts/test-recording.mdx +0 -6
- package/docs/v7/api/act.mdx +1 -0
- package/docs/v7/getting-started/quickstart.mdx +9 -16
- package/interfaces/cli/commands/init.js +33 -19
- package/interfaces/cli/lib/base.js +24 -0
- package/interfaces/cli.js +8 -1
- package/interfaces/logger.js +8 -3
- package/interfaces/vitest-plugin.mjs +16 -71
- package/lib/sentry.js +343 -0
- package/lib/vitest/hooks.mjs +23 -31
- package/package.json +4 -3
- package/sdk-log-formatter.js +41 -0
- package/sdk.js +335 -94
- package/test/testdriver/act.test.mjs +30 -0
- package/test/testdriver/assert.test.mjs +1 -1
- package/test/testdriver/hover-text.test.mjs +1 -1
- package/test/testdriver/installer.test.mjs +47 -0
- package/test/testdriver/launch-vscode-linux.test.mjs +55 -0
- package/test/testdriver/setup/testHelpers.mjs +8 -118
- package/tests/example.test.js +33 -0
- package/tests/login.js +28 -0
- package/vitest.config.js +18 -0
- package/vitest.config.mjs +1 -0
- package/agent/lib/cache.js +0 -142
package/sdk.js
CHANGED
|
@@ -263,6 +263,48 @@ class ElementNotFoundError extends Error {
|
|
|
263
263
|
}
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
+
/**
|
|
267
|
+
* Custom error class for act() failures
|
|
268
|
+
* Includes task execution details and retry information
|
|
269
|
+
*/
|
|
270
|
+
class ActError extends Error {
|
|
271
|
+
/**
|
|
272
|
+
* @param {string} message - Error message
|
|
273
|
+
* @param {Object} details - Additional details about the failure
|
|
274
|
+
* @param {string} details.task - The task that was attempted
|
|
275
|
+
* @param {number} details.tries - Number of check attempts made
|
|
276
|
+
* @param {number} details.maxTries - Maximum tries that were allowed
|
|
277
|
+
* @param {number} details.duration - Total execution time in milliseconds
|
|
278
|
+
* @param {Error} [details.cause] - The underlying error that caused the failure
|
|
279
|
+
*/
|
|
280
|
+
constructor(message, details = {}) {
|
|
281
|
+
super(message);
|
|
282
|
+
this.name = "ActError";
|
|
283
|
+
this.task = details.task;
|
|
284
|
+
this.tries = details.tries;
|
|
285
|
+
this.maxTries = details.maxTries;
|
|
286
|
+
this.duration = details.duration;
|
|
287
|
+
this.cause = details.cause;
|
|
288
|
+
this.timestamp = new Date().toISOString();
|
|
289
|
+
|
|
290
|
+
// Capture stack trace
|
|
291
|
+
if (Error.captureStackTrace) {
|
|
292
|
+
Error.captureStackTrace(this, ActError);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Enhance error message with execution details
|
|
296
|
+
this.message += `\n\n=== Act Execution Details ===`;
|
|
297
|
+
this.message += `\nTask: "${this.task}"`;
|
|
298
|
+
this.message += `\nTries: ${this.tries}/${this.maxTries}`;
|
|
299
|
+
this.message += `\nDuration: ${this.duration}ms`;
|
|
300
|
+
this.message += `\nTimestamp: ${this.timestamp}`;
|
|
301
|
+
|
|
302
|
+
if (this.cause) {
|
|
303
|
+
this.message += `\nUnderlying error: ${this.cause.message}`;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
266
308
|
/**
|
|
267
309
|
* Element class representing a located or to-be-located element
|
|
268
310
|
*/
|
|
@@ -1702,26 +1744,49 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
1702
1744
|
* @returns {Promise<void>}
|
|
1703
1745
|
*/
|
|
1704
1746
|
vscode: async (options = {}) => {
|
|
1705
|
-
|
|
1747
|
+
// Automatically wait for connection to be ready
|
|
1748
|
+
await this.ready();
|
|
1706
1749
|
|
|
1707
1750
|
const {
|
|
1708
1751
|
workspace = null,
|
|
1709
1752
|
extensions = [],
|
|
1710
1753
|
} = options;
|
|
1711
1754
|
|
|
1755
|
+
const shell = this.os === 'windows' ? 'pwsh' : 'sh';
|
|
1756
|
+
|
|
1757
|
+
// If dashcam is available, set up file logging
|
|
1758
|
+
if (this._dashcam) {
|
|
1759
|
+
// Create the log file on the remote machine
|
|
1760
|
+
const logPath = this.os === "windows"
|
|
1761
|
+
? "C:\\Users\\testdriver\\Documents\\testdriver.log"
|
|
1762
|
+
: "/tmp/testdriver.log";
|
|
1763
|
+
|
|
1764
|
+
const createLogCmd = this.os === "windows"
|
|
1765
|
+
? `New-Item -ItemType File -Path "${logPath}" -Force | Out-Null`
|
|
1766
|
+
: `touch ${logPath}`;
|
|
1767
|
+
|
|
1768
|
+
await this.exec(shell, createLogCmd, 10000, true);
|
|
1769
|
+
await this._dashcam.addFileLog(logPath, "TestDriver Log");
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
// Automatically start dashcam if not already recording
|
|
1773
|
+
if (!this._dashcam || !this._dashcam.recording) {
|
|
1774
|
+
await this.dashcam.start();
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1712
1777
|
// Install extensions if provided
|
|
1713
1778
|
for (const extension of extensions) {
|
|
1714
|
-
|
|
1779
|
+
console.log(`[provision.vscode] Installing extension: ${extension}`);
|
|
1715
1780
|
await this.exec(
|
|
1716
1781
|
shell,
|
|
1717
|
-
`code --install-extension ${extension}`,
|
|
1718
|
-
|
|
1782
|
+
`code --install-extension ${extension} --force`,
|
|
1783
|
+
120000,
|
|
1719
1784
|
true
|
|
1720
1785
|
);
|
|
1786
|
+
console.log(`[provision.vscode] ✅ Extension installed: ${extension}`);
|
|
1721
1787
|
}
|
|
1722
1788
|
|
|
1723
1789
|
// Launch VS Code
|
|
1724
|
-
const shell = this.os === 'windows' ? 'pwsh' : 'sh';
|
|
1725
1790
|
const workspaceArg = workspace ? `"${workspace}"` : '';
|
|
1726
1791
|
|
|
1727
1792
|
if (this.os === 'windows') {
|
|
@@ -1738,10 +1803,148 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
1738
1803
|
);
|
|
1739
1804
|
}
|
|
1740
1805
|
|
|
1806
|
+
// Wait for VS Code to start up
|
|
1807
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
1808
|
+
|
|
1741
1809
|
// Wait for VS Code to be ready
|
|
1742
1810
|
await this.focusApplication('Visual Studio Code');
|
|
1743
1811
|
},
|
|
1744
1812
|
|
|
1813
|
+
/**
|
|
1814
|
+
* Download and install an application
|
|
1815
|
+
* @param {Object} options - Installer options
|
|
1816
|
+
* @param {string} options.url - URL to download the installer from
|
|
1817
|
+
* @param {string} [options.filename] - Filename to save as (auto-detected from URL if not provided)
|
|
1818
|
+
* @param {string} [options.appName] - Application name to focus after install
|
|
1819
|
+
* @param {boolean} [options.launch=true] - Whether to launch the app after installation
|
|
1820
|
+
* @returns {Promise<string>} Path to the downloaded file
|
|
1821
|
+
* @example
|
|
1822
|
+
* // Install a .deb package on Linux (auto-detected)
|
|
1823
|
+
* await testdriver.provision.installer({
|
|
1824
|
+
* url: 'https://example.com/app.deb',
|
|
1825
|
+
* appName: 'MyApp'
|
|
1826
|
+
* });
|
|
1827
|
+
*
|
|
1828
|
+
* @example
|
|
1829
|
+
* // Download and run custom commands
|
|
1830
|
+
* const filePath = await testdriver.provision.installer({
|
|
1831
|
+
* url: 'https://example.com/app.AppImage',
|
|
1832
|
+
* launch: false
|
|
1833
|
+
* });
|
|
1834
|
+
* await testdriver.exec('sh', `chmod +x "${filePath}" && "${filePath}" &`, 10000);
|
|
1835
|
+
*/
|
|
1836
|
+
installer: async (options = {}) => {
|
|
1837
|
+
// Automatically wait for connection to be ready
|
|
1838
|
+
await this.ready();
|
|
1839
|
+
|
|
1840
|
+
const {
|
|
1841
|
+
url,
|
|
1842
|
+
filename,
|
|
1843
|
+
appName,
|
|
1844
|
+
launch = true,
|
|
1845
|
+
} = options;
|
|
1846
|
+
|
|
1847
|
+
if (!url) {
|
|
1848
|
+
throw new Error('[provision.installer] url is required');
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
const shell = this.os === 'windows' ? 'pwsh' : 'sh';
|
|
1852
|
+
|
|
1853
|
+
// If dashcam is available, set up file logging
|
|
1854
|
+
if (this._dashcam) {
|
|
1855
|
+
const logPath = this.os === "windows"
|
|
1856
|
+
? "C:\\Users\\testdriver\\Documents\\testdriver.log"
|
|
1857
|
+
: "/tmp/testdriver.log";
|
|
1858
|
+
|
|
1859
|
+
const createLogCmd = this.os === "windows"
|
|
1860
|
+
? `New-Item -ItemType File -Path "${logPath}" -Force | Out-Null`
|
|
1861
|
+
: `touch ${logPath}`;
|
|
1862
|
+
|
|
1863
|
+
await this.exec(shell, createLogCmd, 10000, true);
|
|
1864
|
+
await this._dashcam.addFileLog(logPath, "TestDriver Log");
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
// Automatically start dashcam if not already recording
|
|
1868
|
+
if (!this._dashcam || !this._dashcam.recording) {
|
|
1869
|
+
await this.dashcam.start();
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
// Determine filename from URL if not provided
|
|
1873
|
+
const urlObj = new URL(url);
|
|
1874
|
+
const detectedFilename = filename || urlObj.pathname.split('/').pop() || 'installer';
|
|
1875
|
+
|
|
1876
|
+
// Determine download directory and full path
|
|
1877
|
+
const downloadDir = this.os === 'windows'
|
|
1878
|
+
? 'C:\\Users\\testdriver\\Downloads'
|
|
1879
|
+
: '/tmp';
|
|
1880
|
+
const filePath = this.os === 'windows'
|
|
1881
|
+
? `${downloadDir}\\${detectedFilename}`
|
|
1882
|
+
: `${downloadDir}/${detectedFilename}`;
|
|
1883
|
+
|
|
1884
|
+
console.log(`[provision.installer] Downloading ${url}...`);
|
|
1885
|
+
|
|
1886
|
+
// Download the file
|
|
1887
|
+
if (this.os === 'windows') {
|
|
1888
|
+
await this.exec(
|
|
1889
|
+
shell,
|
|
1890
|
+
`Invoke-WebRequest -Uri "${url}" -OutFile "${filePath}"`,
|
|
1891
|
+
300000, // 5 min timeout for download
|
|
1892
|
+
true
|
|
1893
|
+
);
|
|
1894
|
+
} else {
|
|
1895
|
+
await this.exec(
|
|
1896
|
+
shell,
|
|
1897
|
+
`curl -L -o "${filePath}" "${url}"`,
|
|
1898
|
+
300000,
|
|
1899
|
+
true
|
|
1900
|
+
);
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
console.log(`[provision.installer] ✅ Downloaded to ${filePath}`);
|
|
1904
|
+
|
|
1905
|
+
// Auto-detect install command based on file extension
|
|
1906
|
+
const ext = detectedFilename.split('.').pop()?.toLowerCase();
|
|
1907
|
+
let installCommand = null;
|
|
1908
|
+
|
|
1909
|
+
if (this.os === 'windows') {
|
|
1910
|
+
if (ext === 'msi') {
|
|
1911
|
+
installCommand = `Start-Process msiexec -ArgumentList '/i', '"${filePath}"', '/quiet', '/norestart' -Wait`;
|
|
1912
|
+
} else if (ext === 'exe') {
|
|
1913
|
+
installCommand = `Start-Process "${filePath}" -ArgumentList '/S' -Wait`;
|
|
1914
|
+
}
|
|
1915
|
+
} else if (this.os === 'linux') {
|
|
1916
|
+
if (ext === 'deb') {
|
|
1917
|
+
installCommand = `sudo dpkg -i "${filePath}" && sudo apt-get install -f -y`;
|
|
1918
|
+
} else if (ext === 'rpm') {
|
|
1919
|
+
installCommand = `sudo rpm -i "${filePath}"`;
|
|
1920
|
+
} else if (ext === 'appimage') {
|
|
1921
|
+
installCommand = `chmod +x "${filePath}"`;
|
|
1922
|
+
} else if (ext === 'sh') {
|
|
1923
|
+
installCommand = `chmod +x "${filePath}" && "${filePath}"`;
|
|
1924
|
+
}
|
|
1925
|
+
} else if (this.os === 'darwin') {
|
|
1926
|
+
if (ext === 'dmg') {
|
|
1927
|
+
installCommand = `hdiutil attach "${filePath}" -mountpoint /Volumes/installer && cp -R /Volumes/installer/*.app /Applications/ && hdiutil detach /Volumes/installer`;
|
|
1928
|
+
} else if (ext === 'pkg') {
|
|
1929
|
+
installCommand = `sudo installer -pkg "${filePath}" -target /`;
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
if (installCommand) {
|
|
1934
|
+
console.log(`[provision.installer] Installing...`);
|
|
1935
|
+
await this.exec(shell, installCommand, 300000, true);
|
|
1936
|
+
console.log(`[provision.installer] ✅ Installation complete`);
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
// Launch and focus the app if appName is provided and launch is true
|
|
1940
|
+
if (appName && launch) {
|
|
1941
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
1942
|
+
await this.focusApplication(appName);
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
return filePath;
|
|
1946
|
+
},
|
|
1947
|
+
|
|
1745
1948
|
/**
|
|
1746
1949
|
* Launch Electron app
|
|
1747
1950
|
* @param {Object} options - Electron launch options
|
|
@@ -2027,10 +2230,7 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
2027
2230
|
const absoluteTimestamp = Date.now();
|
|
2028
2231
|
const startTime = absoluteTimestamp;
|
|
2029
2232
|
|
|
2030
|
-
// Log finding all action
|
|
2031
2233
|
const { events } = require("./agent/events.js");
|
|
2032
|
-
const findingMessage = formatter.formatElementsFinding(description);
|
|
2033
|
-
this.emitter.emit(events.log.log, findingMessage);
|
|
2034
2234
|
|
|
2035
2235
|
try {
|
|
2036
2236
|
const screenshot = await this.system.captureScreenBase64();
|
|
@@ -2096,16 +2296,16 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
2096
2296
|
const duration = Date.now() - startTime;
|
|
2097
2297
|
|
|
2098
2298
|
if (response && response.elements && response.elements.length > 0) {
|
|
2099
|
-
//
|
|
2100
|
-
const
|
|
2299
|
+
// Single log at the end - found elements
|
|
2300
|
+
const formattedMessage = formatter.formatFindAllSingleLine(
|
|
2101
2301
|
description,
|
|
2102
2302
|
response.elements.length,
|
|
2103
2303
|
{
|
|
2104
|
-
duration:
|
|
2304
|
+
duration: duration,
|
|
2105
2305
|
cacheHit: response.cached || false,
|
|
2106
2306
|
},
|
|
2107
2307
|
);
|
|
2108
|
-
this.emitter.emit(events.log.
|
|
2308
|
+
this.emitter.emit(events.log.narration, formattedMessage, true);
|
|
2109
2309
|
|
|
2110
2310
|
// Create Element instances for each found element
|
|
2111
2311
|
const elements = response.elements.map((elementData) => {
|
|
@@ -2155,7 +2355,6 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
2155
2355
|
|
|
2156
2356
|
// Log debug information when elements are found
|
|
2157
2357
|
if (process.env.VERBOSE || process.env.DEBUG || process.env.TD_DEBUG) {
|
|
2158
|
-
const { events } = require("./agent/events.js");
|
|
2159
2358
|
this.emitter.emit(
|
|
2160
2359
|
events.log.debug,
|
|
2161
2360
|
`✓ Found ${elements.length} element(s): "${description}"`,
|
|
@@ -2169,6 +2368,19 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
2169
2368
|
|
|
2170
2369
|
return elements;
|
|
2171
2370
|
} else {
|
|
2371
|
+
const duration = Date.now() - startTime;
|
|
2372
|
+
|
|
2373
|
+
// Single log at the end - no elements found
|
|
2374
|
+
const formattedMessage = formatter.formatFindAllSingleLine(
|
|
2375
|
+
description,
|
|
2376
|
+
0,
|
|
2377
|
+
{
|
|
2378
|
+
duration: duration,
|
|
2379
|
+
cacheHit: response?.cached || false,
|
|
2380
|
+
},
|
|
2381
|
+
);
|
|
2382
|
+
this.emitter.emit(events.log.narration, formattedMessage, true);
|
|
2383
|
+
|
|
2172
2384
|
// No elements found - track interaction (fire-and-forget, don't block)
|
|
2173
2385
|
const sessionId = this.getSessionId();
|
|
2174
2386
|
if (sessionId && this.sandbox?.send) {
|
|
@@ -2193,6 +2405,18 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
2193
2405
|
return [];
|
|
2194
2406
|
}
|
|
2195
2407
|
} catch (error) {
|
|
2408
|
+
const duration = Date.now() - startTime;
|
|
2409
|
+
|
|
2410
|
+
// Single log at the end - error
|
|
2411
|
+
const formattedMessage = formatter.formatFindAllSingleLine(
|
|
2412
|
+
description,
|
|
2413
|
+
0,
|
|
2414
|
+
{
|
|
2415
|
+
duration: duration,
|
|
2416
|
+
},
|
|
2417
|
+
);
|
|
2418
|
+
this.emitter.emit(events.log.narration, formattedMessage, true);
|
|
2419
|
+
|
|
2196
2420
|
// Track findAll error interaction (fire-and-forget, don't block)
|
|
2197
2421
|
const sessionId = this.getSessionId();
|
|
2198
2422
|
if (sessionId && this.sandbox?.send) {
|
|
@@ -2210,8 +2434,6 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
2210
2434
|
});
|
|
2211
2435
|
}
|
|
2212
2436
|
|
|
2213
|
-
const { events } = require("./agent/events.js");
|
|
2214
|
-
this.emitter.emit(events.log.log, `Error in findAll: ${error.message}`);
|
|
2215
2437
|
return [];
|
|
2216
2438
|
}
|
|
2217
2439
|
}
|
|
@@ -2606,17 +2828,21 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
2606
2828
|
createMarkdownLogger(this.emitter);
|
|
2607
2829
|
|
|
2608
2830
|
// Set up basic event logging
|
|
2831
|
+
// Note: We only console.log here - the console spy in vitest/hooks.mjs
|
|
2832
|
+
// handles forwarding to sandbox. This prevents duplicate output to server.
|
|
2609
2833
|
this.emitter.on("log:**", (message) => {
|
|
2610
2834
|
const event = this.emitter.event;
|
|
2835
|
+
|
|
2836
|
+
if (event.includes("markdown")) {
|
|
2837
|
+
return;
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2611
2840
|
if (event === events.log.debug && !debugMode) return;
|
|
2612
2841
|
if (this.loggingEnabled && message) {
|
|
2613
2842
|
const prefixedMessage = this.testContext
|
|
2614
2843
|
? `[${this.testContext}] ${message}`
|
|
2615
2844
|
: message;
|
|
2616
2845
|
console.log(prefixedMessage);
|
|
2617
|
-
|
|
2618
|
-
// Also forward to sandbox for dashcam
|
|
2619
|
-
this._forwardLogToSandbox(prefixedMessage);
|
|
2620
2846
|
}
|
|
2621
2847
|
});
|
|
2622
2848
|
|
|
@@ -2675,36 +2901,6 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
2675
2901
|
});
|
|
2676
2902
|
}
|
|
2677
2903
|
|
|
2678
|
-
/**
|
|
2679
|
-
* Forward log message to sandbox for debugger display
|
|
2680
|
-
* @private
|
|
2681
|
-
* @param {string} message - Log message to forward
|
|
2682
|
-
*/
|
|
2683
|
-
_forwardLogToSandbox(message) {
|
|
2684
|
-
try {
|
|
2685
|
-
// Only forward if sandbox is connected
|
|
2686
|
-
if (this.sandbox && this.sandbox.instanceSocketConnected) {
|
|
2687
|
-
// Don't send objects as they cause base64 encoding errors
|
|
2688
|
-
if (typeof message === "object") {
|
|
2689
|
-
return;
|
|
2690
|
-
}
|
|
2691
|
-
|
|
2692
|
-
// Add test context prefix if available
|
|
2693
|
-
const prefixedMessage = this.testContext
|
|
2694
|
-
? `[${this.testContext}] ${message}`
|
|
2695
|
-
: message;
|
|
2696
|
-
|
|
2697
|
-
this.sandbox.send({
|
|
2698
|
-
type: "output",
|
|
2699
|
-
output: Buffer.from(prefixedMessage).toString("base64"),
|
|
2700
|
-
});
|
|
2701
|
-
}
|
|
2702
|
-
} catch {
|
|
2703
|
-
// Silently fail to avoid breaking the log flow
|
|
2704
|
-
// console.error("Error forwarding log to sandbox:", error);
|
|
2705
|
-
}
|
|
2706
|
-
}
|
|
2707
|
-
|
|
2708
2904
|
/**
|
|
2709
2905
|
* Open URL in default browser
|
|
2710
2906
|
* @private
|
|
@@ -2790,39 +2986,11 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
2790
2986
|
const platform = options.platform || this.config.TD_PLATFORM || "linux";
|
|
2791
2987
|
|
|
2792
2988
|
// Auto-detect sandbox ID from the active sandbox if not provided
|
|
2793
|
-
|
|
2989
|
+
// For E2B (Linux), the instance has sandboxId; for AWS (Windows), it has instanceId
|
|
2990
|
+
const sandboxId = options.sandboxId || this.instance?.sandboxId || this.instance?.instanceId || this.agent?.sandboxId || null;
|
|
2794
2991
|
|
|
2795
2992
|
// Get or create session ID using the agent's newSession method
|
|
2796
2993
|
let sessionId = this.agent?.sessionInstance?.get() || null;
|
|
2797
|
-
|
|
2798
|
-
// If no session exists, create one using the agent's method
|
|
2799
|
-
if (!sessionId && this.agent?.newSession) {
|
|
2800
|
-
try {
|
|
2801
|
-
await this.agent.newSession();
|
|
2802
|
-
sessionId = this.agent.sessionInstance.get();
|
|
2803
|
-
|
|
2804
|
-
// Save session ID to file for reuse across test runs
|
|
2805
|
-
if (sessionId) {
|
|
2806
|
-
const sessionFile = path.join(os.homedir(), '.testdriverai-session');
|
|
2807
|
-
fs.writeFileSync(sessionFile, sessionId, { encoding: 'utf-8' });
|
|
2808
|
-
}
|
|
2809
|
-
} catch (error) {
|
|
2810
|
-
// Log but don't fail - tests can run without a session
|
|
2811
|
-
console.warn('Failed to create session:', error.message);
|
|
2812
|
-
}
|
|
2813
|
-
}
|
|
2814
|
-
|
|
2815
|
-
// If still no session, try reading from file (for reporter/separate processes)
|
|
2816
|
-
if (!sessionId) {
|
|
2817
|
-
try {
|
|
2818
|
-
const sessionFile = path.join(os.homedir(), '.testdriverai-session');
|
|
2819
|
-
if (fs.existsSync(sessionFile)) {
|
|
2820
|
-
sessionId = fs.readFileSync(sessionFile, 'utf-8').trim();
|
|
2821
|
-
}
|
|
2822
|
-
} catch (error) {
|
|
2823
|
-
// Ignore file read errors
|
|
2824
|
-
}
|
|
2825
|
-
}
|
|
2826
2994
|
|
|
2827
2995
|
const data = {
|
|
2828
2996
|
runId: options.runId,
|
|
@@ -2946,26 +3114,98 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
2946
3114
|
* This is the SDK equivalent of the CLI's exploratory loop
|
|
2947
3115
|
*
|
|
2948
3116
|
* @param {string} task - Natural language description of what to do
|
|
2949
|
-
* @param {Object} options - Execution options
|
|
2950
|
-
* @param {
|
|
2951
|
-
* @returns {Promise<
|
|
3117
|
+
* @param {Object} [options] - Execution options
|
|
3118
|
+
* @param {number} [options.tries=7] - Maximum number of check/retry attempts before giving up
|
|
3119
|
+
* @returns {Promise<ActResult>} Result object with success status and details
|
|
3120
|
+
* @throws {ActError} When the task fails after all tries are exhausted
|
|
3121
|
+
*
|
|
3122
|
+
* @typedef {Object} ActResult
|
|
3123
|
+
* @property {boolean} success - Whether the task completed successfully
|
|
3124
|
+
* @property {string} task - The original task that was executed
|
|
3125
|
+
* @property {number} tries - Number of check attempts made
|
|
3126
|
+
* @property {number} maxTries - Maximum tries that were allowed
|
|
3127
|
+
* @property {number} duration - Total execution time in milliseconds
|
|
3128
|
+
* @property {string} [response] - AI's final response if available
|
|
2952
3129
|
*
|
|
2953
3130
|
* @example
|
|
2954
3131
|
* // Simple execution
|
|
2955
|
-
* await client.act('Click the submit button');
|
|
3132
|
+
* const result = await client.act('Click the submit button');
|
|
3133
|
+
* console.log(result.success); // true
|
|
3134
|
+
*
|
|
3135
|
+
* @example
|
|
3136
|
+
* // With custom retry limit
|
|
3137
|
+
* const result = await client.act('Fill out the contact form', { tries: 10 });
|
|
3138
|
+
* console.log(`Completed in ${result.tries} tries`);
|
|
2956
3139
|
*
|
|
2957
3140
|
* @example
|
|
2958
|
-
* //
|
|
2959
|
-
*
|
|
2960
|
-
*
|
|
3141
|
+
* // Handle failures
|
|
3142
|
+
* try {
|
|
3143
|
+
* await client.act('Complete the checkout process', { tries: 3 });
|
|
3144
|
+
* } catch (error) {
|
|
3145
|
+
* console.log(`Failed after ${error.tries} tries: ${error.message}`);
|
|
3146
|
+
* }
|
|
2961
3147
|
*/
|
|
2962
|
-
async act(task) {
|
|
3148
|
+
async act(task, options = {}) {
|
|
2963
3149
|
this._ensureConnected();
|
|
2964
3150
|
|
|
2965
|
-
|
|
3151
|
+
const { tries = 7 } = options;
|
|
3152
|
+
|
|
3153
|
+
this.analytics.track("sdk.act", { task, tries });
|
|
2966
3154
|
|
|
2967
|
-
|
|
2968
|
-
|
|
3155
|
+
const { events } = require("./agent/events.js");
|
|
3156
|
+
const startTime = Date.now();
|
|
3157
|
+
|
|
3158
|
+
// Store original checkLimit and set custom one if provided
|
|
3159
|
+
const originalCheckLimit = this.agent.checkLimit;
|
|
3160
|
+
this.agent.checkLimit = tries;
|
|
3161
|
+
|
|
3162
|
+
// Reset check count for this act() call
|
|
3163
|
+
const originalCheckCount = this.agent.checkCount;
|
|
3164
|
+
this.agent.checkCount = 0;
|
|
3165
|
+
|
|
3166
|
+
// Emit scoped start marker for act()
|
|
3167
|
+
this.emitter.emit(events.log.log, formatter.formatActStart(task));
|
|
3168
|
+
|
|
3169
|
+
try {
|
|
3170
|
+
// Use the agent's exploratoryLoop method directly
|
|
3171
|
+
const response = await this.agent.exploratoryLoop(task, false, true, false);
|
|
3172
|
+
|
|
3173
|
+
const duration = Date.now() - startTime;
|
|
3174
|
+
const triesUsed = this.agent.checkCount;
|
|
3175
|
+
|
|
3176
|
+
this.emitter.emit(events.log.log, formatter.formatActComplete(duration, true));
|
|
3177
|
+
|
|
3178
|
+
// Restore original checkLimit
|
|
3179
|
+
this.agent.checkLimit = originalCheckLimit;
|
|
3180
|
+
this.agent.checkCount = originalCheckCount;
|
|
3181
|
+
|
|
3182
|
+
return {
|
|
3183
|
+
success: true,
|
|
3184
|
+
task,
|
|
3185
|
+
tries: triesUsed,
|
|
3186
|
+
maxTries: tries,
|
|
3187
|
+
duration,
|
|
3188
|
+
response: response || undefined,
|
|
3189
|
+
};
|
|
3190
|
+
} catch (error) {
|
|
3191
|
+
const duration = Date.now() - startTime;
|
|
3192
|
+
const triesUsed = this.agent.checkCount;
|
|
3193
|
+
|
|
3194
|
+
this.emitter.emit(events.log.log, formatter.formatActComplete(duration, false, error.message));
|
|
3195
|
+
|
|
3196
|
+
// Restore original checkLimit
|
|
3197
|
+
this.agent.checkLimit = originalCheckLimit;
|
|
3198
|
+
this.agent.checkCount = originalCheckCount;
|
|
3199
|
+
|
|
3200
|
+
// Create an enhanced error with additional context using ActError class
|
|
3201
|
+
throw new ActError(`Act failed: ${error.message}`, {
|
|
3202
|
+
task,
|
|
3203
|
+
tries: triesUsed,
|
|
3204
|
+
maxTries: tries,
|
|
3205
|
+
duration,
|
|
3206
|
+
cause: error,
|
|
3207
|
+
});
|
|
3208
|
+
}
|
|
2969
3209
|
}
|
|
2970
3210
|
|
|
2971
3211
|
/**
|
|
@@ -2973,15 +3213,16 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
2973
3213
|
* Execute a natural language task using AI
|
|
2974
3214
|
*
|
|
2975
3215
|
* @param {string} task - Natural language description of what to do
|
|
2976
|
-
* @param {Object} options - Execution options
|
|
2977
|
-
* @param {
|
|
2978
|
-
* @returns {Promise<
|
|
3216
|
+
* @param {Object} [options] - Execution options
|
|
3217
|
+
* @param {number} [options.tries=7] - Maximum number of check/retry attempts
|
|
3218
|
+
* @returns {Promise<ActResult>} Result object with success status and details
|
|
2979
3219
|
*/
|
|
2980
|
-
async ai(task) {
|
|
2981
|
-
return await this.act(task);
|
|
3220
|
+
async ai(task, options) {
|
|
3221
|
+
return await this.act(task, options);
|
|
2982
3222
|
}
|
|
2983
3223
|
}
|
|
2984
3224
|
|
|
2985
3225
|
module.exports = TestDriverSDK;
|
|
2986
3226
|
module.exports.Element = Element;
|
|
2987
3227
|
module.exports.ElementNotFoundError = ElementNotFoundError;
|
|
3228
|
+
module.exports.ActError = ActError;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TestDriver SDK - Act Test (Vitest)
|
|
3
|
+
* Tests the AI exploratory loop (act) functionality
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
import { TestDriver } from "../../lib/vitest/hooks.mjs";
|
|
8
|
+
|
|
9
|
+
describe("Act Test", () => {
|
|
10
|
+
it("should use act to search for testdriver on Google", async (context) => {
|
|
11
|
+
const testdriver = TestDriver(context, { newSandbox: true });
|
|
12
|
+
|
|
13
|
+
// provision.chrome() automatically calls ready() and starts dashcam
|
|
14
|
+
await testdriver.provision.chrome({
|
|
15
|
+
url: 'https://www.google.com',
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Use act to search for testdriver
|
|
19
|
+
let actRes = await testdriver.act("click on the empty search box, type 'testdriver', and hit enter. do not click the plus button in the search bar");
|
|
20
|
+
|
|
21
|
+
console.log("Act response:", actRes);
|
|
22
|
+
|
|
23
|
+
// Assert the search results are displayed
|
|
24
|
+
const result = await testdriver.assert(
|
|
25
|
+
"search results for testdriver are visible",
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
expect(result).toBeTruthy();
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -8,7 +8,7 @@ import { TestDriver } from "../../lib/vitest/hooks.mjs";
|
|
|
8
8
|
|
|
9
9
|
describe("Assert Test", () => {
|
|
10
10
|
it("should assert the testdriver login page shows", async (context) => {
|
|
11
|
-
const testdriver = TestDriver(context, { newSandbox: true });
|
|
11
|
+
const testdriver = TestDriver(context, { newSandbox: true, headless: false });
|
|
12
12
|
|
|
13
13
|
// provision.chrome() automatically calls ready() and starts dashcam
|
|
14
14
|
await testdriver.provision.chrome({
|
|
@@ -8,7 +8,7 @@ import { TestDriver } from "../../lib/vitest/hooks.mjs";
|
|
|
8
8
|
|
|
9
9
|
describe("Hover Text Test", () => {
|
|
10
10
|
it("should click Sign In and verify error message", async (context) => {
|
|
11
|
-
const testdriver = TestDriver(context, { headless:
|
|
11
|
+
const testdriver = TestDriver(context, { headless: false, newSandbox: true, cacheKey: 'hover-text-test' });
|
|
12
12
|
await testdriver.provision.chrome({ url: 'http://testdriver-sandbox.vercel.app/login' });
|
|
13
13
|
|
|
14
14
|
// Click on Sign In button using new find() API
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TestDriver SDK - Installer Test (Vitest)
|
|
3
|
+
* Tests the provision.installer() method for downloading and installing apps
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
import { TestDriver } from "../../lib/vitest/hooks.mjs";
|
|
8
|
+
|
|
9
|
+
describe("Provision Installer", () => {
|
|
10
|
+
it(
|
|
11
|
+
"should download and install a .deb package on Linux",
|
|
12
|
+
async (context) => {
|
|
13
|
+
const testdriver = TestDriver(context, { newSandbox: true });
|
|
14
|
+
|
|
15
|
+
// Install bat (a cat clone with syntax highlighting) using provision.installer
|
|
16
|
+
const filePath = await testdriver.provision.installer({
|
|
17
|
+
url: 'https://github.com/sharkdp/bat/releases/download/v0.24.0/bat_0.24.0_amd64.deb',
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Verify the file was downloaded
|
|
21
|
+
expect(filePath).toContain('bat');
|
|
22
|
+
|
|
23
|
+
// Verify bat was installed by running it
|
|
24
|
+
await testdriver.exec('sh', 'bat --version', 10000);
|
|
25
|
+
},
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
it(
|
|
29
|
+
"should download a shell script and verify it exists",
|
|
30
|
+
async (context) => {
|
|
31
|
+
const testdriver = TestDriver(context, { newSandbox: true });
|
|
32
|
+
|
|
33
|
+
// Download a shell script (nvm installer)
|
|
34
|
+
const filePath = await testdriver.provision.installer({
|
|
35
|
+
url: 'https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh',
|
|
36
|
+
launch: false, // Don't auto-run the script
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Verify the file was downloaded
|
|
40
|
+
expect(filePath).toContain('install.sh');
|
|
41
|
+
|
|
42
|
+
// Verify the file is executable
|
|
43
|
+
const result = await testdriver.exec('sh', `ls -la "${filePath}"`, 10000);
|
|
44
|
+
expect(result).toBeTruthy();
|
|
45
|
+
},
|
|
46
|
+
);
|
|
47
|
+
});
|