testronaut 1.10.2 → 1.12.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.
Files changed (36) hide show
  1. package/README.md +198 -0
  2. package/TODO.md +5 -0
  3. package/bin/cli.js +234 -5
  4. package/bin/terminalUi.js +51 -0
  5. package/core/agent.js +1 -0
  6. package/core/browserEvidence.js +184 -0
  7. package/core/jevRouting.js +77 -0
  8. package/core/jevShadow.js +325 -0
  9. package/core/jevSuiteSelection.js +39 -0
  10. package/core/jevUsage.js +62 -0
  11. package/core/optimization.js +98 -0
  12. package/core/turnLoop.js +174 -10
  13. package/experiments/CROSS_PROVIDER.md +19 -0
  14. package/experiments/README.md +62 -0
  15. package/experiments/buildExperimentPublicationPackage.js +219 -0
  16. package/experiments/buildOptimizationDataPackage.js +390 -0
  17. package/experiments/crossProviderBenchmark.js +227 -0
  18. package/experiments/optimizationBenchmark.js +339 -0
  19. package/package.json +5 -1
  20. package/tests/binTests/cli.helpers.test.js +16 -0
  21. package/tests/binTests/cli.integration.test.js +14 -0
  22. package/tests/binTests/terminalUi.test.js +34 -0
  23. package/tests/coreTests/browserEvidence.test.js +70 -0
  24. package/tests/coreTests/jevRouting.test.js +57 -0
  25. package/tests/coreTests/jevShadow.test.js +158 -0
  26. package/tests/coreTests/jevSuiteSelection.test.js +24 -0
  27. package/tests/coreTests/jevUsage.test.js +58 -0
  28. package/tests/coreTests/optimization.test.js +80 -0
  29. package/tests/coreTests/turnLoop.test.js +304 -0
  30. package/tests/experimentTests/buildOptimizationDataPackage.test.js +25 -0
  31. package/tests/experimentTests/crossProviderBenchmark.test.js +37 -0
  32. package/tests/experimentTests/optimizationBenchmark.test.js +49 -0
  33. package/tests/toolsTests/generateHtmlReport.test.js +83 -0
  34. package/tests/toolsTests/turnLoopUtils.test.js +17 -0
  35. package/tools/generateHtmlReport.js +96 -26
  36. package/tools/turnLoopUtils.js +7 -11
@@ -0,0 +1,184 @@
1
+ import * as cheerio from 'cheerio';
2
+
3
+ function compact(value, maxLength = 160) {
4
+ const text = String(value ?? '').replace(/\s+/g, ' ').trim();
5
+ return text.length <= maxLength ? text : `${text.slice(0, maxLength)}…`;
6
+ }
7
+
8
+ function safeUrl(value) {
9
+ try {
10
+ const url = new URL(String(value));
11
+ return `${url.origin}${url.pathname}`;
12
+ } catch {
13
+ return compact(String(value ?? '').split(/[?#]/, 1)[0], 500) || null;
14
+ }
15
+ }
16
+
17
+ function unique(values, limit) {
18
+ return [...new Set(values.filter(Boolean))].slice(0, limit);
19
+ }
20
+
21
+ function controlLabel($, element) {
22
+ const node = $(element);
23
+ const id = node.attr('id');
24
+ const explicitLabel = id
25
+ ? $('label').filter((_, label) => $(label).attr('for') === id).first().text()
26
+ : '';
27
+ return compact(
28
+ node.attr('aria-label')
29
+ || explicitLabel
30
+ || node.closest('label').text()
31
+ || node.attr('placeholder')
32
+ || node.text(),
33
+ 120,
34
+ );
35
+ }
36
+
37
+ function safeSelectorAttribute(value) {
38
+ return compact(value, 100).replace(/[^A-Za-z0-9_.:-]/g, '');
39
+ }
40
+
41
+ function selectorFor($, element, fallbackIndex) {
42
+ const node = $(element);
43
+ const id = safeSelectorAttribute(node.attr('id'));
44
+ if (id) return `#${id}`;
45
+ const tag = String(element.tagName || element.name || 'section').toLowerCase();
46
+ return `${tag}:nth-of-type(${fallbackIndex + 1})`;
47
+ }
48
+
49
+ /**
50
+ * Extract bounded, value-free evidence from a DOM snapshot already collected by
51
+ * Testronaut. Input values, query strings, hashes, and body text are omitted.
52
+ */
53
+ export function buildBrowserEvidence(html, {
54
+ url,
55
+ previous = null,
56
+ redactText = value => String(value ?? ''),
57
+ } = {}) {
58
+ const currentUrl = safeUrl(url) || previous?.url || null;
59
+ const routeChanged = Boolean(previous?.url && currentUrl && previous.url !== currentUrl);
60
+ const base = !routeChanged && previous ? { ...previous } : {};
61
+ const evidence = {
62
+ ...base,
63
+ url: currentUrl,
64
+ previousUrl: routeChanged ? previous.url : (previous?.previousUrl || null),
65
+ routeChanged,
66
+ };
67
+
68
+ if (typeof html !== 'string' || !html.includes('<')) return evidence;
69
+
70
+ const $ = cheerio.load(html);
71
+ const redact = value => compact(redactText(compact(value)), 160);
72
+ const headings = unique(
73
+ $('h1, h2, h3').map((_, element) => redact($(element).text())).get(),
74
+ 12,
75
+ );
76
+ const controls = [];
77
+ $('button, a[href], input, select, textarea, [role="button"], [role="link"]').each((_, element) => {
78
+ if (controls.length >= 20) return;
79
+ const node = $(element);
80
+ const tag = String(element.tagName || element.name || '').toLowerCase();
81
+ const type = compact(node.attr('type'), 40);
82
+ controls.push({
83
+ tag,
84
+ ...(node.attr('role') ? { role: compact(node.attr('role'), 40) } : {}),
85
+ ...(type ? { type } : {}),
86
+ ...(controlLabel($, element) ? { label: redact(controlLabel($, element)) } : {}),
87
+ ...(node.attr('id') ? { id: safeSelectorAttribute(node.attr('id')) } : {}),
88
+ ...(node.attr('name') ? { name: safeSelectorAttribute(node.attr('name')) } : {}),
89
+ ...(node.attr('href') ? { href: safeUrl(node.attr('href')) } : {}),
90
+ });
91
+ });
92
+ const regions = [];
93
+ $('header, nav, main, aside, footer, form, table, [role="dialog"], [role="main"], [role="navigation"]').each((index, element) => {
94
+ if (regions.length >= 12) return;
95
+ const node = $(element);
96
+ const kind = node.attr('role') || String(element.tagName || element.name || 'region').toLowerCase();
97
+ const regionHeadings = unique(
98
+ node.find('h1, h2, h3').map((_, heading) => redact($(heading).text())).get(),
99
+ 4,
100
+ );
101
+ const regionControls = unique(
102
+ node.find('button, a[href], input, select, textarea, [role="button"], [role="link"]')
103
+ .map((_, control) => redact(controlLabel($, control))).get(),
104
+ 8,
105
+ );
106
+ regions.push({
107
+ id: `region_${regions.length + 1}`,
108
+ kind: compact(kind, 40),
109
+ selector: selectorFor($, element, index),
110
+ headings: regionHeadings,
111
+ controls: regionControls,
112
+ counts: {
113
+ headings: node.find('h1, h2, h3').length,
114
+ controls: node.find('button, a[href], input, select, textarea, [role="button"], [role="link"]').length,
115
+ },
116
+ });
117
+ });
118
+
119
+ return {
120
+ ...evidence,
121
+ title: redact($('title').first().text()),
122
+ headings,
123
+ controls,
124
+ regions,
125
+ counts: {
126
+ headings: $('h1, h2, h3').length,
127
+ forms: $('form').length,
128
+ buttons: $('button, [role="button"]').length,
129
+ links: $('a[href], [role="link"]').length,
130
+ inputs: $('input, select, textarea').length,
131
+ },
132
+ };
133
+ }
134
+
135
+ export function mergeBrowserControlState(evidence, states = []) {
136
+ if (!evidence || !Array.isArray(evidence.controls)) return evidence;
137
+ return {
138
+ ...evidence,
139
+ controls: evidence.controls.map((control, index) => {
140
+ const state = states[index];
141
+ if (!state) return control;
142
+ return {
143
+ ...control,
144
+ hasValue: Boolean(state.hasValue),
145
+ checked: Boolean(state.checked),
146
+ disabled: Boolean(state.disabled),
147
+ readOnly: Boolean(state.readOnly),
148
+ visible: Boolean(state.visible),
149
+ };
150
+ }),
151
+ };
152
+ }
153
+
154
+ /**
155
+ * Read only boolean control state from the live page. Values and value lengths
156
+ * never leave the browser context.
157
+ */
158
+ export async function collectBrowserControlState(browser) {
159
+ try {
160
+ const locator = browser?.page?.locator?.(
161
+ 'button, a[href], input, select, textarea, [role="button"], [role="link"]',
162
+ );
163
+ if (!locator?.evaluateAll) return [];
164
+ return await locator.evaluateAll(elements => elements.slice(0, 20).map(element => {
165
+ const tag = element.tagName.toLowerCase();
166
+ const style = getComputedStyle(element);
167
+ const rect = element.getBoundingClientRect();
168
+ const supportsValue = tag === 'input' || tag === 'select' || tag === 'textarea';
169
+ const inputType = tag === 'input' ? String(element.type || '').toLowerCase() : '';
170
+ const supportsChecked = tag === 'input' && ['checkbox', 'radio'].includes(inputType);
171
+ return {
172
+ hasValue: supportsValue ? String(element.value ?? '').length > 0 : false,
173
+ checked: supportsChecked ? Boolean(element.checked) : false,
174
+ disabled: 'disabled' in element ? Boolean(element.disabled) : element.getAttribute('aria-disabled') === 'true',
175
+ readOnly: 'readOnly' in element ? Boolean(element.readOnly) : false,
176
+ visible: rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden',
177
+ };
178
+ }));
179
+ } catch {
180
+ return [];
181
+ }
182
+ }
183
+
184
+ export const __browserEvidenceInternals = { compact, safeUrl };
@@ -0,0 +1,77 @@
1
+ const DEFAULT_CONFIDENCE_THRESHOLD = 0.4;
2
+
3
+ function probability(value, fallback) {
4
+ const parsed = Number(value);
5
+ return Number.isFinite(parsed) && parsed >= 0 && parsed <= 1 ? parsed : fallback;
6
+ }
7
+
8
+ export function resolveJevRoutingConfig(env = process.env, { provider, primaryModel } = {}) {
9
+ const rawMode = String(env.TESTRONAUT_JEV_ROUTING || 'off').trim().toLowerCase();
10
+ const mode = ['shadow', 'live'].includes(rawMode) ? rawMode : 'off';
11
+ const providerKey = String(provider).toLowerCase();
12
+ const familyDefault = {
13
+ openai: 'gpt-5.6-luna',
14
+ gemini: 'gemini-3.5-flash-lite',
15
+ anthropic: 'claude-haiku-4-5',
16
+ claude: 'claude-haiku-4-5',
17
+ }[providerKey] || '';
18
+ const defaultFastModel = primaryModel === familyDefault ? '' : familyDefault;
19
+ return {
20
+ mode,
21
+ fastModel: String(env.TESTRONAUT_JEV_FAST_MODEL || defaultFastModel).trim(),
22
+ confidenceThreshold: probability(
23
+ env.TESTRONAUT_JEV_ROUTING_THRESHOLD,
24
+ DEFAULT_CONFIDENCE_THRESHOLD,
25
+ ),
26
+ };
27
+ }
28
+
29
+ function belongsToProvider(model, provider) {
30
+ const value = String(model || '').toLowerCase();
31
+ switch (String(provider || '').toLowerCase()) {
32
+ case 'openai': return /^(gpt-|o\d)/.test(value);
33
+ case 'gemini': return value.startsWith('gemini-');
34
+ case 'anthropic':
35
+ case 'claude': return value.startsWith('claude-');
36
+ default: return true;
37
+ }
38
+ }
39
+
40
+ export function chooseJevTurnModel(strategy, config, { provider, primaryModel } = {}) {
41
+ const route = strategy?.modelRoute;
42
+ const score = Number(route?.probabilities?.fast_model);
43
+ const confidence = Number(route?.confidence);
44
+ const recommendation = route?.choice || null;
45
+ const stage = strategy?.missionStage?.choice || null;
46
+ const recovery = strategy?.recovery?.choice || null;
47
+ const blockedReason =
48
+ config.mode === 'off' ? 'routing_off' :
49
+ recommendation !== 'fast_model' ? 'not_fast_model' :
50
+ !config.fastModel ? 'fast_model_unconfigured' :
51
+ !belongsToProvider(config.fastModel, provider) ? 'fast_model_provider_mismatch' :
52
+ config.fastModel === primaryModel ? 'fast_model_matches_primary' :
53
+ !Number.isFinite(score) || score < config.confidenceThreshold ? 'low_probability' :
54
+ ['authenticate', 'failed', 'complete'].includes(stage) ? `stage_${stage}` :
55
+ (recovery && recovery !== 'continue') ? `recovery_${recovery}` :
56
+ null;
57
+ const eligible = !blockedReason;
58
+ const applied = eligible && config.mode === 'live';
59
+
60
+ return {
61
+ mode: config.mode,
62
+ provider,
63
+ primaryModel,
64
+ fastModel: config.fastModel || null,
65
+ selectedModel: applied ? config.fastModel : primaryModel,
66
+ recommendation,
67
+ confidence: Number.isFinite(confidence) ? confidence : null,
68
+ score: Number.isFinite(score) ? score : null,
69
+ scoreType: 'fast_model_probability',
70
+ threshold: config.confidenceThreshold,
71
+ eligible,
72
+ applied,
73
+ reason: blockedReason || (applied ? 'jev_fast_model' : 'shadow_only'),
74
+ };
75
+ }
76
+
77
+ export const __jevRoutingInternals = { DEFAULT_CONFIDENCE_THRESHOLD, probability, belongsToProvider };
@@ -0,0 +1,325 @@
1
+ const DEFAULT_API_BASE = 'https://api.typesafe.ai';
2
+ const DEFAULT_MODEL = 'jev-latest';
3
+ const DEFAULT_TIMEOUT_MS = 5000;
4
+ const DEFAULT_GATE_THRESHOLD = 0.8;
5
+
6
+ const SHADOW_QUESTIONS = {
7
+ missionComplete: {
8
+ type: 'noul',
9
+ instructions: 'Is there enough evidence that the stated browser-testing mission is complete?',
10
+ criteria: {
11
+ true: 'The goal is demonstrably satisfied by the current state or observed results.',
12
+ false: 'The goal is incomplete, failed, uncertain, or still requires verification.',
13
+ },
14
+ },
15
+ progressState: {
16
+ type: 'choice',
17
+ instructions: 'Classify the current progress of this browser-testing mission.',
18
+ criteria: {
19
+ complete: 'The mission is demonstrably complete.',
20
+ advancing: 'Recent actions made useful progress, but more work is required.',
21
+ stalled: 'The agent is repeating, blocked, or not making useful progress.',
22
+ failed: 'The available evidence shows the mission cannot or did not complete.',
23
+ uncertain: 'There is not enough evidence to classify progress reliably.',
24
+ },
25
+ },
26
+ nextCompute: {
27
+ type: 'choice',
28
+ instructions: 'What kind of computation should Testronaut use next?',
29
+ criteria: {
30
+ finish: 'No more browser-agent reasoning is needed.',
31
+ deterministic_action: 'A known browser action can run without generative reasoning.',
32
+ observe: 'More browser state should be collected before deciding.',
33
+ full_reasoning: 'A generative reasoning model is needed to decide or construct the next action.',
34
+ stop_failed: 'The mission should stop as failed rather than spend more tokens.',
35
+ },
36
+ },
37
+ };
38
+
39
+ const STRATEGY_QUESTIONS = {
40
+ modelRoute: {
41
+ type: 'choice',
42
+ instructions: 'Choose the least expensive reliable computation for the next turn.',
43
+ criteria: {
44
+ deterministic: 'The exact action and every required argument are already constructed in state; code can execute it directly.',
45
+ fast_model: 'The relevant control or local intent is apparent, but a short tool choice, known-value mapping, or localized inference is still needed.',
46
+ full_model: 'Multi-step planning, broad ambiguity, novel content, or difficult recovery requires frontier reasoning.',
47
+ observe: 'Collect more browser evidence before choosing.',
48
+ finish: 'The mission is complete and should finish.',
49
+ },
50
+ },
51
+ postconditionSatisfied: {
52
+ type: 'noul',
53
+ instructions: 'Did the most recent browser action satisfy its intended postcondition?',
54
+ criteria: {
55
+ true: 'Structured browser evidence shows the intended state change or result.',
56
+ false: 'The result is absent, contradicted, failed, or cannot yet be verified.',
57
+ },
58
+ },
59
+ recoveryRoute: {
60
+ type: 'choice',
61
+ instructions: 'Choose the best recovery policy for the current mission state.',
62
+ criteria: {
63
+ continue: 'Progress is healthy; continue normally.',
64
+ retry_action: 'Retry the most recent action once.',
65
+ reobserve: 'Refresh browser evidence before deciding.',
66
+ escalate: 'Use the full reasoning model to recover.',
67
+ stop_failed: 'Further work is unlikely to succeed; stop as failed.',
68
+ },
69
+ },
70
+ missionStage: {
71
+ type: 'choice',
72
+ instructions: 'Classify the current stage of this browser mission.',
73
+ criteria: {
74
+ setup: 'Preparing browser or initial state.',
75
+ navigate: 'Navigating to the target area.',
76
+ authenticate: 'Establishing or verifying authentication.',
77
+ interact: 'Performing the mission-specific user actions.',
78
+ verify: 'Checking that the requested outcome occurred.',
79
+ complete: 'The mission outcome is complete.',
80
+ failed: 'The mission cannot complete.',
81
+ },
82
+ },
83
+ prerequisiteSatisfied: {
84
+ type: 'noul',
85
+ instructions: 'Does the current browser state already satisfy the mission prerequisite or required starting state?',
86
+ criteria: {
87
+ true: 'The required session, route, and starting conditions are already present.',
88
+ false: 'Setup, navigation, authentication, or other prerequisite work remains.',
89
+ },
90
+ },
91
+ };
92
+
93
+ function truthy(value) {
94
+ return /^(1|true|yes|on)$/i.test(String(value ?? '').trim());
95
+ }
96
+
97
+ function positiveInteger(value, fallback) {
98
+ const parsed = Number(value);
99
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
100
+ }
101
+
102
+ function probability(value, fallback) {
103
+ const parsed = Number(value);
104
+ return Number.isFinite(parsed) && parsed >= 0 && parsed <= 1 ? parsed : fallback;
105
+ }
106
+
107
+ function trimTrailingSlash(value) {
108
+ return String(value || DEFAULT_API_BASE).replace(/\/+$/, '');
109
+ }
110
+
111
+ function compactText(value, maxLength = 2000) {
112
+ const text = typeof value === 'string' ? value : JSON.stringify(value ?? '');
113
+ if (text.length <= maxLength) return text;
114
+ return `${text.slice(0, maxLength)}…[truncated ${text.length - maxLength} chars]`;
115
+ }
116
+
117
+ export function resolveJevShadowConfig(env = process.env) {
118
+ const gateEnabled = truthy(env.TESTRONAUT_JEV_GATE);
119
+ const routingEnabled = /^(shadow|live)$/i.test(String(env.TESTRONAUT_JEV_ROUTING || '').trim());
120
+ const strategiesEnabled = truthy(env.TESTRONAUT_JEV_STRATEGIES) || routingEnabled;
121
+ return {
122
+ enabled: truthy(env.TESTRONAUT_JEV_SHADOW) || gateEnabled || strategiesEnabled,
123
+ gateEnabled,
124
+ gateThreshold: probability(env.TESTRONAUT_JEV_GATE_THRESHOLD, DEFAULT_GATE_THRESHOLD),
125
+ strategiesEnabled,
126
+ apiKey: env.TESTRONAUT_JEV_API_KEY || env.TYPESAFE_API_KEY || env.JEV_API_KEY || '',
127
+ apiBase: trimTrailingSlash(env.TESTRONAUT_JEV_API_BASE || env.TYPESAFE_BASE_URL),
128
+ model: env.TESTRONAUT_JEV_MODEL || env.TYPESAFE_DEFAULT_MODEL || DEFAULT_MODEL,
129
+ timeoutMs: positiveInteger(env.TESTRONAUT_JEV_TIMEOUT_MS, DEFAULT_TIMEOUT_MS),
130
+ };
131
+ }
132
+
133
+ function actionDescription(control = {}) {
134
+ const selector = control.id ? `#${control.id}` : control.name ? `[name="${control.name}"]` : '';
135
+ const label = control.label ? ` “${control.label}”` : '';
136
+ const state = [
137
+ control.hasValue ? 'currently populated' : 'currently empty',
138
+ control.checked ? 'checked' : '',
139
+ control.disabled ? 'disabled' : 'enabled',
140
+ control.visible === false ? 'not visible' : 'visible',
141
+ ].filter(Boolean).join(', ');
142
+ if (['input', 'select', 'textarea'].includes(control.tag)) {
143
+ return `Fill or select ${control.tag}${label}${selector ? ` at ${selector}` : ''} using an already-known mission value. Control state: ${state}. Do not refill an already populated control unless replacement is required.`;
144
+ }
145
+ return `Click ${control.role || control.tag || 'control'}${label}${selector ? ` at ${selector}` : ''}. Control state: ${state}.`;
146
+ }
147
+
148
+ export function buildJevStrategyQuestions(browserEvidence = {}) {
149
+ const questions = { ...SHADOW_QUESTIONS, ...STRATEGY_QUESTIONS };
150
+ const regions = (browserEvidence.regions || []).slice(0, 12);
151
+ for (const region of regions) {
152
+ questions[`regionRelevant_${region.id}`] = {
153
+ type: 'noul',
154
+ instructions: `Is browser region ${region.id} relevant to deciding or executing the next mission action?`,
155
+ criteria: {
156
+ true: 'The region contains evidence or controls needed for the next action.',
157
+ false: 'The region can be omitted from the next generative-model context.',
158
+ },
159
+ };
160
+ }
161
+
162
+ const controls = (browserEvidence.controls || []).slice(0, 20);
163
+ const actionCriteria = {
164
+ observe: 'Collect more browser evidence before acting.',
165
+ full_reasoning: 'No listed action is safely executable without generative reasoning.',
166
+ };
167
+ controls.forEach((control, index) => {
168
+ actionCriteria[`action_${index + 1}`] = actionDescription(control);
169
+ });
170
+ if (controls.length) {
171
+ questions.actionChoice = {
172
+ type: 'choice',
173
+ instructions: 'Choose the safest next action for progressing the mission.',
174
+ criteria: actionCriteria,
175
+ };
176
+ }
177
+ return { questions, regions, controls };
178
+ }
179
+
180
+ export function extractJevStrategyResults(answers = {}, catalog = {}) {
181
+ const regionRelevance = (catalog.regions || []).map(region => ({
182
+ ...region,
183
+ probability: Number(answers[`regionRelevant_${region.id}`]?.noul),
184
+ }));
185
+ const actionAnswer = answers.actionChoice;
186
+ const actionIndex = String(actionAnswer?.choice || '').match(/^action_(\d+)$/)?.[1];
187
+ return {
188
+ domRelevance: regionRelevance,
189
+ actionChoice: actionAnswer ? {
190
+ ...actionAnswer,
191
+ selectedControl: actionIndex ? catalog.controls?.[Number(actionIndex) - 1] ?? null : null,
192
+ } : null,
193
+ modelRoute: answers.modelRoute ?? null,
194
+ verification: answers.postconditionSatisfied ?? null,
195
+ recovery: answers.recoveryRoute ?? null,
196
+ missionStage: answers.missionStage ?? null,
197
+ prerequisite: answers.prerequisiteSatisfied ?? null,
198
+ };
199
+ }
200
+
201
+ export function evaluateJevCompletionCandidate(result, {
202
+ threshold = DEFAULT_GATE_THRESHOLD,
203
+ browserEvidence,
204
+ } = {}) {
205
+ const answers = result?.status === 'ok' ? result.answers : null;
206
+ const signals = {
207
+ missionComplete: Number(answers?.missionComplete?.noul),
208
+ progressComplete: Number(answers?.progressState?.probabilities?.complete),
209
+ finish: Number(answers?.nextCompute?.probabilities?.finish),
210
+ };
211
+ const hasBrowserEvidence = Boolean(
212
+ browserEvidence?.title
213
+ || browserEvidence?.headings?.length
214
+ || browserEvidence?.controls?.length,
215
+ );
216
+ const values = Object.values(signals);
217
+ return {
218
+ candidate: Boolean(
219
+ hasBrowserEvidence
220
+ && values.every(value => Number.isFinite(value) && value >= threshold),
221
+ ),
222
+ threshold,
223
+ hasBrowserEvidence,
224
+ signals,
225
+ };
226
+ }
227
+
228
+ export function buildJevShadowState({
229
+ mission,
230
+ turn,
231
+ maxTurns,
232
+ phase,
233
+ assistantSummary,
234
+ assistantContent,
235
+ proposedTools = [],
236
+ events = [],
237
+ groundControl,
238
+ browserEvidence,
239
+ } = {}) {
240
+ return {
241
+ mission: compactText(mission, 3000),
242
+ turn: { current: Number(turn) + 1, maximum: maxTurns },
243
+ phase,
244
+ assistant: {
245
+ summary: compactText(assistantSummary, 1000),
246
+ content: compactText(assistantContent, 2000),
247
+ proposedTools: proposedTools.slice(0, 12).map(tool => ({
248
+ name: compactText(tool?.name, 120),
249
+ arguments: tool?.arguments,
250
+ })),
251
+ },
252
+ recentEvents: events.slice(-12).map(event => compactText(event, 500)),
253
+ groundControl: groundControl ?? null,
254
+ browserEvidence: browserEvidence ?? null,
255
+ };
256
+ }
257
+
258
+ export function sanitizeJevShadowEvents(events = [], redactText = value => String(value ?? '')) {
259
+ return events
260
+ .filter(event => !String(event).startsWith('[tool ] ←') || String(event).includes(' result:'))
261
+ .map(event => redactText(event));
262
+ }
263
+
264
+ export async function evaluateJevShadow(state, {
265
+ config = resolveJevShadowConfig(),
266
+ questions = SHADOW_QUESTIONS,
267
+ fetchImpl = globalThis.fetch,
268
+ now = () => Date.now(),
269
+ } = {}) {
270
+ if (!config.enabled) return null;
271
+ if (!config.apiKey) {
272
+ return { status: 'skipped', reason: 'missing_api_key' };
273
+ }
274
+ if (typeof fetchImpl !== 'function') {
275
+ return { status: 'error', error: 'fetch_unavailable' };
276
+ }
277
+
278
+ const controller = new AbortController();
279
+ const timer = setTimeout(() => controller.abort(), config.timeoutMs);
280
+ const startedAt = now();
281
+
282
+ try {
283
+ const response = await fetchImpl(`${config.apiBase}/v1/systemone`, {
284
+ method: 'POST',
285
+ headers: {
286
+ authorization: `Bearer ${config.apiKey}`,
287
+ 'content-type': 'application/json',
288
+ },
289
+ body: JSON.stringify({
290
+ state,
291
+ model: config.model,
292
+ questions,
293
+ }),
294
+ signal: controller.signal,
295
+ });
296
+
297
+ const latencyMs = Math.max(0, now() - startedAt);
298
+ if (!response.ok) {
299
+ return {
300
+ status: 'error',
301
+ error: `http_${response.status}`,
302
+ latencyMs,
303
+ };
304
+ }
305
+
306
+ const payload = await response.json();
307
+ return {
308
+ status: 'ok',
309
+ model: payload.model,
310
+ answers: payload.answers,
311
+ usage: payload.usage,
312
+ latencyMs,
313
+ };
314
+ } catch (error) {
315
+ return {
316
+ status: 'error',
317
+ error: error?.name === 'AbortError' ? 'timeout' : compactText(error?.message || error, 300),
318
+ latencyMs: Math.max(0, now() - startedAt),
319
+ };
320
+ } finally {
321
+ clearTimeout(timer);
322
+ }
323
+ }
324
+
325
+ export const __jevShadowInternals = { SHADOW_QUESTIONS, compactText, truthy };
@@ -0,0 +1,39 @@
1
+ export function buildJevSuiteSelectionRequest({ changedFiles = [], missions = [] } = {}) {
2
+ const catalog = missions.map((mission, index) => ({
3
+ id: `mission_${index + 1}`,
4
+ file: mission.file,
5
+ tags: mission.tags || [],
6
+ }));
7
+ const questions = {};
8
+ for (const mission of catalog) {
9
+ questions[`select_${mission.id}`] = {
10
+ type: 'noul',
11
+ instructions: `Should the test mission ${mission.file} be selected for this code change?`,
12
+ criteria: {
13
+ true: 'The changed files could affect behavior covered by this mission, directly or through shared dependencies.',
14
+ false: 'The mission is unrelated to the changed files and can be omitted from this targeted run.',
15
+ },
16
+ };
17
+ }
18
+ return {
19
+ state: {
20
+ task: 'Select relevant end-to-end test missions for a targeted CI run. Prefer coverage when uncertain.',
21
+ changedFiles,
22
+ missions: catalog,
23
+ },
24
+ questions,
25
+ catalog,
26
+ };
27
+ }
28
+
29
+ export function interpretJevSuiteSelection(answers = {}, catalog = [], threshold = 0.7) {
30
+ return catalog.map(mission => {
31
+ const probability = Number(answers[`select_${mission.id}`]?.noul);
32
+ return {
33
+ file: mission.file,
34
+ tags: mission.tags,
35
+ probability,
36
+ selected: Number.isFinite(probability) && probability >= threshold,
37
+ };
38
+ });
39
+ }
@@ -0,0 +1,62 @@
1
+ export const DEFAULT_JEV_INPUT_USD_PER_MILLION = 0.042;
2
+
3
+ function nonNegativeNumber(value, fallback = 0) {
4
+ const number = Number(value);
5
+ return Number.isFinite(number) && number >= 0 ? number : fallback;
6
+ }
7
+
8
+ export function resolveJevInputPrice(env = process.env) {
9
+ return nonNegativeNumber(
10
+ env.TESTRONAUT_JEV_INPUT_USD_PER_MILLION,
11
+ DEFAULT_JEV_INPUT_USD_PER_MILLION,
12
+ );
13
+ }
14
+
15
+ export function summarizeJevUsage(steps = [], {
16
+ inputUsdPerMillion = resolveJevInputPrice(),
17
+ } = {}) {
18
+ const entries = (Array.isArray(steps) ? steps : [])
19
+ .map(step => step?.jevShadow)
20
+ .filter(Boolean);
21
+ const completed = entries.filter(entry => entry.status === 'ok');
22
+ const inputTokens = completed.reduce(
23
+ (sum, entry) => sum + nonNegativeNumber(entry.usage?.input_tokens),
24
+ 0,
25
+ );
26
+ const outputTokens = completed.reduce(
27
+ (sum, entry) => sum + nonNegativeNumber(entry.usage?.output_tokens),
28
+ 0,
29
+ );
30
+ const strategyEntries = completed.map(entry => entry.strategies).filter(Boolean);
31
+ const countChoices = (values) => values.reduce((counts, value) => {
32
+ if (value) counts[value] = (counts[value] || 0) + 1;
33
+ return counts;
34
+ }, {});
35
+ const regionDecisions = strategyEntries.flatMap(entry => entry.domRelevance || []);
36
+
37
+ return {
38
+ evaluations: entries.length,
39
+ completed: completed.length,
40
+ errors: entries.filter(entry => entry.status === 'error').length,
41
+ skipped: entries.filter(entry => entry.status === 'skipped').length,
42
+ inputTokens,
43
+ outputTokens,
44
+ latencyMs: completed.reduce((sum, entry) => sum + nonNegativeNumber(entry.latencyMs), 0),
45
+ completionCandidates: completed.filter(entry => entry.completionGate?.candidate).length,
46
+ triggeredCompletions: (Array.isArray(steps) ? steps : [])
47
+ .filter(step => step?.jevGate?.triggered).length,
48
+ inputUsdPerMillion,
49
+ estimatedInputCostUsd: inputTokens * inputUsdPerMillion / 1_000_000,
50
+ strategies: strategyEntries.length ? {
51
+ evaluations: strategyEntries.length,
52
+ modelRoutes: countChoices(strategyEntries.map(entry => entry.modelRoute?.choice)),
53
+ actionChoices: countChoices(strategyEntries.map(entry => entry.actionChoice?.choice)),
54
+ recoveryRoutes: countChoices(strategyEntries.map(entry => entry.recovery?.choice)),
55
+ missionStages: countChoices(strategyEntries.map(entry => entry.missionStage?.choice)),
56
+ verifiedPostconditions: strategyEntries.filter(entry => Number(entry.verification?.noul) >= 0.8).length,
57
+ satisfiedPrerequisites: strategyEntries.filter(entry => Number(entry.prerequisite?.noul) >= 0.8).length,
58
+ regionsEvaluated: regionDecisions.length,
59
+ relevantRegions: regionDecisions.filter(region => Number(region.probability) >= 0.5).length,
60
+ } : null,
61
+ };
62
+ }