testronaut 1.0.7 โ 1.0.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/README.md +10 -6
- package/bin/cli.js +39 -1
- package/bin/createWelcomeMission.js +2 -2
- package/core/agent.js +21 -42
- package/core/turnLoop.js +48 -76
- package/package.json +9 -2
- package/runner/missionLaunch.js +1 -1
- package/runner/testronaut.js +3 -38
- package/tools/chromeBrowser.js +1 -6
- 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
|
@@ -2,41 +2,14 @@ import 'dotenv/config';
|
|
|
2
2
|
import { ChromeBrowser } from '../tools/chromeBrowser.js';
|
|
3
3
|
import { turnLoop } from './turnLoop.js';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
// export async function runAgent(goal, maxTurns = 10) {
|
|
7
|
-
// console.log(`\n๐ Running Agent with goal: ${goal}`);
|
|
8
|
-
// const browser = new ChromeBrowser();
|
|
9
|
-
// await browser.start();
|
|
10
|
-
|
|
11
|
-
// const messages = [
|
|
12
|
-
// {
|
|
13
|
-
// role: 'system',
|
|
14
|
-
// content: `
|
|
15
|
-
// You are an autonomous web agent. Use function calls to complete the user's goal.
|
|
16
|
-
// If you are unsure of the selectors for inputs or buttons, call 'get_dom' to retrieve page HTML,
|
|
17
|
-
// analyze it, then make your best guess based on labels, names, types, and placeholder values.
|
|
18
|
-
// After completing the goal, respond with a final plain-text message starting with SUCCESS or FAILURE.
|
|
19
|
-
// `.trim(),
|
|
20
|
-
// },
|
|
21
|
-
// { role: 'user', content: goal },
|
|
22
|
-
// ];
|
|
23
|
-
|
|
24
|
-
// try {
|
|
25
|
-
// const result = await turnLoop(browser, messages, maxTurns);
|
|
26
|
-
// !result && console.log('๐ Agent ran out of turns.')
|
|
27
|
-
// } finally {
|
|
28
|
-
// await browser.close();
|
|
29
|
-
// }
|
|
30
|
-
// }
|
|
31
|
-
|
|
32
|
-
export async function runAgent(goals, maxTurns = 20) {
|
|
5
|
+
export async function runAgent(goals, missionName, maxTurns = 20, ) {
|
|
33
6
|
const browser = new ChromeBrowser();
|
|
34
7
|
await browser.start();
|
|
8
|
+
const startTime = Date.now();
|
|
35
9
|
|
|
36
10
|
try {
|
|
37
11
|
for (const goal of goals) {
|
|
38
|
-
console.log(`\n๐ Running Agent with goal:\n${goal}\n`);
|
|
39
|
-
|
|
12
|
+
// console.log(`\n๐ Running Agent with goal:\n${goal}\n`);
|
|
40
13
|
const messages = [
|
|
41
14
|
{
|
|
42
15
|
role: 'system',
|
|
@@ -59,24 +32,30 @@ export async function runAgent(goals, maxTurns = 20) {
|
|
|
59
32
|
const success = await turnLoop(browser, messages, maxTurns);
|
|
60
33
|
if (!success) {
|
|
61
34
|
console.log('๐ Agent stopped due to failed goal.\n');
|
|
62
|
-
|
|
35
|
+
const endTime = Date.now();
|
|
36
|
+
return {
|
|
37
|
+
missionName: missionName,
|
|
38
|
+
status: 'failed',
|
|
39
|
+
steps: [ /* tool steps or key moments */ ],
|
|
40
|
+
startTime: startTime,
|
|
41
|
+
endTime: endTime,
|
|
42
|
+
}
|
|
43
|
+
;
|
|
63
44
|
}
|
|
64
45
|
}
|
|
65
46
|
|
|
66
47
|
console.log('โ
All goals completed successfully.\n');
|
|
67
|
-
|
|
48
|
+
const endTime = Date.now();
|
|
49
|
+
return {
|
|
50
|
+
missionName: missionName,
|
|
51
|
+
status: 'passed',
|
|
52
|
+
steps: [ /* tool steps or key moments */ ],
|
|
53
|
+
startTime: startTime,
|
|
54
|
+
endTime: endTime,
|
|
55
|
+
}
|
|
56
|
+
;
|
|
68
57
|
|
|
69
58
|
} finally {
|
|
70
59
|
await browser.close();
|
|
71
60
|
}
|
|
72
61
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
// (async () => {
|
|
76
|
-
// await runAgent(`
|
|
77
|
-
// Visit ${process.env.URL}.
|
|
78
|
-
// Log in using ${process.env.USERNAME} and password ${process.env.PASSWORD}.
|
|
79
|
-
// After clicking the login button, use 'check_text' to verify that "${process.env.AFTER_LOGIN_CHECK}" appears on the page.
|
|
80
|
-
// If found, report SUCCESS. Otherwise, report FAILURE.
|
|
81
|
-
// `);
|
|
82
|
-
// })();
|
package/core/turnLoop.js
CHANGED
|
@@ -18,6 +18,7 @@ const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
|
|
18
18
|
|
|
19
19
|
let totalTokensUsed = 0;
|
|
20
20
|
let turnTimestamps = [];
|
|
21
|
+
let shouldBackoff;
|
|
21
22
|
|
|
22
23
|
const pushDOMAssistant = async (browser, messages, agentMemory, { skipIfLastTool } = {}) => {
|
|
23
24
|
if (skipIfLastTool && skipIfLastTool.includes(messages.at(-1)?.name)) {
|
|
@@ -43,7 +44,7 @@ const pushDOMAssistant = async (browser, messages, agentMemory, { skipIfLastTool
|
|
|
43
44
|
|
|
44
45
|
const domHtml = await CHROME_TOOL_MAP.get_dom(browser, { limit: 100000, exclude: true }, agentMemory);
|
|
45
46
|
await tokenEstimate('gpt-4o', domHtml);
|
|
46
|
-
fs.writeFileSync(`missions/mission_reports/debug-expanded-${Date.now()}.html`, domHtml);
|
|
47
|
+
// fs.writeFileSync(`missions/mission_reports/debug-expanded-${Date.now()}.html`, domHtml);
|
|
47
48
|
|
|
48
49
|
messages.push({
|
|
49
50
|
role: 'tool',
|
|
@@ -61,48 +62,40 @@ export const turnLoop = async (browser, messages, maxTurns, currentTurn = 0, ret
|
|
|
61
62
|
|
|
62
63
|
for (let turn = currentTurn; turn < maxTurns; turn++) {
|
|
63
64
|
console.log(`\n๐ Turn ${turn + 1}/${maxTurns}`);
|
|
64
|
-
// console.log("current tools", toolsSchema);
|
|
65
|
-
|
|
66
|
-
// const now = Date.now();
|
|
67
|
-
// turnTimestamps = turnTimestamps.filter(ts => now - ts < 60000);
|
|
68
|
-
|
|
69
|
-
// Recalculate the rolling window before checking cooldown
|
|
70
|
-
const pruned = pruneOldTokenUsage(turnTimestamps);
|
|
71
|
-
turnTimestamps = pruned.turnTimestamps;
|
|
72
|
-
totalTokensUsed = pruned.totalTokensUsed;
|
|
73
|
-
|
|
74
|
-
// Cool off check
|
|
75
|
-
const cooldownResult = await tokenUseCoolOff(totalTokensUsed, turnTimestamps);
|
|
76
|
-
totalTokensUsed = cooldownResult.totalTokensUsed;
|
|
77
|
-
turnTimestamps = cooldownResult.turnTimestamps;
|
|
78
|
-
if (cooldownResult.shouldBackoff) {
|
|
79
|
-
return await turnLoop(browser, messages, maxTurns, turn);
|
|
80
|
-
}
|
|
81
|
-
// if (await tokenUseCoolOff(totalTokensUsed, turnTimestamps)) {return await turnLoop(browser, messages, maxTurns, turn)}
|
|
82
|
-
|
|
83
|
-
const isValid = validateAndInsertMissingToolResponses(messages, {
|
|
84
|
-
insertPlaceholders: true, // safe fallback behavior
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
if (!isValid) {
|
|
88
|
-
console.error('โ Tool call structure invalid and no placeholders inserted.');
|
|
89
|
-
return false;
|
|
90
|
-
}
|
|
91
|
-
// console.log(JSON.stringify(messages, null, 2));
|
|
92
|
-
|
|
93
65
|
let response;
|
|
94
66
|
try {
|
|
95
|
-
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
//
|
|
104
|
-
|
|
67
|
+
|
|
68
|
+
// Recalculate the rolling window before checking cooldown
|
|
69
|
+
({ turnTimestamps, totalTokensUsed } = pruneOldTokenUsage(turnTimestamps));
|
|
70
|
+
|
|
71
|
+
// Cool off check
|
|
72
|
+
({ totalTokensUsed, turnTimestamps, shouldBackoff } = await tokenUseCoolOff(totalTokensUsed, turnTimestamps));
|
|
73
|
+
if(shouldBackoff) return await turnLoop(browser, messages, maxTurns, turn);
|
|
74
|
+
|
|
75
|
+
//Tool Validator
|
|
76
|
+
if (!validateAndInsertMissingToolResponses(messages, { insertPlaceholders: true })) {
|
|
77
|
+
console.error('โ Tool call structure invalid and no placeholders inserted.');
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const unrespondedCalls = messages.filter(
|
|
82
|
+
(msg, i) =>
|
|
83
|
+
msg.role === 'assistant' &&
|
|
84
|
+
msg.tool_calls?.length &&
|
|
85
|
+
!msg.tool_calls.every(call =>
|
|
86
|
+
messages.slice(i + 1).some(
|
|
87
|
+
m => m.role === 'tool' && m.tool_call_id === call.id
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
);
|
|
105
91
|
|
|
92
|
+
if (unrespondedCalls.length) {
|
|
93
|
+
console.error('๐ Detected assistant tool calls without matching tool responses:');
|
|
94
|
+
console.dir(unrespondedCalls, { depth: 5 });
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
106
99
|
response = await openai.chat.completions.create({
|
|
107
100
|
model: 'gpt-4o',
|
|
108
101
|
messages,
|
|
@@ -122,7 +115,8 @@ export const turnLoop = async (browser, messages, maxTurns, currentTurn = 0, ret
|
|
|
122
115
|
return await turnLoop(browser, messages, maxTurns, turn, retryCount + 1);
|
|
123
116
|
} else if (err.status === 400) {
|
|
124
117
|
console.error('โ Bad request:', err.message);
|
|
125
|
-
console.log("current tools", toolsSchema);
|
|
118
|
+
// console.log("current tools", toolsSchema);
|
|
119
|
+
// console.log("messages: ", messages)
|
|
126
120
|
return false;
|
|
127
121
|
} else {
|
|
128
122
|
throw err;
|
|
@@ -130,19 +124,12 @@ export const turnLoop = async (browser, messages, maxTurns, currentTurn = 0, ret
|
|
|
130
124
|
}
|
|
131
125
|
console.log('Response received from OpenAI');
|
|
132
126
|
const usage = response.usage;
|
|
133
|
-
// if (usage) {
|
|
134
|
-
// console.log(`๐ Token Usage This Turn โ Prompt: ${usage.prompt_tokens}, Completion: ${usage.completion_tokens}, Total: ${usage.total_tokens}`);
|
|
135
|
-
// totalTokensUsed += usage?.total_tokens || 0;
|
|
136
|
-
// turnTimestamps.push(now);
|
|
137
|
-
// console.log(`๐ Running Total Tokens Used: ${totalTokensUsed}`);
|
|
138
|
-
// }
|
|
139
127
|
if (usage) {
|
|
140
128
|
const tokensUsed = usage.total_tokens || 0;
|
|
141
129
|
console.log(`๐ Token Usage This Turn โ Total: ${tokensUsed}`);
|
|
142
130
|
recordTokenUsage(turnTimestamps, tokensUsed);
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
totalTokensUsed = pruned.totalTokensUsed;
|
|
131
|
+
({ turnTimestamps, totalTokensUsed } = pruneOldTokenUsage(turnTimestamps));
|
|
132
|
+
|
|
146
133
|
console.log(`๐ Running Total Tokens Used (Rolling 60s): ${totalTokensUsed}`);
|
|
147
134
|
}
|
|
148
135
|
|
|
@@ -150,29 +137,21 @@ export const turnLoop = async (browser, messages, maxTurns, currentTurn = 0, ret
|
|
|
150
137
|
|
|
151
138
|
if (msg.tool_calls?.length) {
|
|
152
139
|
console.log('Processing tool calls...');
|
|
153
|
-
|
|
140
|
+
const toolResponses = [];
|
|
154
141
|
let lastToolName = null;
|
|
142
|
+
|
|
155
143
|
for (const call of msg.tool_calls) {
|
|
156
144
|
const fnName = call.function.name;
|
|
157
145
|
lastToolName = fnName;
|
|
158
146
|
const args = JSON.parse(call.function.arguments || '{}');
|
|
159
147
|
console.log(`[model] โ ${fnName}`, args);
|
|
160
|
-
console.log('Calling tool:', fnName);
|
|
148
|
+
// console.log('Calling tool:', fnName);
|
|
161
149
|
let result;
|
|
162
150
|
let errorMessage = null;
|
|
163
151
|
try {
|
|
164
|
-
console.log("agentMemory: ", agentMemory);
|
|
152
|
+
// console.log("agentMemory: ", agentMemory);
|
|
165
153
|
result = await CHROME_TOOL_MAP[fnName](browser, args, agentMemory);
|
|
166
154
|
|
|
167
|
-
// Smart DOM-ready wait after major actions like click
|
|
168
|
-
// if (['click', 'click_text', 'fill'].includes(fnName)) {
|
|
169
|
-
// try {
|
|
170
|
-
// await browser.page.waitForLoadState?.('domcontentloaded', { timeout: 5000 });
|
|
171
|
-
// await browser.page.waitForTimeout?.(2000); // allow animations/navigation
|
|
172
|
-
// } catch (err) {
|
|
173
|
-
// console.warn(`โ ๏ธ DOM readiness wait failed after ${fnName}: ${err.message}`);
|
|
174
|
-
// }
|
|
175
|
-
// }
|
|
176
155
|
if (typeof result !== 'string') {
|
|
177
156
|
result = JSON.stringify(result ?? '');
|
|
178
157
|
}
|
|
@@ -181,26 +160,18 @@ export const turnLoop = async (browser, messages, maxTurns, currentTurn = 0, ret
|
|
|
181
160
|
result = errorMessage;
|
|
182
161
|
}
|
|
183
162
|
|
|
184
|
-
// ๐ Diagnostic: log whether this tool call succeeded or failed
|
|
185
163
|
console.log(`[tool ] โ ${fnName} result:`, errorMessage ? 'โ Failed' : 'โ
Success');
|
|
186
|
-
|
|
187
164
|
const truncated = result.length > 100 ? result.slice(0, 100) + 'โฆ' : result;
|
|
165
|
+
console.log(`[tool ] โ ${truncated}`);
|
|
188
166
|
|
|
189
|
-
|
|
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 {
|
|
194
|
-
console.log(`[tool ] โ ${truncated}`);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
messages.push({
|
|
167
|
+
toolResponses.push({
|
|
198
168
|
role: 'tool',
|
|
199
169
|
tool_call_id: call.id,
|
|
200
170
|
name: fnName,
|
|
201
171
|
type: 'function',
|
|
202
172
|
content: result,
|
|
203
173
|
});
|
|
174
|
+
|
|
204
175
|
if (fnName === 'expand_menu') {
|
|
205
176
|
agentMemory.lastMenuExpanded = true;
|
|
206
177
|
}
|
|
@@ -210,17 +181,18 @@ export const turnLoop = async (browser, messages, maxTurns, currentTurn = 0, ret
|
|
|
210
181
|
const domHtml = await CHROME_TOOL_MAP.get_dom(browser, {
|
|
211
182
|
limit: 100000,
|
|
212
183
|
exclude: true,
|
|
213
|
-
focus: [],
|
|
184
|
+
focus: [],
|
|
214
185
|
}, agentMemory);
|
|
215
186
|
await tokenEstimate('gpt-4o', domHtml);
|
|
216
|
-
fs.writeFileSync(`missions/mission_reports/debug-expanded-${Date.now()}.html`, domHtml);
|
|
187
|
+
// fs.writeFileSync(`missions/mission_reports/debug-expanded-${Date.now()}.html`, domHtml);
|
|
217
188
|
await pushDOMAssistant(browser, messages, agentMemory, {
|
|
218
189
|
skipIfLastTool: ['get_dom', 'check_text']
|
|
219
190
|
});
|
|
220
191
|
console.log(`[auto] โ DOM size after ${fnName}: ${domHtml.length} chars`);
|
|
221
192
|
}
|
|
193
|
+
|
|
222
194
|
}
|
|
223
|
-
|
|
195
|
+
messages.push(msg, ...toolResponses);
|
|
224
196
|
continue;
|
|
225
197
|
}
|
|
226
198
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "testronaut",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"main": "./runner/testronaut.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"testronaut": "./bin/cli.js"
|
|
@@ -25,5 +25,12 @@
|
|
|
25
25
|
"openai": "^5.10.2",
|
|
26
26
|
"playwright": "^1.54.1",
|
|
27
27
|
"prompts": "^2.4.2"
|
|
28
|
-
}
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"cli",
|
|
31
|
+
"testing",
|
|
32
|
+
"automation",
|
|
33
|
+
"node",
|
|
34
|
+
"qa"
|
|
35
|
+
]
|
|
29
36
|
}
|
package/runner/missionLaunch.js
CHANGED
package/runner/testronaut.js
CHANGED
|
@@ -16,39 +16,7 @@ export async function launch(preMissionSetup) {
|
|
|
16
16
|
await runSuite(objectives, preMissionSetup);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
// export async function runMissions({ preMission, mission, postMission }) {
|
|
21
|
-
// const goals = [];
|
|
22
|
-
|
|
23
|
-
// if (preMission) {
|
|
24
|
-
// console.log('\n๐งญ Running pre-mission setup:', preMission);
|
|
25
|
-
// goals.push({ goal: preMission, label: 'pre-mission' });
|
|
26
|
-
// }
|
|
27
|
-
|
|
28
|
-
// if (mission) {
|
|
29
|
-
// console.log('\n๐งญ Running main mission:', mission);
|
|
30
|
-
// goals.push({ goal: mission, label: 'mission' });
|
|
31
|
-
// }
|
|
32
|
-
|
|
33
|
-
// if (postMission) {
|
|
34
|
-
// console.log('\n๐งญ Running post-mission cleanup:', postMission);
|
|
35
|
-
// goals.push({ goal: postMission, label: 'post-mission' });
|
|
36
|
-
// }
|
|
37
|
-
|
|
38
|
-
// console.log('\n๐งญ Running mission flow:\n', goals);
|
|
39
|
-
|
|
40
|
-
// // for (const { goal, label } of goals) {
|
|
41
|
-
// // console.log(`\n๐งญ Running ${label}:\n`);
|
|
42
|
-
// const success = await runAgent(goals);
|
|
43
|
-
// if (!success) {
|
|
44
|
-
// console.log(`โ Aborting after failed goal.`);
|
|
45
|
-
// return;
|
|
46
|
-
// }
|
|
47
|
-
// // }
|
|
48
|
-
// console.log('\nโ
Mission flow complete.');
|
|
49
|
-
// }
|
|
50
|
-
|
|
51
|
-
export async function runMissions({ preMission, mission, postMission }) {
|
|
19
|
+
export async function runMissions({ preMission, mission, postMission }, missionName) {
|
|
52
20
|
const normalizeToArray = (x) => (Array.isArray(x) ? x : x ? [x] : []);
|
|
53
21
|
|
|
54
22
|
const pre = normalizeToArray(preMission);
|
|
@@ -58,17 +26,14 @@ export async function runMissions({ preMission, mission, postMission }) {
|
|
|
58
26
|
const goals = [];
|
|
59
27
|
|
|
60
28
|
if (pre.length) {
|
|
61
|
-
console.log('\n๐งญ Running pre-mission setup:', pre);
|
|
62
29
|
goals.push(...pre.map(fn => ({ goal: fn, label: 'pre-mission' })));
|
|
63
30
|
}
|
|
64
31
|
|
|
65
32
|
if (main.length) {
|
|
66
|
-
console.log('\n๐งญ Running main mission(s):', main);
|
|
67
33
|
goals.push(...main.map(fn => ({ goal: fn, label: 'mission' })));
|
|
68
34
|
}
|
|
69
35
|
|
|
70
36
|
if (post.length) {
|
|
71
|
-
console.log('\n๐งญ Running post-mission cleanup:', post);
|
|
72
37
|
goals.push(...post.map(fn => ({ goal: fn, label: 'post-mission' })));
|
|
73
38
|
}
|
|
74
39
|
|
|
@@ -77,11 +42,11 @@ export async function runMissions({ preMission, mission, postMission }) {
|
|
|
77
42
|
goals.map(g => `${g.label}: ${g.goal?.name ?? g.goal}`)
|
|
78
43
|
);
|
|
79
44
|
|
|
80
|
-
const success = await runAgent(goals);
|
|
45
|
+
const success = await runAgent(goals, missionName);
|
|
81
46
|
if (!success) {
|
|
82
47
|
console.log(`โ Aborting after failed goal.`);
|
|
83
48
|
return;
|
|
84
49
|
}
|
|
85
|
-
|
|
86
50
|
console.log('\nโ
Mission flow complete.');
|
|
51
|
+
return success;
|
|
87
52
|
}
|
package/tools/chromeBrowser.js
CHANGED
|
@@ -92,7 +92,7 @@ export class ChromeBrowser {
|
|
|
92
92
|
await el.click();
|
|
93
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}"`;
|
|
97
97
|
|
|
98
98
|
}
|
|
@@ -147,11 +147,6 @@ export class ChromeBrowser {
|
|
|
147
147
|
await this.page.waitForSelector(selector, { timeout: 30_000 });
|
|
148
148
|
await this.page.click(selector);
|
|
149
149
|
await this.page.waitForTimeout(delayMs);
|
|
150
|
-
|
|
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}`);
|
|
155
150
|
await this.page.screenshot({ path: `missions/mission_reports/screenshot_expand_menu_${Date.now()}.png` });
|
|
156
151
|
|
|
157
152
|
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
|
+
}
|