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 CHANGED
@@ -47,7 +47,8 @@ import { runMissions } from 'testronaut';
47
47
 
48
48
  export const loginMission = `
49
49
  Visit ${process.env.URL}.
50
- Fill in the username field with ${process.env.USERNAME} and password field with ${process.env.PASSWORD}.
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(); // or call a named export like `executeMissions()`
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`);
@@ -23,9 +23,9 @@ If you cannot reach this message, report FAILURE.
23
23
  \`;
24
24
 
25
25
  export async function executeMission() {
26
- await runMissions({
26
+ return await runMissions({
27
27
  mission: welcomeGoal
28
- });
28
+ }, 'Welcome Mission!');
29
29
  }
30
30
 
31
31
  `;
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
- return false;
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
- return true;
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
- result = `ERROR: ${err.message}`;
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
- const logFile = `missions/mission_reports/tool-result-${Date.now()}.log`;
175
- fs.writeFileSync(logFile, result);
176
- console.log(`[tool ] โ† ${truncated} (full output written to ${logFile})`);
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
- messages.push({
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testronaut",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "main": "./runner/testronaut.js",
5
5
  "bin": {
6
6
  "testronaut": "./bin/cli.js"
@@ -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
  }
@@ -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
+ }
@@ -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);
@@ -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) {