testronaut 1.0.7 โ 1.0.8
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/README.md +10 -6
- package/bin/cli.js +39 -1
- package/bin/createWelcomeMission.js +2 -2
- package/core/agent.js +21 -5
- package/core/turnLoop.js +62 -13
- package/package.json +1 -1
- package/runner/testronaut.js +7 -6
- package/tools/chromeBrowser.js +3 -3
- package/tools/generateHtmlReport.js +52 -0
package/README.md
CHANGED
|
@@ -47,7 +47,8 @@ import { runMissions } from 'testronaut';
|
|
|
47
47
|
|
|
48
48
|
export const loginMission = `
|
|
49
49
|
Visit ${process.env.URL}.
|
|
50
|
-
Fill
|
|
50
|
+
Fill the username field with ${process.env.USERNAME}.
|
|
51
|
+
Fill the password field with ${process.env.PASSWORD}.
|
|
51
52
|
Submit the form.
|
|
52
53
|
Wait for the dashboard to appear.
|
|
53
54
|
Report SUCCESS if the dashboard is loaded, otherwise report FAILURE.
|
|
@@ -56,7 +57,7 @@ Report SUCCESS if the dashboard is loaded, otherwise report FAILURE.
|
|
|
56
57
|
export async function executeMission() {
|
|
57
58
|
await runMissions({
|
|
58
59
|
mission: loginMission
|
|
59
|
-
});
|
|
60
|
+
}, "Login Mission");
|
|
60
61
|
}
|
|
61
62
|
```
|
|
62
63
|
|
|
@@ -89,7 +90,7 @@ export async function executeMission() {
|
|
|
89
90
|
preMission: [loginMission, navigateToContactFormMission],
|
|
90
91
|
mission: fillContactFormMission,
|
|
91
92
|
postMission: logoutMission,
|
|
92
|
-
});
|
|
93
|
+
}, "Fill Contact Form Mission");
|
|
93
94
|
}
|
|
94
95
|
```
|
|
95
96
|
|
|
@@ -97,12 +98,12 @@ export async function executeMission() {
|
|
|
97
98
|
Run all missions in the missions/ directory:
|
|
98
99
|
|
|
99
100
|
```
|
|
100
|
-
testronaut
|
|
101
|
+
npx testronaut
|
|
101
102
|
```
|
|
102
103
|
Run a specific file:
|
|
103
104
|
|
|
104
105
|
```
|
|
105
|
-
testronaut login.mission.js
|
|
106
|
+
npx testronaut login.mission.js
|
|
106
107
|
```
|
|
107
108
|
|
|
108
109
|
๐งฐ Available Helpers
|
|
@@ -122,6 +123,9 @@ runSuite(objectives)
|
|
|
122
123
|
```
|
|
123
124
|
โ Executes all registered test objectives
|
|
124
125
|
|
|
126
|
+
### ๐ Reports
|
|
127
|
+
HTML and JSON reports automatically created in the ```missions/mission_reports/``` directory for each testronaut run
|
|
128
|
+
|
|
125
129
|
### ๐งช Under the Hood
|
|
126
130
|
Uses Playwright for browser automation
|
|
127
131
|
|
|
@@ -141,4 +145,4 @@ Now you can use testronaut in any local project.
|
|
|
141
145
|
### ๐ License
|
|
142
146
|
MIT
|
|
143
147
|
|
|
144
|
-
๐ค Built with โค๏ธ by Shane Fast
|
|
148
|
+
๐ค Built with โค๏ธ by [Shane Fast](https://github.com/scfast)
|
package/bin/cli.js
CHANGED
|
@@ -4,12 +4,17 @@ import path from 'path';
|
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import { initializeTestronautProject } from './init.js';
|
|
6
6
|
import { createWelcomeMission } from './createWelcomeMission.js';
|
|
7
|
+
import { generateHtmlReport } from '../tools/generateHtmlReport.js';
|
|
7
8
|
|
|
8
9
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
10
|
|
|
10
11
|
const args = process.argv.slice(2);
|
|
11
12
|
const missionsDir = path.resolve(process.cwd(), 'missions');
|
|
12
13
|
|
|
14
|
+
const allResults = [];
|
|
15
|
+
const runId = `run_${Date.now()}`;
|
|
16
|
+
const startTime = new Date();
|
|
17
|
+
|
|
13
18
|
const HELP_TEXT = `
|
|
14
19
|
๐งโ๐ testronaut - Autonomous Agent Mission Runner
|
|
15
20
|
|
|
@@ -50,7 +55,11 @@ const runFile = async (filePath) => {
|
|
|
50
55
|
const missionsModule = await import(`file://${modulePath}`);
|
|
51
56
|
|
|
52
57
|
if (typeof missionsModule.executeMission === 'function') {
|
|
53
|
-
await missionsModule.executeMission();
|
|
58
|
+
const result = await missionsModule.executeMission();
|
|
59
|
+
allResults.push({
|
|
60
|
+
file: filePath,
|
|
61
|
+
result
|
|
62
|
+
});
|
|
54
63
|
}
|
|
55
64
|
} catch (err) {
|
|
56
65
|
console.error(`โ Error running mission: ${filePath}`);
|
|
@@ -70,3 +79,32 @@ if (args.length > 0) {
|
|
|
70
79
|
await runFile(file);
|
|
71
80
|
}
|
|
72
81
|
}
|
|
82
|
+
|
|
83
|
+
const endTime = new Date();
|
|
84
|
+
|
|
85
|
+
const flatMissions = allResults.flatMap(entry => {
|
|
86
|
+
const result = entry.result;
|
|
87
|
+
const missions = Array.isArray(result) ? result : [result]; // normalize
|
|
88
|
+
return missions.map(m => ({
|
|
89
|
+
...m,
|
|
90
|
+
file: entry.file
|
|
91
|
+
}));
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
const report = {
|
|
96
|
+
runId,
|
|
97
|
+
startTime: startTime.toISOString(),
|
|
98
|
+
endTime: endTime.toISOString(),
|
|
99
|
+
summary: {
|
|
100
|
+
totalMissions: flatMissions.length,
|
|
101
|
+
passed: flatMissions.filter(m => m.status === 'passed').length,
|
|
102
|
+
failed: flatMissions.filter(m => m.status === 'failed').length,
|
|
103
|
+
},
|
|
104
|
+
missions: flatMissions
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const outputDir = './missions/mission_reports';
|
|
108
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
109
|
+
fs.writeFileSync(`${outputDir}/${runId}.json`, JSON.stringify(report, null, 2));
|
|
110
|
+
generateHtmlReport(report, `${outputDir}/${runId}.html`);
|
package/core/agent.js
CHANGED
|
@@ -29,14 +29,14 @@ import { turnLoop } from './turnLoop.js';
|
|
|
29
29
|
// }
|
|
30
30
|
// }
|
|
31
31
|
|
|
32
|
-
export async function runAgent(goals, maxTurns = 20) {
|
|
32
|
+
export async function runAgent(goals, missionName, maxTurns = 20, ) {
|
|
33
33
|
const browser = new ChromeBrowser();
|
|
34
34
|
await browser.start();
|
|
35
|
+
const startTime = Date.now();
|
|
35
36
|
|
|
36
37
|
try {
|
|
37
38
|
for (const goal of goals) {
|
|
38
|
-
console.log(`\n๐ Running Agent with goal:\n${goal}\n`);
|
|
39
|
-
|
|
39
|
+
// console.log(`\n๐ Running Agent with goal:\n${goal}\n`);
|
|
40
40
|
const messages = [
|
|
41
41
|
{
|
|
42
42
|
role: 'system',
|
|
@@ -59,12 +59,28 @@ export async function runAgent(goals, maxTurns = 20) {
|
|
|
59
59
|
const success = await turnLoop(browser, messages, maxTurns);
|
|
60
60
|
if (!success) {
|
|
61
61
|
console.log('๐ Agent stopped due to failed goal.\n');
|
|
62
|
-
|
|
62
|
+
const endTime = Date.now();
|
|
63
|
+
return {
|
|
64
|
+
missionName: missionName,
|
|
65
|
+
status: 'failed',
|
|
66
|
+
steps: [ /* tool steps or key moments */ ],
|
|
67
|
+
startTime: startTime,
|
|
68
|
+
endTime: endTime,
|
|
69
|
+
}
|
|
70
|
+
;
|
|
63
71
|
}
|
|
64
72
|
}
|
|
65
73
|
|
|
66
74
|
console.log('โ
All goals completed successfully.\n');
|
|
67
|
-
|
|
75
|
+
const endTime = Date.now();
|
|
76
|
+
return {
|
|
77
|
+
missionName: missionName,
|
|
78
|
+
status: 'passed',
|
|
79
|
+
steps: [ /* tool steps or key moments */ ],
|
|
80
|
+
startTime: startTime,
|
|
81
|
+
endTime: endTime,
|
|
82
|
+
}
|
|
83
|
+
;
|
|
68
84
|
|
|
69
85
|
} finally {
|
|
70
86
|
await browser.close();
|
package/core/turnLoop.js
CHANGED
|
@@ -43,7 +43,7 @@ const pushDOMAssistant = async (browser, messages, agentMemory, { skipIfLastTool
|
|
|
43
43
|
|
|
44
44
|
const domHtml = await CHROME_TOOL_MAP.get_dom(browser, { limit: 100000, exclude: true }, agentMemory);
|
|
45
45
|
await tokenEstimate('gpt-4o', domHtml);
|
|
46
|
-
fs.writeFileSync(`missions/mission_reports/debug-expanded-${Date.now()}.html`, domHtml);
|
|
46
|
+
// fs.writeFileSync(`missions/mission_reports/debug-expanded-${Date.now()}.html`, domHtml);
|
|
47
47
|
|
|
48
48
|
messages.push({
|
|
49
49
|
role: 'tool',
|
|
@@ -103,6 +103,24 @@ export const turnLoop = async (browser, messages, maxTurns, currentTurn = 0, ret
|
|
|
103
103
|
// }))
|
|
104
104
|
// );
|
|
105
105
|
|
|
106
|
+
const unrespondedCalls = messages.filter(
|
|
107
|
+
(msg, i) =>
|
|
108
|
+
msg.role === 'assistant' &&
|
|
109
|
+
msg.tool_calls?.length &&
|
|
110
|
+
!msg.tool_calls.every(call =>
|
|
111
|
+
messages.slice(i + 1).some(
|
|
112
|
+
m => m.role === 'tool' && m.tool_call_id === call.id
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
if (unrespondedCalls.length) {
|
|
118
|
+
console.error('๐ Detected assistant tool calls without matching tool responses:');
|
|
119
|
+
console.dir(unrespondedCalls, { depth: 5 });
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
106
124
|
response = await openai.chat.completions.create({
|
|
107
125
|
model: 'gpt-4o',
|
|
108
126
|
messages,
|
|
@@ -123,6 +141,7 @@ export const turnLoop = async (browser, messages, maxTurns, currentTurn = 0, ret
|
|
|
123
141
|
} else if (err.status === 400) {
|
|
124
142
|
console.error('โ Bad request:', err.message);
|
|
125
143
|
console.log("current tools", toolsSchema);
|
|
144
|
+
console.log("messages: ", messages)
|
|
126
145
|
return false;
|
|
127
146
|
} else {
|
|
128
147
|
throw err;
|
|
@@ -148,20 +167,31 @@ export const turnLoop = async (browser, messages, maxTurns, currentTurn = 0, ret
|
|
|
148
167
|
|
|
149
168
|
const msg = response.choices[0].message;
|
|
150
169
|
|
|
170
|
+
// if (!msg.tool_calls?.length) {
|
|
171
|
+
// console.error('โ Assistant message had empty or missing tool_calls array');
|
|
172
|
+
// return false;
|
|
173
|
+
// }
|
|
174
|
+
|
|
151
175
|
if (msg.tool_calls?.length) {
|
|
152
176
|
console.log('Processing tool calls...');
|
|
153
|
-
messages.push(msg);
|
|
177
|
+
// messages.push(msg);
|
|
178
|
+
const toolResponses = [];
|
|
154
179
|
let lastToolName = null;
|
|
180
|
+
|
|
155
181
|
for (const call of msg.tool_calls) {
|
|
182
|
+
const toolCallId = call.id;
|
|
183
|
+
// const hasResponse = messages.some(m =>
|
|
184
|
+
// m.role === 'tool' && m.tool_call_id === toolCallId
|
|
185
|
+
// );
|
|
156
186
|
const fnName = call.function.name;
|
|
157
187
|
lastToolName = fnName;
|
|
158
188
|
const args = JSON.parse(call.function.arguments || '{}');
|
|
159
189
|
console.log(`[model] โ ${fnName}`, args);
|
|
160
|
-
console.log('Calling tool:', fnName);
|
|
190
|
+
// console.log('Calling tool:', fnName);
|
|
161
191
|
let result;
|
|
162
192
|
let errorMessage = null;
|
|
163
193
|
try {
|
|
164
|
-
console.log("agentMemory: ", agentMemory);
|
|
194
|
+
// console.log("agentMemory: ", agentMemory);
|
|
165
195
|
result = await CHROME_TOOL_MAP[fnName](browser, args, agentMemory);
|
|
166
196
|
|
|
167
197
|
// Smart DOM-ready wait after major actions like click
|
|
@@ -186,21 +216,30 @@ export const turnLoop = async (browser, messages, maxTurns, currentTurn = 0, ret
|
|
|
186
216
|
|
|
187
217
|
const truncated = result.length > 100 ? result.slice(0, 100) + 'โฆ' : result;
|
|
188
218
|
|
|
189
|
-
if (result.length > 100) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
} else {
|
|
219
|
+
// if (result.length > 100) {
|
|
220
|
+
// // const logFile = `missions/mission_reports/tool-result-${Date.now()}.log`;
|
|
221
|
+
// // fs.writeFileSync(logFile, result);
|
|
222
|
+
// console.log(`[tool ] โ ${truncated} (full output written to ${logFile})`);
|
|
223
|
+
// } else {
|
|
194
224
|
console.log(`[tool ] โ ${truncated}`);
|
|
195
|
-
}
|
|
225
|
+
// }
|
|
226
|
+
|
|
227
|
+
// messages.push({
|
|
228
|
+
// role: 'tool',
|
|
229
|
+
// tool_call_id: call.id,
|
|
230
|
+
// name: fnName,
|
|
231
|
+
// type: 'function',
|
|
232
|
+
// content: result,
|
|
233
|
+
// });
|
|
196
234
|
|
|
197
|
-
|
|
235
|
+
toolResponses.push({
|
|
198
236
|
role: 'tool',
|
|
199
237
|
tool_call_id: call.id,
|
|
200
238
|
name: fnName,
|
|
201
239
|
type: 'function',
|
|
202
240
|
content: result,
|
|
203
241
|
});
|
|
242
|
+
|
|
204
243
|
if (fnName === 'expand_menu') {
|
|
205
244
|
agentMemory.lastMenuExpanded = true;
|
|
206
245
|
}
|
|
@@ -213,13 +252,23 @@ export const turnLoop = async (browser, messages, maxTurns, currentTurn = 0, ret
|
|
|
213
252
|
focus: [], // or try focusing on ['main'] to keep it light
|
|
214
253
|
}, agentMemory);
|
|
215
254
|
await tokenEstimate('gpt-4o', domHtml);
|
|
216
|
-
fs.writeFileSync(`missions/mission_reports/debug-expanded-${Date.now()}.html`, domHtml);
|
|
255
|
+
// fs.writeFileSync(`missions/mission_reports/debug-expanded-${Date.now()}.html`, domHtml);
|
|
217
256
|
await pushDOMAssistant(browser, messages, agentMemory, {
|
|
218
257
|
skipIfLastTool: ['get_dom', 'check_text']
|
|
219
258
|
});
|
|
220
259
|
console.log(`[auto] โ DOM size after ${fnName}: ${domHtml.length} chars`);
|
|
221
260
|
}
|
|
222
|
-
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
// if (!hasResponse) {
|
|
265
|
+
// console.error(`โ Tool call ${toolCallId} for ${call.function.name} was not followed by a response`);
|
|
266
|
+
// return false;
|
|
267
|
+
// }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Only after ALL tool responses are ready
|
|
271
|
+
messages.push(msg, ...toolResponses);
|
|
223
272
|
|
|
224
273
|
continue;
|
|
225
274
|
}
|
package/package.json
CHANGED
package/runner/testronaut.js
CHANGED
|
@@ -48,7 +48,7 @@ export async function launch(preMissionSetup) {
|
|
|
48
48
|
// console.log('\nโ
Mission flow complete.');
|
|
49
49
|
// }
|
|
50
50
|
|
|
51
|
-
export async function runMissions({ preMission, mission, postMission }) {
|
|
51
|
+
export async function runMissions({ preMission, mission, postMission }, missionName) {
|
|
52
52
|
const normalizeToArray = (x) => (Array.isArray(x) ? x : x ? [x] : []);
|
|
53
53
|
|
|
54
54
|
const pre = normalizeToArray(preMission);
|
|
@@ -58,17 +58,17 @@ export async function runMissions({ preMission, mission, postMission }) {
|
|
|
58
58
|
const goals = [];
|
|
59
59
|
|
|
60
60
|
if (pre.length) {
|
|
61
|
-
console.log('\n๐งญ Running pre-mission setup:', pre);
|
|
61
|
+
// console.log('\n๐งญ Running pre-mission setup:', pre);
|
|
62
62
|
goals.push(...pre.map(fn => ({ goal: fn, label: 'pre-mission' })));
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
if (main.length) {
|
|
66
|
-
console.log('\n๐งญ Running main mission(s):', main);
|
|
66
|
+
// console.log('\n๐งญ Running main mission(s):', main);
|
|
67
67
|
goals.push(...main.map(fn => ({ goal: fn, label: 'mission' })));
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
if (post.length) {
|
|
71
|
-
console.log('\n๐งญ Running post-mission cleanup:', post);
|
|
71
|
+
// console.log('\n๐งญ Running post-mission cleanup:', post);
|
|
72
72
|
goals.push(...post.map(fn => ({ goal: fn, label: 'post-mission' })));
|
|
73
73
|
}
|
|
74
74
|
|
|
@@ -77,11 +77,12 @@ export async function runMissions({ preMission, mission, postMission }) {
|
|
|
77
77
|
goals.map(g => `${g.label}: ${g.goal?.name ?? g.goal}`)
|
|
78
78
|
);
|
|
79
79
|
|
|
80
|
-
const success = await runAgent(goals);
|
|
80
|
+
const success = await runAgent(goals, missionName);
|
|
81
81
|
if (!success) {
|
|
82
82
|
console.log(`โ Aborting after failed goal.`);
|
|
83
83
|
return;
|
|
84
84
|
}
|
|
85
|
-
|
|
86
85
|
console.log('\nโ
Mission flow complete.');
|
|
86
|
+
return success;
|
|
87
|
+
|
|
87
88
|
}
|
package/tools/chromeBrowser.js
CHANGED
|
@@ -149,9 +149,9 @@ export class ChromeBrowser {
|
|
|
149
149
|
await this.page.waitForTimeout(delayMs);
|
|
150
150
|
|
|
151
151
|
const expandedDom = await this.page.content();
|
|
152
|
-
const filename = `missions/mission_reports/expand_menu_dump_${Date.now()}.html`;
|
|
153
|
-
fs.writeFileSync(filename, expandedDom);
|
|
154
|
-
console.log(`[expand_menu] โ Dumped DOM after expand click to ${filename}`);
|
|
152
|
+
// const filename = `missions/mission_reports/expand_menu_dump_${Date.now()}.html`;
|
|
153
|
+
// fs.writeFileSync(filename, expandedDom);
|
|
154
|
+
// console.log(`[expand_menu] โ Dumped DOM after expand click to ${filename}`);
|
|
155
155
|
await this.page.screenshot({ path: `missions/mission_reports/screenshot_expand_menu_${Date.now()}.png` });
|
|
156
156
|
|
|
157
157
|
return `expanded menu using ${selector}`;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
|
|
3
|
+
export function generateHtmlReport(report, outputPath) {
|
|
4
|
+
const { runId, startTime, endTime, summary, missions } = report;
|
|
5
|
+
|
|
6
|
+
const missionHtml = missions.map(m => {
|
|
7
|
+
const status = m.status?.toUpperCase?.() || 'UNKNOWN';
|
|
8
|
+
const missionName = m.missionName || 'Unnamed';
|
|
9
|
+
const file = m.file || 'N/A';
|
|
10
|
+
const steps = Array.isArray(m.steps) ? m.steps : [];
|
|
11
|
+
|
|
12
|
+
return `
|
|
13
|
+
<div class="mission ${status.toLowerCase()}">
|
|
14
|
+
<h3>${file} - ${missionName} - <span>${status}</span></h3>
|
|
15
|
+
<p><em>${m.startTime || 'N/A'} โ ${m.endTime || 'N/A'}</em></p>
|
|
16
|
+
<ul>
|
|
17
|
+
${steps.map(step => `
|
|
18
|
+
<li>
|
|
19
|
+
<strong>${step.tool}</strong> โ ${step.output}
|
|
20
|
+
</li>
|
|
21
|
+
`).join('')}
|
|
22
|
+
</ul>
|
|
23
|
+
</div>
|
|
24
|
+
`;
|
|
25
|
+
}).join('');
|
|
26
|
+
|
|
27
|
+
const html = `
|
|
28
|
+
<html>
|
|
29
|
+
<head>
|
|
30
|
+
<style>
|
|
31
|
+
body { font-family: sans-serif; padding: 20px; }
|
|
32
|
+
.passed { background-color: #e6ffed; }
|
|
33
|
+
.failed { background-color: #ffe6e6; }
|
|
34
|
+
.unknown { background-color: #f9f9f9; }
|
|
35
|
+
h3 span { font-weight: bold; }
|
|
36
|
+
ul { margin-top: 0; }
|
|
37
|
+
</style>
|
|
38
|
+
</head>
|
|
39
|
+
<body>
|
|
40
|
+
<h1>๐งโ๐ Testronaut Report</h1>
|
|
41
|
+
<p><strong>Run ID:</strong> ${runId}</p>
|
|
42
|
+
<p><strong>Start:</strong> ${startTime}</p>
|
|
43
|
+
<p><strong>End:</strong> ${endTime}</p>
|
|
44
|
+
<p><strong>Summary:</strong> ${summary.passed}/${summary.totalMissions} passed</p>
|
|
45
|
+
${missionHtml}
|
|
46
|
+
</body>
|
|
47
|
+
</html>
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
fs.writeFileSync(outputPath, html);
|
|
51
|
+
console.log(`๐ Report generated at: ${outputPath}`);
|
|
52
|
+
}
|