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 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',
@@ -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
- const logFile = `missions/mission_reports/tool-result-${Date.now()}.log`;
191
- fs.writeFileSync(logFile, result);
192
- console.log(`[tool ] โ† ${truncated} (full output written to ${logFile})`);
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
- messages.push({
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testronaut",
3
- "version": "1.0.7",
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
  }
@@ -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
+ }