testronaut 1.2.4 → 1.3.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/README.md +1 -1
- package/bin/cli.js +14 -8
- package/bin/init.js +2 -1
- package/bin/initHelpers.js +1 -0
- package/core/agent.js +75 -13
- package/core/turnLoop.js +69 -19
- package/llm/gemini/geminiProvider.js +14 -1
- package/package.json +1 -1
- package/tests/llmTests/geminiProvider.test.js +29 -0
- package/tests/toolsTests/contextControl.test.js +595 -0
- package/tests/toolsTests/domControl.test.js +329 -0
- package/tools/chromeBrowser.js +4 -0
- package/tools/contextControl.js +307 -0
- package/tools/domControl.js +129 -3
- package/tools/tokenControl.js +14 -2
- package/tools/toolSchema.js +101 -0
- package/tools/turnLoopUtils.js +0 -1
package/tools/domControl.js
CHANGED
|
@@ -1,3 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* domControl.js
|
|
3
|
+
* --------------
|
|
4
|
+
* Purpose:
|
|
5
|
+
* Centralized DOM-normalization helpers for Testronaut.
|
|
6
|
+
* These functions take a Cheerio instance and aggressively strip,
|
|
7
|
+
* normalize, and compress HTML so it’s cheaper and clearer for LLMs.
|
|
8
|
+
*
|
|
9
|
+
* Responsibilities:
|
|
10
|
+
* - Remove non-content tags (scripts, styles, iframes, etc.) and noisy attributes.
|
|
11
|
+
* - Collapse overly long lists/sections into truncated markers.
|
|
12
|
+
* - Reduce repeated div blocks to a representative sample.
|
|
13
|
+
* - Strip obfuscated/utility CSS classes that don’t help reasoning.
|
|
14
|
+
* - Minify whitespace in text nodes without breaking semantics.
|
|
15
|
+
*
|
|
16
|
+
* Related tests:
|
|
17
|
+
* Located in `tests/toolsTests/domControl.test.js`
|
|
18
|
+
*
|
|
19
|
+
* Used by:
|
|
20
|
+
* - tools/chromeBrowser.js (get_dom) before sending DOM to the LLM
|
|
21
|
+
* - Any future DOM-inspection tools that need a compact, semantic HTML view
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Remove tags and attributes that are unlikely to help LLM reasoning.
|
|
26
|
+
* - Strips: script, style, meta, link, noscript, iframe, canvas, svg
|
|
27
|
+
* - Keeps only a safe whitelist of attributes on remaining elements.
|
|
28
|
+
* - Removes HTML comments.
|
|
29
|
+
*
|
|
30
|
+
* @param {$} $ - Cheerio root
|
|
31
|
+
* @returns {$} same Cheerio instance for chaining
|
|
32
|
+
*/
|
|
1
33
|
export const removeNonContentTags = async ($) => {
|
|
2
34
|
$('script, style, meta, link, noscript, iframe, canvas, svg').remove();
|
|
3
35
|
|
|
@@ -34,6 +66,19 @@ export const removeNonContentTags = async ($) => {
|
|
|
34
66
|
return $;
|
|
35
67
|
}
|
|
36
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Collapse very long lists and container sections.
|
|
71
|
+
* - For <ul>/<ol>, keeps the first `limit` <li> children and appends a
|
|
72
|
+
* sentinel <li data-collapsed="true">[...truncated]</li>.
|
|
73
|
+
* - For <div>/<section>, keeps the first `limit` child elements and appends
|
|
74
|
+
* a sentinel <div data-collapsed="true">[...truncated]</div>.
|
|
75
|
+
*
|
|
76
|
+
* This is meant to cut down giant feeds/tables while preserving structure.
|
|
77
|
+
*
|
|
78
|
+
* @param {$} $ - Cheerio root
|
|
79
|
+
* @param {number} [limit=10] - Max children to preserve per container
|
|
80
|
+
* @returns {$} same Cheerio instance for chaining
|
|
81
|
+
*/
|
|
37
82
|
export const collapseLongContent = async ($, limit = 10) => {
|
|
38
83
|
$('ul, ol').each((_, el) => {
|
|
39
84
|
const $el = $(el);
|
|
@@ -56,6 +101,18 @@ export const collapseLongContent = async ($, limit = 10) => {
|
|
|
56
101
|
});
|
|
57
102
|
}
|
|
58
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Extract logical sections of the page into named chunks.
|
|
106
|
+
* - Returns an object keyed by region: header, nav, main, footer, aside, body.
|
|
107
|
+
* - Each key maps to the inner HTML of that section (or '' if missing).
|
|
108
|
+
* - If `focus` is a non-empty array, only the requested keys are returned.
|
|
109
|
+
*
|
|
110
|
+
* This is a structural pre-step for “header/nav/main/…” focus slicing.
|
|
111
|
+
*
|
|
112
|
+
* @param {$} $ - Cheerio root
|
|
113
|
+
* @param {string[]} [focus] - Optional list of section keys to keep
|
|
114
|
+
* @returns {Record<string,string>} map of sectionName -> html
|
|
115
|
+
*/
|
|
59
116
|
export const chunkBySection = async ($, focus) => {
|
|
60
117
|
const chunks = {
|
|
61
118
|
header: $('header').html() || '',
|
|
@@ -85,6 +142,18 @@ export const chunkBySection = async ($, focus) => {
|
|
|
85
142
|
|
|
86
143
|
}
|
|
87
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Reduce repeated sibling <div> blocks that share the same class name.
|
|
147
|
+
* - For each parent <body> or <div>, group child divs by `class`.
|
|
148
|
+
* - If a group exceeds `maxAllowed`, remove the extras and append a
|
|
149
|
+
* sentinel <div data-collapsed="true" data-class="...">[...truncated]</div>.
|
|
150
|
+
*
|
|
151
|
+
* This keeps a few representative cards/rows and collapses the rest.
|
|
152
|
+
*
|
|
153
|
+
* @param {$} $ - Cheerio root
|
|
154
|
+
* @param {number} [maxAllowed=3] - Max sibling divs per class to keep
|
|
155
|
+
* @returns {$} same Cheerio instance for chaining
|
|
156
|
+
*/
|
|
88
157
|
export function reduceRepeatedDivs($, maxAllowed = 3) {
|
|
89
158
|
$('body, div').each((_, parentEl) => {
|
|
90
159
|
const $parent = $(parentEl);
|
|
@@ -117,8 +186,17 @@ export function reduceRepeatedDivs($, maxAllowed = 3) {
|
|
|
117
186
|
}
|
|
118
187
|
|
|
119
188
|
/**
|
|
120
|
-
* Remove obfuscated or hashed class names from
|
|
121
|
-
* Criteria
|
|
189
|
+
* Remove obfuscated or hashed class names from elements.
|
|
190
|
+
* Criteria for stripping:
|
|
191
|
+
* - Very short classes (len <= 2).
|
|
192
|
+
* - Lowercase+numeric hashes like "a1b2c3".
|
|
193
|
+
* - Mixed-case hashes with digits like "AbCdE1".
|
|
194
|
+
*
|
|
195
|
+
* The goal is to keep semantic class names (e.g. "btn-primary") and drop
|
|
196
|
+
* ones that look like auto-generated hashes.
|
|
197
|
+
*
|
|
198
|
+
* @param {$} $ - Cheerio root
|
|
199
|
+
* @returns {$} same Cheerio instance for chaining
|
|
122
200
|
*/
|
|
123
201
|
export function removeObfuscatedClassNames($) {
|
|
124
202
|
$('[class]').each((_, el) => {
|
|
@@ -141,7 +219,16 @@ export function removeObfuscatedClassNames($) {
|
|
|
141
219
|
}
|
|
142
220
|
|
|
143
221
|
/**
|
|
144
|
-
* Remove known auto-generated
|
|
222
|
+
* Remove known auto-generated / utility CSS classes (Tailwind, MUI, etc.).
|
|
223
|
+
* - Matches a broad set of patterns for spacing, layout, color, breakpoint
|
|
224
|
+
* variants, and other low-signal utility styles.
|
|
225
|
+
* - Preserves any remaining classes that don’t match those patterns.
|
|
226
|
+
*
|
|
227
|
+
* This keeps semantic-ish classes (e.g., "btn-primary", "modal-root")
|
|
228
|
+
* while stripping the utility noise that doesn’t help the LLM.
|
|
229
|
+
*
|
|
230
|
+
* @param {$} $ - Cheerio root
|
|
231
|
+
* @returns {$} same Cheerio instance for chaining
|
|
145
232
|
*/
|
|
146
233
|
export function removeAutoGeneratedClasses($) {
|
|
147
234
|
// Regex patterns for utility classes, internal prefixes, and other noise
|
|
@@ -168,3 +255,42 @@ export function removeAutoGeneratedClasses($) {
|
|
|
168
255
|
|
|
169
256
|
return $;
|
|
170
257
|
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Minify DOM by collapsing whitespace in text nodes.
|
|
261
|
+
* - Collapses all runs of whitespace to a single space.
|
|
262
|
+
* - Preserves whitespace in <pre>, <code>, <textarea> (where formatting matters).
|
|
263
|
+
* - Keeps at least one space for purely-whitespace nodes to avoid jamming
|
|
264
|
+
* words from adjacent nodes together.
|
|
265
|
+
*
|
|
266
|
+
* This is intended as a low-risk token saver before sending DOM to the LLM.
|
|
267
|
+
*
|
|
268
|
+
* @param {$} $ - Cheerio root
|
|
269
|
+
* @returns {$} same Cheerio instance for chaining
|
|
270
|
+
*/
|
|
271
|
+
export function minifyDomWhitespace($) {
|
|
272
|
+
const PRESERVE_SELECTOR = 'pre, code, textarea';
|
|
273
|
+
|
|
274
|
+
$('*').contents().each(function () {
|
|
275
|
+
if (this.type !== 'text') return;
|
|
276
|
+
|
|
277
|
+
// Skip any text node living inside pre/code/textarea
|
|
278
|
+
if ($(this).closest(PRESERVE_SELECTOR).length) return;
|
|
279
|
+
|
|
280
|
+
const orig = this.data || '';
|
|
281
|
+
if (!orig) return;
|
|
282
|
+
|
|
283
|
+
// Collapse all whitespace to single spaces
|
|
284
|
+
const collapsed = orig.replace(/\s+/g, ' ');
|
|
285
|
+
|
|
286
|
+
if (!orig.trim()) {
|
|
287
|
+
// Pure whitespace → keep a single space so adjacent words don't concatenate
|
|
288
|
+
this.data = ' ';
|
|
289
|
+
} else {
|
|
290
|
+
// Normal text → trim ends, keep single internal spaces
|
|
291
|
+
this.data = collapsed.trim();
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
return $;
|
|
296
|
+
}
|
package/tools/tokenControl.js
CHANGED
|
@@ -35,6 +35,7 @@ import { wait } from './turnLoopUtils.js';
|
|
|
35
35
|
*/
|
|
36
36
|
const DEFAULT_LIMITS = [
|
|
37
37
|
// ── OpenAI (newer first) ────────────────────────────────────────────────
|
|
38
|
+
{ test: /^gpt-5.1(-|$)/i, tpm: 120000 },
|
|
38
39
|
{ test: /^gpt-5(-|$)/i, tpm: 90000 },
|
|
39
40
|
{ test: /^gpt-5-mini(-|$)/i, tpm: 240000 },
|
|
40
41
|
{ test: /^gpt-5-nano(-|$)/i, tpm: 600000 },
|
|
@@ -71,6 +72,13 @@ const warnedModels = new Set();
|
|
|
71
72
|
* - o200k_base for modern, long-context families (OpenAI 4o/4.1/5, Gemini 2.5, O*)
|
|
72
73
|
* - cl100k_base as a broad fallback
|
|
73
74
|
*/
|
|
75
|
+
/**
|
|
76
|
+
* Resolve the best-effort tokenizer for a model.
|
|
77
|
+
* Falls back to o200k_base for modern long-context models and cl100k_base otherwise.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} model - Model identifier used to pick an encoding.
|
|
80
|
+
* @returns {import('@dqbd/tiktoken').Tiktoken | null} tokenizer or null on failure
|
|
81
|
+
*/
|
|
74
82
|
function getTokenizer(model) {
|
|
75
83
|
try {
|
|
76
84
|
return encoding_for_model(model);
|
|
@@ -228,6 +236,7 @@ export const tokenUseCoolOff = async (totalTokensUsed, turnTimestamps, model) =>
|
|
|
228
236
|
* Record tokens for the current turn in the rolling window.
|
|
229
237
|
* @param {Array<[number,number]>} turnTimestamps
|
|
230
238
|
* @param {number} tokensUsed
|
|
239
|
+
* @returns {void}
|
|
231
240
|
*/
|
|
232
241
|
export const recordTokenUsage = (turnTimestamps, tokensUsed) => {
|
|
233
242
|
const now = Date.now();
|
|
@@ -272,8 +281,11 @@ const getDynamicBackoffMs = async (turnTimestamps, tokenLimit) => {
|
|
|
272
281
|
};
|
|
273
282
|
|
|
274
283
|
|
|
275
|
-
|
|
284
|
+
/**
|
|
285
|
+
* Test-only helper to clear internal state between runs.
|
|
286
|
+
* @returns {void}
|
|
287
|
+
*/
|
|
276
288
|
export function __resetTokenControlForTests() {
|
|
277
289
|
liveLimits.clear();
|
|
278
290
|
warnedModels.clear();
|
|
279
|
-
}
|
|
291
|
+
}
|
package/tools/toolSchema.js
CHANGED
|
@@ -285,6 +285,107 @@ const toolsSchema = [
|
|
|
285
285
|
required: []
|
|
286
286
|
}
|
|
287
287
|
}
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
type: 'function',
|
|
291
|
+
function: {
|
|
292
|
+
name: 'set_ground_control_state',
|
|
293
|
+
description: 'Update stable mission facts for Ground Control: app URL, current page role, login status, and constraints. Use this when you learn something that should remain true across future turns and should not be forgotten.',
|
|
294
|
+
parameters: {
|
|
295
|
+
type: 'object',
|
|
296
|
+
properties: {
|
|
297
|
+
app: {
|
|
298
|
+
type: 'object',
|
|
299
|
+
description: 'App/URL related state. Only send fields you are certain about.',
|
|
300
|
+
properties: {
|
|
301
|
+
baseUrl: {
|
|
302
|
+
type: 'string',
|
|
303
|
+
description: 'Canonical base URL of the app under test, e.g. "https://demo.testronaut.app".'
|
|
304
|
+
},
|
|
305
|
+
currentUrl: {
|
|
306
|
+
type: 'string',
|
|
307
|
+
description: 'Current page URL after navigation.'
|
|
308
|
+
},
|
|
309
|
+
routeRole: {
|
|
310
|
+
type: 'string',
|
|
311
|
+
description: 'Short label for the current page role, e.g. "login", "chat", "dashboard".'
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
additionalProperties: false
|
|
315
|
+
},
|
|
316
|
+
session: {
|
|
317
|
+
type: 'object',
|
|
318
|
+
description: 'User/session related facts.',
|
|
319
|
+
properties: {
|
|
320
|
+
loggedIn: {
|
|
321
|
+
type: 'boolean',
|
|
322
|
+
description: 'Whether the user is currently logged in.'
|
|
323
|
+
},
|
|
324
|
+
userLabel: {
|
|
325
|
+
type: 'string',
|
|
326
|
+
description: 'Short label for the user account, e.g. "Demo user", "Buzz Aldrin".'
|
|
327
|
+
},
|
|
328
|
+
tenant: {
|
|
329
|
+
type: 'string',
|
|
330
|
+
description: 'Tenant or workspace name, if applicable.'
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
additionalProperties: false
|
|
334
|
+
},
|
|
335
|
+
navigation: {
|
|
336
|
+
type: 'object',
|
|
337
|
+
description: 'Navigation-related state.',
|
|
338
|
+
properties: {
|
|
339
|
+
currentLabel: {
|
|
340
|
+
type: 'string',
|
|
341
|
+
description: 'Short human label for the current page, e.g. "Main chat page".'
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
additionalProperties: false
|
|
345
|
+
},
|
|
346
|
+
constraints: {
|
|
347
|
+
type: 'object',
|
|
348
|
+
description: 'Mission or environment constraints.',
|
|
349
|
+
properties: {
|
|
350
|
+
stayWithinBaseUrl: {
|
|
351
|
+
type: 'boolean',
|
|
352
|
+
description: 'If true, do not navigate outside the app base URL unless explicitly instructed.'
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
additionalProperties: false
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
additionalProperties: false
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
type: 'function',
|
|
364
|
+
function: {
|
|
365
|
+
name: 'record_mission_telemetry',
|
|
366
|
+
description: 'Record a small, durable observation for the mission log: breadcrumbs, assertions, issues, or general notes for Ground Control.',
|
|
367
|
+
parameters: {
|
|
368
|
+
type: 'object',
|
|
369
|
+
properties: {
|
|
370
|
+
kind: {
|
|
371
|
+
type: 'string',
|
|
372
|
+
enum: ['breadcrumb', 'assertion', 'issue', 'note'],
|
|
373
|
+
description: 'Type of observation: navigation breadcrumb, assertion, discovered issue, or general note.'
|
|
374
|
+
},
|
|
375
|
+
text: {
|
|
376
|
+
type: 'string',
|
|
377
|
+
description: 'Short description of the observation in 1–2 sentences.'
|
|
378
|
+
},
|
|
379
|
+
status: {
|
|
380
|
+
type: 'string',
|
|
381
|
+
enum: ['passed', 'failed', 'n/a'],
|
|
382
|
+
description: 'Optional status for assertions or issues.'
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
required: ['kind', 'text'],
|
|
386
|
+
additionalProperties: false
|
|
387
|
+
}
|
|
388
|
+
}
|
|
288
389
|
}
|
|
289
390
|
];
|
|
290
391
|
|
package/tools/turnLoopUtils.js
CHANGED
|
@@ -20,7 +20,6 @@ export const wait = (ms) => new Promise(res => setTimeout(res, ms));
|
|
|
20
20
|
export function validateAndInsertMissingToolResponses(messages, options = {}) {
|
|
21
21
|
const { insertPlaceholders = true } = options;
|
|
22
22
|
const missingResponses = [];
|
|
23
|
-
|
|
24
23
|
for (let i = 0; i < messages.length; i++) {
|
|
25
24
|
const msg = messages[i];
|
|
26
25
|
if (msg.role === 'assistant' && msg.tool_calls?.length) {
|