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.
@@ -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 a DOM string.
121
- * Criteria: class names with high entropy (random letters/numbers), or very short length
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 or utility classes (e.g., Tailwind/MUI/CSS-in-JS)
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
+ }
@@ -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
- // Test-only helper to clear internal state between tests
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
+ }
@@ -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
 
@@ -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) {