testronaut 1.0.6 โ 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 +79 -14
- package/package.json +1 -1
- package/runner/testronaut.js +7 -6
- package/tools/chromeBrowser.js +5 -5
- package/tools/generateHtmlReport.js +52 -0
- package/tools/tokenControl.js +2 -2
- package/tools/turnLoopUtils.js +3 -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',
|
|
@@ -92,6 +92,35 @@ export const turnLoop = async (browser, messages, maxTurns, currentTurn = 0, ret
|
|
|
92
92
|
|
|
93
93
|
let response;
|
|
94
94
|
try {
|
|
95
|
+
// console.log(
|
|
96
|
+
// '๐ Verifying message structure before OpenAI call:\n',
|
|
97
|
+
// messages.map(m => ({
|
|
98
|
+
// role: m.role,
|
|
99
|
+
// name: m.name,
|
|
100
|
+
// tool_call_id: m.tool_call_id,
|
|
101
|
+
// tool_calls: m.tool_calls?.map(t => t.id),
|
|
102
|
+
// content: m.content?.slice?.(0, 100), // truncate for readability
|
|
103
|
+
// }))
|
|
104
|
+
// );
|
|
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
|
+
|
|
95
124
|
response = await openai.chat.completions.create({
|
|
96
125
|
model: 'gpt-4o',
|
|
97
126
|
messages,
|
|
@@ -112,6 +141,7 @@ export const turnLoop = async (browser, messages, maxTurns, currentTurn = 0, ret
|
|
|
112
141
|
} else if (err.status === 400) {
|
|
113
142
|
console.error('โ Bad request:', err.message);
|
|
114
143
|
console.log("current tools", toolsSchema);
|
|
144
|
+
console.log("messages: ", messages)
|
|
115
145
|
return false;
|
|
116
146
|
} else {
|
|
117
147
|
throw err;
|
|
@@ -137,19 +167,31 @@ export const turnLoop = async (browser, messages, maxTurns, currentTurn = 0, ret
|
|
|
137
167
|
|
|
138
168
|
const msg = response.choices[0].message;
|
|
139
169
|
|
|
170
|
+
// if (!msg.tool_calls?.length) {
|
|
171
|
+
// console.error('โ Assistant message had empty or missing tool_calls array');
|
|
172
|
+
// return false;
|
|
173
|
+
// }
|
|
174
|
+
|
|
140
175
|
if (msg.tool_calls?.length) {
|
|
141
176
|
console.log('Processing tool calls...');
|
|
142
|
-
messages.push(msg);
|
|
177
|
+
// messages.push(msg);
|
|
178
|
+
const toolResponses = [];
|
|
143
179
|
let lastToolName = null;
|
|
180
|
+
|
|
144
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
|
+
// );
|
|
145
186
|
const fnName = call.function.name;
|
|
146
187
|
lastToolName = fnName;
|
|
147
188
|
const args = JSON.parse(call.function.arguments || '{}');
|
|
148
189
|
console.log(`[model] โ ${fnName}`, args);
|
|
149
|
-
console.log('Calling tool:', fnName);
|
|
190
|
+
// console.log('Calling tool:', fnName);
|
|
150
191
|
let result;
|
|
192
|
+
let errorMessage = null;
|
|
151
193
|
try {
|
|
152
|
-
console.log("agentMemory: ", agentMemory);
|
|
194
|
+
// console.log("agentMemory: ", agentMemory);
|
|
153
195
|
result = await CHROME_TOOL_MAP[fnName](browser, args, agentMemory);
|
|
154
196
|
|
|
155
197
|
// Smart DOM-ready wait after major actions like click
|
|
@@ -165,26 +207,39 @@ export const turnLoop = async (browser, messages, maxTurns, currentTurn = 0, ret
|
|
|
165
207
|
result = JSON.stringify(result ?? '');
|
|
166
208
|
}
|
|
167
209
|
} catch (err) {
|
|
168
|
-
|
|
210
|
+
errorMessage = `ERROR: ${err.message}`;
|
|
211
|
+
result = errorMessage;
|
|
169
212
|
}
|
|
170
213
|
|
|
214
|
+
// ๐ Diagnostic: log whether this tool call succeeded or failed
|
|
215
|
+
console.log(`[tool ] โ ${fnName} result:`, errorMessage ? 'โ Failed' : 'โ
Success');
|
|
216
|
+
|
|
171
217
|
const truncated = result.length > 100 ? result.slice(0, 100) + 'โฆ' : result;
|
|
172
218
|
|
|
173
|
-
if (result.length > 100) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
} 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 {
|
|
178
224
|
console.log(`[tool ] โ ${truncated}`);
|
|
179
|
-
}
|
|
225
|
+
// }
|
|
226
|
+
|
|
227
|
+
// messages.push({
|
|
228
|
+
// role: 'tool',
|
|
229
|
+
// tool_call_id: call.id,
|
|
230
|
+
// name: fnName,
|
|
231
|
+
// type: 'function',
|
|
232
|
+
// content: result,
|
|
233
|
+
// });
|
|
180
234
|
|
|
181
|
-
|
|
235
|
+
toolResponses.push({
|
|
182
236
|
role: 'tool',
|
|
183
237
|
tool_call_id: call.id,
|
|
184
238
|
name: fnName,
|
|
185
239
|
type: 'function',
|
|
186
240
|
content: result,
|
|
187
241
|
});
|
|
242
|
+
|
|
188
243
|
if (fnName === 'expand_menu') {
|
|
189
244
|
agentMemory.lastMenuExpanded = true;
|
|
190
245
|
}
|
|
@@ -197,13 +252,23 @@ export const turnLoop = async (browser, messages, maxTurns, currentTurn = 0, ret
|
|
|
197
252
|
focus: [], // or try focusing on ['main'] to keep it light
|
|
198
253
|
}, agentMemory);
|
|
199
254
|
await tokenEstimate('gpt-4o', domHtml);
|
|
200
|
-
fs.writeFileSync(`missions/mission_reports/debug-expanded-${Date.now()}.html`, domHtml);
|
|
255
|
+
// fs.writeFileSync(`missions/mission_reports/debug-expanded-${Date.now()}.html`, domHtml);
|
|
201
256
|
await pushDOMAssistant(browser, messages, agentMemory, {
|
|
202
257
|
skipIfLastTool: ['get_dom', 'check_text']
|
|
203
258
|
});
|
|
204
259
|
console.log(`[auto] โ DOM size after ${fnName}: ${domHtml.length} chars`);
|
|
205
260
|
}
|
|
206
|
-
|
|
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);
|
|
207
272
|
|
|
208
273
|
continue;
|
|
209
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
|
@@ -90,7 +90,7 @@ export class ChromeBrowser {
|
|
|
90
90
|
if (innerText.includes(text)) {
|
|
91
91
|
await el.scrollIntoViewIfNeeded().catch(() => {});
|
|
92
92
|
await el.click();
|
|
93
|
-
await this.page.screenshot({ path: `screenshot_after_click_${Date.now()}.png` });
|
|
93
|
+
await this.page.screenshot({ path: `missions/mission_reports/screenshot_after_click_${Date.now()}.png` });
|
|
94
94
|
await this.page.waitForTimeout(5000);
|
|
95
95
|
// await this.page.screenshot({ path: `screenshot_after_waiting_click_${Date.now()}.png` });
|
|
96
96
|
return `clicked text "${text}"`;
|
|
@@ -119,7 +119,7 @@ export class ChromeBrowser {
|
|
|
119
119
|
console.log(`[click_text] โ Clicking visible element with bounding box`, box);
|
|
120
120
|
await el.click();
|
|
121
121
|
await this.page.waitForTimeout(delayMs);
|
|
122
|
-
await this.page.screenshot({ path: `screenshot_after_click_${Date.now()}.png` });
|
|
122
|
+
await this.page.screenshot({ path: `missions/mission_reports/screenshot_after_click_${Date.now()}.png` });
|
|
123
123
|
return `clicked visible text "${text}"`;
|
|
124
124
|
}
|
|
125
125
|
}
|
|
@@ -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
|
+
}
|
package/tools/tokenControl.js
CHANGED
|
@@ -12,8 +12,8 @@ export const tokenEstimate = async (model, text) => {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export const tokenUseCoolOff = async (totalTokensUsed, turnTimestamps) => {
|
|
15
|
-
console.log("total tokens used: ", totalTokensUsed);
|
|
16
|
-
console.log("turnTimestamps: ", turnTimestamps);
|
|
15
|
+
// console.log("total tokens used: ", totalTokensUsed);
|
|
16
|
+
// console.log("turnTimestamps: ", turnTimestamps);
|
|
17
17
|
|
|
18
18
|
if (totalTokensUsed > TOKEN_LIMIT_PER_MIN) {
|
|
19
19
|
const msToWait = await getDynamicBackoffMs(turnTimestamps);
|
package/tools/turnLoopUtils.js
CHANGED
|
@@ -34,6 +34,9 @@ export function validateAndInsertMissingToolResponses(messages, options = {}) {
|
|
|
34
34
|
);
|
|
35
35
|
|
|
36
36
|
if (!hasResponse) {
|
|
37
|
+
console.warn(
|
|
38
|
+
`โ ๏ธ Missing response for tool_call_id: ${expectedId}, function: ${toolCall.function.name}, from assistant message index: ${i}`
|
|
39
|
+
);
|
|
37
40
|
missingResponses.push(expectedId);
|
|
38
41
|
|
|
39
42
|
if (insertPlaceholders) {
|