testronaut 1.0.5 → 1.0.7

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
@@ -20,9 +20,13 @@
20
20
  ```
21
21
  npm install -g testronaut
22
22
  ```
23
- Or use with npx:
23
+ Then initialize with npx, following the instructions:
24
24
  ```
25
- npx testronaut
25
+ npx testronaut --init
26
+ ```
27
+ run the welcome mission:
28
+ ```
29
+ npx testronaut welcome.mission.js
26
30
  ```
27
31
  📁 Directory Structure
28
32
  Your project should include a missions/ folder with mission files like:
@@ -98,7 +102,7 @@ testronaut
98
102
  Run a specific file:
99
103
 
100
104
  ```
101
- testronaut missions/login.mission.js
105
+ testronaut login.mission.js
102
106
  ```
103
107
 
104
108
  🧰 Available Helpers
package/bin/cli.js CHANGED
@@ -2,12 +2,43 @@
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
4
  import { fileURLToPath } from 'url';
5
+ import { initializeTestronautProject } from './init.js';
6
+ import { createWelcomeMission } from './createWelcomeMission.js';
5
7
 
6
8
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
9
 
8
10
  const args = process.argv.slice(2);
9
11
  const missionsDir = path.resolve(process.cwd(), 'missions');
10
12
 
13
+ const HELP_TEXT = `
14
+ 🧑‍🚀 testronaut - Autonomous Agent Mission Runner
15
+
16
+ Usage:
17
+ npx testronaut Run all missions in the ./missions directory
18
+ npx testronaut <file> Run a specific mission file (e.g., login.mission.js)
19
+
20
+ Options:
21
+ --init Scaffold project folders and a welcome mission
22
+ --help Show this help message
23
+
24
+ Examples:
25
+ npx testronaut
26
+ npx testronaut login.mission.js
27
+ npx testronaut --init
28
+ `;
29
+
30
+
31
+ if (args.includes('--init')) {
32
+ await initializeTestronautProject();
33
+ await createWelcomeMission();
34
+ process.exit(0);
35
+ }
36
+
37
+ if (args.includes('--help')) {
38
+ console.log(HELP_TEXT);
39
+ process.exit(0);
40
+ }
41
+
11
42
  if (!fs.existsSync(missionsDir)) {
12
43
  console.error('❌ No `missions` directory found.');
13
44
  process.exit(1);
@@ -0,0 +1,35 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ export const createWelcomeMission = async () => {
5
+ const missionsDir = path.join(process.cwd(), 'missions');
6
+ const welcomePath = path.join(missionsDir, 'welcome.mission.js');
7
+
8
+ if (fs.existsSync(welcomePath)) {
9
+ console.log('✅ welcome.mission.js already exists');
10
+ return;
11
+ }
12
+
13
+ const content = `import { runMissions } from 'testronaut';
14
+
15
+ const welcomeGoal = \`
16
+ Welcome to Testronaut!
17
+
18
+ This is your first mission.
19
+ All we ask is that you confirm this setup is working correctly.
20
+
21
+ To complete this mission, report SUCCESS immediately.
22
+ If you cannot reach this message, report FAILURE.
23
+ \`;
24
+
25
+ export async function executeMission() {
26
+ await runMissions({
27
+ mission: welcomeGoal
28
+ });
29
+ }
30
+
31
+ `;
32
+
33
+ fs.writeFileSync(welcomePath, content);
34
+ console.log('🚀 Created missions/welcome.mission.js');
35
+ }
package/bin/init.js ADDED
@@ -0,0 +1,77 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import prompts from 'prompts';
4
+ import { createWelcomeMission } from './createWelcomeMission.js';
5
+
6
+ export async function initializeTestronautProject() {
7
+ const root = process.cwd();
8
+ const missionsDir = path.join(root, 'missions');
9
+ const reportsDir = path.join(missionsDir, 'mission_reports');
10
+ const configPath = path.join(root, 'testronaut-config.json');
11
+ const envPath = path.join(root, '.env');
12
+
13
+ let config = {};
14
+ if (fs.existsSync(configPath)) {
15
+ config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
16
+ }
17
+
18
+ // If not initialized before, walk through setup
19
+ if (!config.initialized) {
20
+ // Prompt for LLM
21
+ const response = await prompts({
22
+ type: 'select',
23
+ name: 'llmProvider',
24
+ message: 'Which LLM provider would you like to use?',
25
+ choices: [
26
+ { title: 'OpenAI (GPT-4o)', value: 'openai' },
27
+ // Add others like Anthropic, Mistral, etc. later
28
+ ]
29
+ });
30
+
31
+ config.model = response.llmProvider;
32
+ config.initialized = true;
33
+ config.outputDir = 'missions/mission_reports';
34
+ config.projectName = path.basename(root);
35
+ config.maxTurns = 20;
36
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
37
+ console.log('📝 Created testronaut-config.json');
38
+
39
+ if (response.llmProvider === 'openai') {
40
+ // Display .env instructions
41
+ if (!fs.existsSync(envPath)) {
42
+ const exampleEnv = `# Add your OpenAI API key below\nOPENAI_API_KEY=sk-...\n`;
43
+ fs.writeFileSync(envPath, exampleEnv);
44
+ console.log('🔐 Created .env file');
45
+ }
46
+
47
+ console.log(`
48
+ 🔧 Setup Complete!
49
+
50
+ 👉 Please ensure your .env file contains your OpenAI API key like this:
51
+
52
+ OPENAI_API_KEY=sk-...
53
+
54
+ 📌 The key must have permissions to call the OpenAI Chat Completions API.
55
+
56
+ ✅ You’re now ready to write missions in \`missions/\` and run your first mission using:
57
+
58
+ testronaut welcome.mission.js
59
+ `);
60
+ }
61
+
62
+ createWelcomeMission();
63
+ } else {
64
+ console.log('🔁 Already initialized. Skipping setup.');
65
+ }
66
+
67
+ // Create directories if they don't exist
68
+ if (!fs.existsSync(missionsDir)) {
69
+ fs.mkdirSync(missionsDir);
70
+ console.log('📁 Created missions/');
71
+ }
72
+
73
+ if (!fs.existsSync(reportsDir)) {
74
+ fs.mkdirSync(reportsDir);
75
+ console.log('📁 Created missions/mission_reports/');
76
+ }
77
+ }
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(`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,17 @@ 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
+
95
106
  response = await openai.chat.completions.create({
96
107
  model: 'gpt-4o',
97
108
  messages,
@@ -148,6 +159,7 @@ export const turnLoop = async (browser, messages, maxTurns, currentTurn = 0, ret
148
159
  console.log(`[model] → ${fnName}`, args);
149
160
  console.log('Calling tool:', fnName);
150
161
  let result;
162
+ let errorMessage = null;
151
163
  try {
152
164
  console.log("agentMemory: ", agentMemory);
153
165
  result = await CHROME_TOOL_MAP[fnName](browser, args, agentMemory);
@@ -165,13 +177,17 @@ export const turnLoop = async (browser, messages, maxTurns, currentTurn = 0, ret
165
177
  result = JSON.stringify(result ?? '');
166
178
  }
167
179
  } catch (err) {
168
- result = `ERROR: ${err.message}`;
180
+ errorMessage = `ERROR: ${err.message}`;
181
+ result = errorMessage;
169
182
  }
170
183
 
184
+ // 🔍 Diagnostic: log whether this tool call succeeded or failed
185
+ console.log(`[tool ] ← ${fnName} result:`, errorMessage ? '❌ Failed' : '✅ Success');
186
+
171
187
  const truncated = result.length > 100 ? result.slice(0, 100) + '…' : result;
172
188
 
173
189
  if (result.length > 100) {
174
- const logFile = `tool-result-${Date.now()}.log`;
190
+ const logFile = `missions/mission_reports/tool-result-${Date.now()}.log`;
175
191
  fs.writeFileSync(logFile, result);
176
192
  console.log(`[tool ] ← ${truncated} (full output written to ${logFile})`);
177
193
  } else {
@@ -197,7 +213,7 @@ export const turnLoop = async (browser, messages, maxTurns, currentTurn = 0, ret
197
213
  focus: [], // or try focusing on ['main'] to keep it light
198
214
  }, agentMemory);
199
215
  await tokenEstimate('gpt-4o', domHtml);
200
- fs.writeFileSync(`debug-expanded-${Date.now()}.html`, domHtml);
216
+ fs.writeFileSync(`missions/mission_reports/debug-expanded-${Date.now()}.html`, domHtml);
201
217
  await pushDOMAssistant(browser, messages, agentMemory, {
202
218
  skipIfLastTool: ['get_dom', 'check_text']
203
219
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testronaut",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "main": "./runner/testronaut.js",
5
5
  "bin": {
6
6
  "testronaut": "./bin/cli.js"
@@ -23,6 +23,7 @@
23
23
  "cheerio": "^1.1.0",
24
24
  "dotenv": "^17.2.0",
25
25
  "openai": "^5.10.2",
26
- "playwright": "^1.54.1"
26
+ "playwright": "^1.54.1",
27
+ "prompts": "^2.4.2"
27
28
  }
28
29
  }
@@ -90,9 +90,9 @@ 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
- await this.page.screenshot({ path: `screenshot_after_waiting_click_${Date.now()}.png` });
95
+ // await this.page.screenshot({ path: `screenshot_after_waiting_click_${Date.now()}.png` });
96
96
  return `clicked text "${text}"`;
97
97
 
98
98
  }
@@ -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,10 +149,10 @@ export class ChromeBrowser {
149
149
  await this.page.waitForTimeout(delayMs);
150
150
 
151
151
  const expandedDom = await this.page.content();
152
- const filename = `expand_menu_dump_${Date.now()}.html`;
152
+ const filename = `missions/mission_reports/expand_menu_dump_${Date.now()}.html`;
153
153
  fs.writeFileSync(filename, expandedDom);
154
154
  console.log(`[expand_menu] → Dumped DOM after expand click to ${filename}`);
155
- await this.page.screenshot({ path: `screenshot_expand_menu_${Date.now()}.png` });
155
+ await this.page.screenshot({ path: `missions/mission_reports/screenshot_expand_menu_${Date.now()}.png` });
156
156
 
157
157
  return `expanded menu using ${selector}`;
158
158
  }
@@ -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) {