testronaut 1.0.0

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/old/agent.js ADDED
@@ -0,0 +1,230 @@
1
+ // agent.js — Autonomous web agent with auto DOM refresh between steps
2
+
3
+ import 'dotenv/config';
4
+ import OpenAI from 'openai';
5
+ import { chromium } from 'playwright';
6
+
7
+ const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
8
+
9
+ class Browser {
10
+ constructor() {
11
+ this.browser = null;
12
+ this.page = null;
13
+ }
14
+
15
+ async start() {
16
+ this.browser = await chromium.launch({ headless: true });
17
+ const context = await this.browser.newContext();
18
+ this.page = await context.newPage();
19
+ }
20
+
21
+ async navigate({ url }) {
22
+ await this.page.goto(url, { timeout: 30_000 });
23
+ return `navigated to ${this.page.url()}`;
24
+ }
25
+
26
+ async fill({ selector, text }) {
27
+ await this.page.waitForSelector(selector, { timeout: 30_000 });
28
+ await this.page.fill(selector, text);
29
+ return `filled ${selector}`;
30
+ }
31
+
32
+ async click({ selector }) {
33
+ await this.page.waitForSelector(selector, { timeout: 30_000 });
34
+ await this.page.click(selector);
35
+ return `clicked ${selector}`;
36
+ }
37
+
38
+ async get_dom({ limit = 5000 }) {
39
+ const html = await this.page.content();
40
+ return html.slice(0, limit);
41
+ }
42
+
43
+ async close() {
44
+ await this.browser?.close();
45
+ }
46
+ }
47
+
48
+ const toolsSchema = [
49
+ {
50
+ type: 'function',
51
+ function: {
52
+ name: 'navigate',
53
+ description: 'Navigate to a URL',
54
+ parameters: {
55
+ type: 'object',
56
+ properties: {
57
+ url: { type: 'string' },
58
+ },
59
+ required: ['url'],
60
+ },
61
+ },
62
+ },
63
+ {
64
+ type: 'function',
65
+ function: {
66
+ name: 'fill',
67
+ description: 'Fill in a field by CSS selector',
68
+ parameters: {
69
+ type: 'object',
70
+ properties: {
71
+ selector: { type: 'string' },
72
+ text: { type: 'string' },
73
+ },
74
+ required: ['selector', 'text'],
75
+ },
76
+ },
77
+ },
78
+ {
79
+ type: 'function',
80
+ function: {
81
+ name: 'click',
82
+ description: 'Click a button or link by CSS selector',
83
+ parameters: {
84
+ type: 'object',
85
+ properties: {
86
+ selector: { type: 'string' },
87
+ },
88
+ required: ['selector'],
89
+ },
90
+ },
91
+ },
92
+ {
93
+ type: 'function',
94
+ function: {
95
+ name: 'get_dom',
96
+ description: 'Return trimmed HTML content for GPT to inspect and find selectors',
97
+ parameters: {
98
+ type: 'object',
99
+ properties: {
100
+ limit: { type: 'number', default: 5000 },
101
+ },
102
+ },
103
+ },
104
+ },
105
+ ];
106
+
107
+ const TOOL_MAP = {
108
+ navigate: (b, args) => b.navigate(args),
109
+ fill: (b, args) => b.fill(args),
110
+ click: (b, args) => b.click(args),
111
+ get_dom: (b, args) => b.get_dom(args),
112
+ };
113
+
114
+ async function runAgent(goal, maxTurns = 10) {
115
+ const browser = new Browser();
116
+ await browser.start();
117
+
118
+ const messages = [
119
+ {
120
+ role: 'system',
121
+ content: `
122
+ You are an autonomous web agent. Use function calls to complete the user's goal.
123
+ If you are unsure of the selectors for inputs or buttons, call 'get_dom' to retrieve page HTML,
124
+ analyze it, then make your best guess based on labels, names, types, and placeholder values.
125
+ After completing the goal, respond with a final plain-text message starting with SUCCESS or FAILURE.
126
+ `.trim(),
127
+ },
128
+ { role: 'user', content: goal },
129
+ ];
130
+
131
+ try {
132
+ for (let turn = 0; turn < maxTurns; turn++) {
133
+ const response = await openai.chat.completions.create({
134
+ model: 'gpt-4o',
135
+ messages,
136
+ tools: toolsSchema,
137
+ });
138
+
139
+ const msg = response.choices[0].message;
140
+
141
+ if (msg.tool_calls?.length) {
142
+ messages.push(msg); // assistant's intent
143
+
144
+ for (const call of msg.tool_calls) {
145
+ const fnName = call.function.name;
146
+ const args = JSON.parse(call.function.arguments || '{}');
147
+ console.log(`[model] → ${fnName}`, args);
148
+
149
+ try {
150
+ let result;
151
+ try {
152
+ result = await TOOL_MAP[fnName](browser, args);
153
+ if (typeof result !== 'string') {
154
+ result = JSON.stringify(result ?? ''); // make sure it's always a string
155
+ }
156
+ } catch (err) {
157
+ result = `ERROR: ${err.message}`;
158
+ }
159
+ console.log(`[tool ] ← ${result}`);
160
+
161
+ messages.push({
162
+ role: 'tool',
163
+ tool_call_id: call.id,
164
+ name: fnName,
165
+ content: result,
166
+ });
167
+ } catch (err) {
168
+ console.error(`[error] ${err.message}`);
169
+ messages.push({
170
+ role: 'tool',
171
+ tool_call_id: call.id,
172
+ name: fnName,
173
+ content: `ERROR: ${err.message}`,
174
+ });
175
+ }
176
+ }
177
+
178
+ continue;
179
+ }
180
+
181
+ // Check if GPT declared final answer
182
+ const final = msg.content?.trim().toLowerCase();
183
+ if (final?.startsWith('success') || final?.startsWith('failure')) {
184
+ console.log('\n┏━ FINAL AGENT RESPONSE ━━━━━━━━━━━━━━━━━━━');
185
+ console.log(msg.content);
186
+ console.log('┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
187
+ return;
188
+ }
189
+
190
+ // Otherwise, auto-inject get_dom so GPT can see the page
191
+ const domCallId = `get_dom_${Date.now()}`;
192
+ const domAssistantMsg = {
193
+ role: 'assistant',
194
+ tool_calls: [
195
+ {
196
+ id: domCallId,
197
+ function: {
198
+ name: 'get_dom',
199
+ arguments: JSON.stringify({ limit: 5000 }),
200
+ },
201
+ },
202
+ ],
203
+ };
204
+ messages.push(domAssistantMsg);
205
+
206
+ const domHtml = await TOOL_MAP.get_dom(browser, { limit: 5000 });
207
+ messages.push({
208
+ role: 'tool',
209
+ tool_call_id: domCallId,
210
+ name: 'get_dom',
211
+ content: domHtml,
212
+ });
213
+
214
+ console.log(`[auto] → Injected DOM for next reasoning step`);
215
+ }
216
+
217
+ console.log('🛑 Agent ran out of turns.');
218
+ } finally {
219
+ await browser.close();
220
+ }
221
+ }
222
+
223
+ (async () => {
224
+ await runAgent(`
225
+ Visit https://staging.collegiumbuilt.com/login.
226
+ Log in using owner@collegiumbuilt.com and password C0ll3g1um_Bu1lt!.
227
+ After clicking the login button, if you see the word "Dashboard" on the page, report SUCCESS.
228
+ Otherwise, report FAILURE.
229
+ `);
230
+ })();
package/old/agent2.js ADDED
@@ -0,0 +1,263 @@
1
+ // agent.js — Autonomous web agent with exclusion-based DOM trimming
2
+
3
+ import 'dotenv/config';
4
+ import OpenAI from 'openai';
5
+ import { chromium } from 'playwright';
6
+ import * as cheerio from 'cheerio';
7
+
8
+ const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
9
+
10
+ class Browser {
11
+ constructor() {
12
+ this.browser = null;
13
+ this.page = null;
14
+ }
15
+
16
+ async start() {
17
+ this.browser = await chromium.launch({ headless: true });
18
+ const context = await this.browser.newContext();
19
+ this.page = await context.newPage();
20
+ }
21
+
22
+ async navigate({ url }) {
23
+ await this.page.goto(url, { timeout: 30_000 });
24
+ return `navigated to ${this.page.url()}`;
25
+ }
26
+
27
+ async fill({ selector, text }) {
28
+ await this.page.waitForSelector(selector, { timeout: 30_000 });
29
+ await this.page.fill(selector, text);
30
+ return `filled ${selector}`;
31
+ }
32
+
33
+ async click({ selector, waitFor = 'load' }) {
34
+ await this.page.waitForSelector(selector, { timeout: 30_000 });
35
+
36
+ // Wait for possible page navigation or DOM change after click
37
+ const [response] = await Promise.all([
38
+ this.page.waitForLoadState(waitFor, { timeout: 10_000 }).catch(() => null), // wait for network/DOM
39
+ this.page.click(selector),
40
+ ]);
41
+
42
+ return `clicked ${selector}`;
43
+ }
44
+
45
+ async get_dom({ limit = 5000, exclude = true }) {
46
+ const html = await this.page.content();
47
+
48
+ if (exclude) {
49
+ const $ = cheerio.load(html);
50
+ $('script, style, meta, link, noscript, iframe, canvas, svg').remove();
51
+
52
+ $('*').each((_, el) => {
53
+ const $el = $(el);
54
+ if (!$el.text().trim() && $el.children().length === 0) {
55
+ $el.remove();
56
+ }
57
+ });
58
+
59
+ $('*')
60
+ .contents()
61
+ .each(function () {
62
+ if (this.type === 'comment') {
63
+ $(this).remove();
64
+ }
65
+ });
66
+
67
+ return $.html().slice(0, limit);
68
+ }
69
+
70
+ return html.slice(0, limit);
71
+ }
72
+
73
+ async close() {
74
+ await this.browser?.close();
75
+ }
76
+ }
77
+
78
+ const toolsSchema = [
79
+ {
80
+ type: 'function',
81
+ function: {
82
+ name: 'navigate',
83
+ description: 'Navigate to a URL',
84
+ parameters: {
85
+ type: 'object',
86
+ properties: {
87
+ url: { type: 'string' },
88
+ },
89
+ required: ['url'],
90
+ },
91
+ },
92
+ },
93
+ {
94
+ type: 'function',
95
+ function: {
96
+ name: 'fill',
97
+ description: 'Fill in a field by CSS selector',
98
+ parameters: {
99
+ type: 'object',
100
+ properties: {
101
+ selector: { type: 'string' },
102
+ text: { type: 'string' },
103
+ },
104
+ required: ['selector', 'text'],
105
+ },
106
+ },
107
+ },
108
+ {
109
+ type: 'function',
110
+ function: {
111
+ name: 'click',
112
+ description: 'Click a button or link by CSS selector',
113
+ parameters: {
114
+ type: 'object',
115
+ properties: {
116
+ selector: { type: 'string' },
117
+ waitFor: {
118
+ type: 'string',
119
+ enum: ['load', 'domcontentloaded', 'networkidle'],
120
+ default: 'load',
121
+ description: 'Wait condition after click',
122
+ },
123
+ },
124
+ required: ['selector'],
125
+ },
126
+ },
127
+ },
128
+ {
129
+ type: 'function',
130
+ function: {
131
+ name: 'get_dom',
132
+ description: 'Return trimmed HTML from the current page',
133
+ parameters: {
134
+ type: 'object',
135
+ properties: {
136
+ limit: {
137
+ type: 'number',
138
+ default: 5000,
139
+ description: 'Max characters to return',
140
+ },
141
+ exclude: {
142
+ type: 'boolean',
143
+ default: true,
144
+ description: 'Whether to exclude noisy tags like script/style/etc.',
145
+ },
146
+ },
147
+ },
148
+ },
149
+ },
150
+ ];
151
+
152
+ const TOOL_MAP = {
153
+ navigate: (b, args) => b.navigate(args),
154
+ fill: (b, args) => b.fill(args),
155
+ click: (b, args) => b.click(args),
156
+ get_dom: (b, args) => b.get_dom(args),
157
+ };
158
+
159
+ async function runAgent(goal, maxTurns = 10) {
160
+ const browser = new Browser();
161
+ await browser.start();
162
+
163
+ const messages = [
164
+ {
165
+ role: 'system',
166
+ content: `
167
+ You are an autonomous web agent. Use function calls to complete the user's goal.
168
+ If you are unsure of the selectors for inputs or buttons, call 'get_dom' to retrieve page HTML,
169
+ analyze it, then make your best guess based on labels, names, types, and placeholder values.
170
+ After completing the goal, respond with a final plain-text message starting with SUCCESS or FAILURE.
171
+ `.trim(),
172
+ },
173
+ { role: 'user', content: goal },
174
+ ];
175
+
176
+ try {
177
+ for (let turn = 0; turn < maxTurns; turn++) {
178
+ const response = await openai.chat.completions.create({
179
+ model: 'gpt-4o',
180
+ messages,
181
+ tools: toolsSchema,
182
+ });
183
+
184
+ const msg = response.choices[0].message;
185
+
186
+ if (msg.tool_calls?.length) {
187
+ messages.push(msg);
188
+
189
+ for (const call of msg.tool_calls) {
190
+ const fnName = call.function.name;
191
+ const args = JSON.parse(call.function.arguments || '{}');
192
+ console.log(`[model] → ${fnName}`, args);
193
+
194
+ let result;
195
+ try {
196
+ result = await TOOL_MAP[fnName](browser, args);
197
+ if (typeof result !== 'string') {
198
+ result = JSON.stringify(result ?? '');
199
+ }
200
+ } catch (err) {
201
+ result = `ERROR: ${err.message}`;
202
+ }
203
+
204
+ console.log(`[tool ] ← ${result}`);
205
+
206
+ messages.push({
207
+ role: 'tool',
208
+ tool_call_id: call.id,
209
+ name: fnName,
210
+ content: result,
211
+ });
212
+ }
213
+
214
+ continue;
215
+ }
216
+
217
+ const final = msg.content?.trim().toLowerCase();
218
+ if (final?.startsWith('success') || final?.startsWith('failure')) {
219
+ console.log('\n┏━ FINAL AGENT RESPONSE ━━━━━━━━━━━━━━━━━━━');
220
+ console.log(msg.content);
221
+ console.log('┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
222
+ return;
223
+ }
224
+
225
+ const domCallId = `get_dom_${Date.now()}`;
226
+ messages.push({
227
+ role: 'assistant',
228
+ tool_calls: [
229
+ {
230
+ id: domCallId,
231
+ function: {
232
+ name: 'get_dom',
233
+ arguments: JSON.stringify({ limit: 5000, exclude: true }),
234
+ },
235
+ },
236
+ ],
237
+ });
238
+
239
+ const domHtml = await TOOL_MAP.get_dom(browser, { limit: 5000, exclude: true });
240
+ messages.push({
241
+ role: 'tool',
242
+ tool_call_id: domCallId,
243
+ name: 'get_dom',
244
+ content: typeof domHtml === 'string' ? domHtml : JSON.stringify(domHtml),
245
+ });
246
+
247
+ console.log(`[auto] → Injected DOM for next reasoning step`);
248
+ }
249
+
250
+ console.log('🛑 Agent ran out of turns.');
251
+ } finally {
252
+ await browser.close();
253
+ }
254
+ }
255
+
256
+ (async () => {
257
+ await runAgent(`
258
+ Visit ${process.env.URL}.
259
+ Log in using ${process.env.USERNAME} and password C0ll3g1um_Bu1lt!.
260
+ After clicking the login button, if you see the word "${process.env.AFTER_LOGIN_CHECK}" on the page, report SUCCESS.
261
+ Otherwise, report FAILURE.
262
+ `);
263
+ })();
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "testronaut",
3
+ "version": "1.0.0",
4
+ "main": "./runner/testronaut.js",
5
+ "bin": {
6
+ "testronaut": "./bin/cli.js"
7
+ },
8
+ "exports": {
9
+ ".": "./runner/testronaut.js",
10
+ "./runner": "./runner/testronaut.js",
11
+ "./runner/testronaut": "./runner/testronaut.js"
12
+ },
13
+ "type": "module",
14
+ "scripts": {
15
+ "test": "echo \"Error: no test specified\" && exit 1",
16
+ "start": "node ./bin/cli.js"
17
+ },
18
+ "author": "",
19
+ "license": "ISC",
20
+ "description": "",
21
+ "dependencies": {
22
+ "@dqbd/tiktoken": "^1.0.21",
23
+ "cheerio": "^1.1.0",
24
+ "dotenv": "^17.2.0",
25
+ "openai": "^5.10.2",
26
+ "playwright": "^1.54.1"
27
+ }
28
+ }
@@ -0,0 +1,9 @@
1
+ import { mission, objective, launch } from './testronaut.js';
2
+ import { loginWorkflow } from '../workflows/loginWorkflow.js';
3
+ import { dashboardWorkflow } from '../workflows/dashboardWorkflow.js';
4
+
5
+ mission('Collegium Dashboard Login Test', () => {
6
+ objective('should land on dashboard after login', dashboardWorkflow);
7
+ });
8
+
9
+ await launch(loginWorkflow); // Setup: login
@@ -0,0 +1,8 @@
1
+ export async function runSuite(objectives, sharedSetup) {
2
+ const browser = await sharedSetup(); // e.g., login
3
+ for (const { name, workflow } of objectives) {
4
+ console.log(`\n🎯 Objective: ${name}`);
5
+ await workflow(browser);
6
+ }
7
+ await browser.close();
8
+ }
@@ -0,0 +1,87 @@
1
+ import { runAgent } from '../core/agent.js';
2
+ import { runSuite } from './suiteRunner.js';
3
+
4
+ const objectives = [];
5
+
6
+ export function mission(title, fn) {
7
+ console.log(`\n🚀 Mission: ${title}`);
8
+ fn();
9
+ }
10
+
11
+ export function objective(desc, workflow) {
12
+ objectives.push({ name: desc, workflow });
13
+ }
14
+
15
+ export async function launch(preMissionSetup) {
16
+ await runSuite(objectives, preMissionSetup);
17
+ }
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 }) {
52
+ const normalizeToArray = (x) => (Array.isArray(x) ? x : x ? [x] : []);
53
+
54
+ const pre = normalizeToArray(preMission);
55
+ const main = normalizeToArray(mission);
56
+ const post = normalizeToArray(postMission);
57
+
58
+ const goals = [];
59
+
60
+ if (pre.length) {
61
+ console.log('\n🧭 Running pre-mission setup:', pre);
62
+ goals.push(...pre.map(fn => ({ goal: fn, label: 'pre-mission' })));
63
+ }
64
+
65
+ if (main.length) {
66
+ console.log('\n🧭 Running main mission(s):', main);
67
+ goals.push(...main.map(fn => ({ goal: fn, label: 'mission' })));
68
+ }
69
+
70
+ if (post.length) {
71
+ console.log('\n🧭 Running post-mission cleanup:', post);
72
+ goals.push(...post.map(fn => ({ goal: fn, label: 'post-mission' })));
73
+ }
74
+
75
+ console.log(
76
+ '\n🧭 Running mission flow:\n',
77
+ goals.map(g => `${g.label}: ${g.goal?.name ?? g.goal}`)
78
+ );
79
+
80
+ const success = await runAgent(goals);
81
+ if (!success) {
82
+ console.log(`❌ Aborting after failed goal.`);
83
+ return;
84
+ }
85
+
86
+ console.log('\n✅ Mission flow complete.');
87
+ }