testronaut 1.11.0 → 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 (32) hide show
  1. package/README.md +198 -0
  2. package/TODO.md +5 -0
  3. package/bin/cli.js +215 -2
  4. package/core/agent.js +1 -0
  5. package/core/browserEvidence.js +184 -0
  6. package/core/jevRouting.js +77 -0
  7. package/core/jevShadow.js +325 -0
  8. package/core/jevSuiteSelection.js +39 -0
  9. package/core/jevUsage.js +62 -0
  10. package/core/optimization.js +98 -0
  11. package/core/turnLoop.js +174 -10
  12. package/experiments/CROSS_PROVIDER.md +19 -0
  13. package/experiments/README.md +62 -0
  14. package/experiments/buildExperimentPublicationPackage.js +219 -0
  15. package/experiments/buildOptimizationDataPackage.js +390 -0
  16. package/experiments/crossProviderBenchmark.js +227 -0
  17. package/experiments/optimizationBenchmark.js +339 -0
  18. package/package.json +5 -1
  19. package/tests/binTests/cli.helpers.test.js +16 -0
  20. package/tests/binTests/cli.integration.test.js +14 -0
  21. package/tests/coreTests/browserEvidence.test.js +70 -0
  22. package/tests/coreTests/jevRouting.test.js +57 -0
  23. package/tests/coreTests/jevShadow.test.js +158 -0
  24. package/tests/coreTests/jevSuiteSelection.test.js +24 -0
  25. package/tests/coreTests/jevUsage.test.js +58 -0
  26. package/tests/coreTests/optimization.test.js +80 -0
  27. package/tests/coreTests/turnLoop.test.js +304 -0
  28. package/tests/experimentTests/buildOptimizationDataPackage.test.js +25 -0
  29. package/tests/experimentTests/crossProviderBenchmark.test.js +37 -0
  30. package/tests/experimentTests/optimizationBenchmark.test.js +49 -0
  31. package/tests/toolsTests/generateHtmlReport.test.js +80 -0
  32. package/tools/generateHtmlReport.js +57 -5
@@ -18,6 +18,7 @@
18
18
  import fs from 'fs';
19
19
  import path from 'path';
20
20
  import { normalizeTags } from '../core/tags.js';
21
+ import { summarizeJevUsage } from '../core/jevUsage.js';
21
22
 
22
23
  /**
23
24
  * Render and write a Testronaut run report to disk.
@@ -27,7 +28,7 @@ import { normalizeTags } from '../core/tags.js';
27
28
  * @returns {string} absolute path to the written HTML file
28
29
  */
29
30
  export function generateHtmlReport(report, outputPath) {
30
- const { runId, startTime, endTime, missions = [], summary = {}, llm = {}, cli = {} } = report;
31
+ const { runId, startTime, endTime, missions = [], summary = {}, llm = {}, cli = {}, optimization } = report;
31
32
  const reportTags = normalizeTags(report.tags ?? missions.flatMap(m => m.submissionType === 'mission' ? (m.tags ?? []) : []));
32
33
  const durationSec =
33
34
  (startTime && endTime)
@@ -47,6 +48,7 @@ export function generateHtmlReport(report, outputPath) {
47
48
  : String(llm.provider ?? '').trim().toLowerCase();
48
49
  const provider = {
49
50
  openai: { name: 'OpenAI', logo: '<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M9.205 8.658v-2.26c0-.19.072-.333.238-.428l4.543-2.616c.619-.357 1.356-.523 2.117-.523 2.854 0 4.662 2.212 4.662 4.566 0 .167 0 .357-.024.547l-4.71-2.759a.797.797 0 00-.856 0l-5.97 3.473zm10.609 8.8V12.06c0-.333-.143-.57-.429-.737l-5.97-3.473 1.95-1.118a.433.433 0 01.476 0l4.543 2.617c1.309.76 2.189 2.378 2.189 3.948 0 1.808-1.07 3.473-2.76 4.163zM7.802 12.703l-1.95-1.142c-.167-.095-.239-.238-.239-.428V5.899c0-2.545 1.95-4.472 4.591-4.472 1 0 1.927.333 2.712.928L8.23 5.067c-.285.166-.428.404-.428.737v6.898zM12 15.128l-2.795-1.57v-3.33L12 8.658l2.795 1.57v3.33L12 15.128zm1.796 7.23c-1 0-1.927-.332-2.712-.927l4.686-2.712c.285-.166.428-.404.428-.737v-6.898l1.974 1.142c.167.095.238.238.238.428v5.233c0 2.545-1.974 4.472-4.614 4.472zm-5.637-5.303l-4.544-2.617c-1.308-.761-2.188-2.378-2.188-3.948A4.482 4.482 0 014.21 6.327v5.423c0 .333.143.571.428.738l5.947 3.449-1.95 1.118a.432.432 0 01-.476 0zm-.262 3.9c-2.688 0-4.662-2.021-4.662-4.519 0-.19.024-.38.047-.57l4.686 2.71c.286.167.571.167.856 0l5.97-3.448v2.26c0 .19-.07.333-.237.428l-4.543 2.616c-.619.357-1.356.523-2.117.523zm5.899 2.83a5.947 5.947 0 005.827-4.756C22.287 18.339 24 15.84 24 13.296c0-1.665-.713-3.282-1.998-4.448.119-.5.19-.999.19-1.498 0-3.401-2.759-5.947-5.946-5.947-.642 0-1.26.095-1.88.31A5.962 5.962 0 0010.205 0a5.947 5.947 0 00-5.827 4.757C1.713 5.447 0 7.945 0 10.49c0 1.666.713 3.283 1.998 4.448-.119.5-.19 1-.19 1.499 0 3.401 2.759 5.946 5.946 5.946.642 0 1.26-.095 1.88-.309a5.96 5.96 0 004.162 1.713z"/></svg>' },
51
+ gemini: { name: 'Google Gemini', logo: '<svg viewBox="0 0 24 24" aria-hidden="true"><defs><linearGradient id="gemini-gradient" x1="4" y1="20" x2="20" y2="4"><stop stop-color="#3186ff"/><stop offset=".5" stop-color="#8e68ff"/><stop offset="1" stop-color="#ef78b5"/></linearGradient></defs><path fill="url(#gemini-gradient)" d="M12 2c.7 5.8 4.2 9.3 10 10-5.8.7-9.3 4.2-10 10-.7-5.8-4.2-9.3-10-10 5.8-.7 9.3-4.2 10-10z"/></svg>' },
50
52
  anthropic: { name: 'Anthropic', logo: '<img src="data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHZpZXdCb3g9IjAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8dGl0bGU+Q2xhdWRlPC90aXRsZT4KICA8cmVjdCB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHJ4PSI1IiBmaWxsPSIjRDk3NzU3Ii8+CiAgPHBhdGggZmlsbD0iI2ZmZiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMyAzKSBzY2FsZSguNzUpIiBkPSJNNC43MDkgMTUuOTU1bDQuNzItMi42NDcuMDgtLjIzLS4wOC0uMTI4SDkuMmwtLjc5LS4wNDgtMi42OTgtLjA3My0yLjMzOS0uMDk3LTIuMjY2LS4xMjItLjU3MS0uMTIxTDAgMTEuNzg0bC4wNTUtLjM1Mi40OC0uMzIxLjY4Ni4wNiAxLjUyLjEwMyAyLjI3OC4xNTggMS42NTIuMDk3IDIuNDQ5LjI1NWguMzg5bC4wNTUtLjE1Ny0uMTM0LS4wOTgtLjEwMy0uMDk3LTIuMzU4LTEuNTk2LTIuNTUyLTEuNjg4LTEuMzM2LS45NzItLjcyNC0uNDkxLS4zNjQtLjQ2Mi0uMTU4LTEuMDA4LjY1Ni0uNzIyLjg4MS4wNi4yMjUuMDYxLjg5My42ODYgMS45MDggMS40NzYgMi40OTEgMS44MzMuMzY1LjMwNC4xNDUtLjEwMy4wMTktLjA3My0uMTY0LS4yNzQtMS4zNTUtMi40NDYtMS40NDYtMi40OS0uNjQ0LTEuMDMyLS4xNy0uNjE5YTIuOTcgMi45NyAwIDAxLS4xMDQtLjcyOUw2LjI4My4xMzQgNi42OTYgMGwuOTk2LjEzNC40Mi4zNjQuNjIgMS40MTQgMS4wMDIgMi4yMjkgMS41NTUgMy4wMy40NTYuODk4LjI0My44MzIuMDkxLjI1NWguMTU4VjkuMDFsLjEyOC0xLjcwNi4yMzctMi4wOTUuMjMtMi42OTUuMDgtLjc2LjM3Ni0uOTEuNzQ3LS40OTIuNTg0LjI4LjQ4LjY4NS0uMDY3LjQ0NC0uMjg2IDEuODUxLS41NTkgMi45MDMtLjM2NCAxLjk0MmguMjEybC4yNDMtLjI0Mi45ODUtMS4zMDYgMS42NTItMi4wNjQuNzMtLjgyLjg1LS45MDQuNTQ3LS40MzFoMS4wMzNsLjc2IDEuMTI5LS4zNCAxLjE2Ni0xLjA2NCAxLjM0Ny0uODgxIDEuMTQyLTEuMjY0IDEuNy0uNzkgMS4zNi4wNzMuMTEuMTg4LS4wMiAyLjg1Ni0uNjA2IDEuNTQzLS4yOCAxLjg0MS0uMzE1LjgzMy4zODguMDkxLjM5NS0uMzI4LjgwNy0xLjk2OS40ODYtMi4zMDkuNDYyLTMuNDM5LjgxMy0uMDQyLjAzLjA0OS4wNjEgMS41NDkuMTQ2LjY2Mi4wMzZoMS42MjJsMy4wMi4yMjUuNzkuNTIyLjQ3NC42MzgtLjA3OS40ODUtMS4yMTUuNjItMS42NC0uMzg5LTMuODI5LS45MS0xLjMxMi0uMzI5aC0uMTgydi4xMWwxLjA5MyAxLjA2OCAyLjAwNiAxLjgxIDIuNTA5IDIuMzMuMTI3LjU3OC0uMzIyLjQ1NS0uMzQtLjA0OS0yLjIwNS0xLjY1Ny0uODUxLS43NDctMS45MjYtMS42MmgtLjEyOHYuMTdsLjQ0NC42NDkgMi4zNDUgMy41MjEuMTIyIDEuMDgtLjE3LjM1My0uNjA4LjIxMy0uNjY4LS4xMjItMS4zNzQtMS45MjUtMS40MTUtMi4xNjctMS4xNDMtMS45NDMtLjE0LjA4LS42NzQgNy4yNTQtLjMxNi4zNy0uNzI5LjI4LS42MDctLjQ2MS0uMzIyLS43NDcuMzIyLTEuNDc2LjM4OS0xLjkyNC4zMTUtMS41My4yODYtMS45LjE3LS42MzItLjAxMi0uMDQyLS4xNC4wMTgtMS40MzQgMS45NjctMi4xOCAyLjk0NS0xLjcyNiAxLjg0NS0uNDE0LjE2NC0uNzE3LS4zNy4wNjctLjY2Mi40MDEtLjU4OSAyLjM4OC0zLjAzNiAxLjQ0LTEuODgyLjkzLTEuMDg2LS4wMDYtLjE1OGgtLjA1NUw0LjEzMiAxOC41NmwtMS4xMy4xNDYtLjQ4Ny0uNDU2LjA2MS0uNzQ2LjIzMS0uMjQzIDEuOTA4LTEuMzEyLS4wMDYuMDA2eiIvPgo8L3N2Zz4K" alt="" />' },
51
53
  }[providerKey] ?? { name: llm.provider || 'Unknown provider', logo: '' };
52
54
  const tagPalette = [
@@ -59,6 +61,34 @@ export function generateHtmlReport(report, outputPath) {
59
61
  const [color, background] = tagPalette[index];
60
62
  return `color:${color};background:${background};border-color:${color}66`;
61
63
  };
64
+ const formatTokens = value => Number(value || 0).toLocaleString('en-US');
65
+ const summarizeModelTokens = (steps) => steps.reduce((usage, step) => {
66
+ const input = Number(step.inputTokens || 0);
67
+ const output = Number(step.outputTokens || 0);
68
+ const model = String(step.model || llm.model || 'Model');
69
+ usage.input += input;
70
+ usage.output += output;
71
+ usage.total += Number(step.tokensUsed ?? (input + output));
72
+ usage.byModel[model] ||= { input: 0, output: 0, total: 0, turns: 0 };
73
+ usage.byModel[model].input += input;
74
+ usage.byModel[model].output += output;
75
+ usage.byModel[model].total += Number(step.tokensUsed ?? (input + output));
76
+ usage.byModel[model].turns += 1;
77
+ return usage;
78
+ }, { input: 0, output: 0, total: 0, byModel: {} });
79
+ const tokenMeta = (modelUsage, jevUsage) => [
80
+ `Model tokens: ${formatTokens(modelUsage.total)} (${formatTokens(modelUsage.input)} in / ${formatTokens(modelUsage.output)} out)`,
81
+ Object.keys(modelUsage.byModel).length > 1
82
+ ? `By model: ${Object.entries(modelUsage.byModel).map(([model, usage]) => `${model} ${formatTokens(usage.total)}`).join(' / ')}`
83
+ : '',
84
+ jevUsage?.completed ? `Jev tokens: ${formatTokens(jevUsage.inputTokens)} in / ${formatTokens(jevUsage.outputTokens)} out` : '',
85
+ ].filter(Boolean).join(' • ');
86
+ const reportJevUsage = report.jevShadow?.evaluations
87
+ ? report.jevShadow
88
+ : summarizeJevUsage(missions.flatMap(m => Array.isArray(m.steps) ? m.steps : []));
89
+ const reportModelUsage = summarizeModelTokens(missions.flatMap(m => Array.isArray(m.steps) ? m.steps : []));
90
+ const reportModels = Object.keys(reportModelUsage.byModel);
91
+ const jevLogo = '<svg viewBox="0 0 24 24" role="img" aria-hidden="true"><rect width="24" height="24" rx="4" fill="#e547b5"/><g fill="none" stroke="#181a1b" stroke-width="2.25" stroke-linecap="square" stroke-linejoin="round"><path d="M12 2.5 6.1 6v10.1l3.2 1.9 2.7-1.6V9.6l3-1.8V4.3z"/><path d="m9.3 9.6 2.7 1.6 3-1.8 3 1.8v7L12 21.5l-3-1.8V13z"/><path d="m12 11.2 3 1.8 3-1.8M15 13v5.2l-6 3.3M9.3 9.6 12 8l3 1.8"/></g></svg>';
62
92
 
63
93
  const submissionBlock = (m) => {
64
94
  const mDurationSec =
@@ -67,6 +97,8 @@ export function generateHtmlReport(report, outputPath) {
67
97
  : (typeof m.durationSeconds === 'number' ? m.durationSeconds.toFixed(2) : '—');
68
98
 
69
99
  const steps = Array.isArray(m.steps) ? m.steps : [];
100
+ const missionJevUsage = m.jevShadow?.evaluations ? m.jevShadow : summarizeJevUsage(steps);
101
+ const missionModelUsage = summarizeModelTokens(steps);
70
102
  const stepItems = steps.map((step, idx) => {
71
103
  const events = Array.isArray(step.events) ? step.events : [];
72
104
  const ok = /✅|Passed|Mission Success/i.test(step.result || '');
@@ -87,6 +119,16 @@ export function generateHtmlReport(report, outputPath) {
87
119
  const imgTag = step.screenshotPath
88
120
  ? `<img src="${esc(step.screenshotPath)}" alt="screenshot turn ${esc(step.turn ?? idx)}">`
89
121
  : '';
122
+ const stepJev = step.jevShadow?.status === 'ok'
123
+ ? ` • Jev: ${formatTokens(step.jevShadow.usage?.input_tokens)} in / ${formatTokens(step.jevShadow.usage?.output_tokens)} out`
124
+ : step.jevShadow
125
+ ? ` • Jev: ${esc(step.jevShadow.status)}${step.jevShadow.reason ? ` (${esc(step.jevShadow.reason)})` : ''}`
126
+ : '';
127
+ const routeLabel = step.modelRouting?.fallback
128
+ ? ' • Jev route fallback'
129
+ : step.modelRouting?.applied
130
+ ? ' • routed by Jev'
131
+ : '';
90
132
 
91
133
  const plan = (typeof step.summary === 'string' && step.summary.trim())
92
134
  ? step.summary.trim()
@@ -102,7 +144,7 @@ export function generateHtmlReport(report, outputPath) {
102
144
  ${humanInput}
103
145
  ${planSpan}
104
146
  <span class="step-result ${ok ? 'ok' : 'bad'}" ${resultTooltip ? `title="${esc(resultTooltip)}"` : ''}>${esc(resultRaw)}</span>
105
- <span class="tokens">tokens: ${esc(step.tokensUsed ?? '—')} (in: ${esc(step.inputTokens ?? '—')}, out: ${esc(step.outputTokens ?? '—')}) / rolling: ${esc(step.totalTokensUsed ?? '—')}</span>
147
+ <span class="tokens">${esc(step.model || llm.model || 'Model')} tokens: ${esc(step.tokensUsed ?? '—')} (${esc(step.inputTokens ?? '—')} in / ${esc(step.outputTokens ?? '—')} out)${routeLabel}${stepJev}</span>
106
148
  </summary>
107
149
  <pre class="events">${esc(events.join('\n')) || '(no events)'}</pre>
108
150
  ${imgTag}
@@ -120,7 +162,7 @@ export function generateHtmlReport(report, outputPath) {
120
162
  <summary>
121
163
  <span class="name">${prettyTitle}</span>
122
164
  <span class="status ${statusClass}">${badge(m.status)}</span>
123
- <span class="meta">steps: ${steps.length} • duration: ${mDurationSec}s</span>
165
+ <span class="meta">steps: ${steps.length} • duration: ${mDurationSec}s • ${esc(tokenMeta(missionModelUsage, missionJevUsage))}</span>
124
166
  <span class="toolbar">
125
167
  <button class="btn-mini toggle" data-scope="submission" aria-label="Expand">▼</button>
126
168
  </span>
@@ -151,6 +193,8 @@ export function generateHtmlReport(report, outputPath) {
151
193
  const groupDur = (firstStart != null && lastEnd != null) ? ((lastEnd - firstStart) / 1000).toFixed(2) : '—';
152
194
  const tags = normalizeTags(subs.flatMap(s => s.submissionType === 'mission' ? (s.tags ?? []) : []));
153
195
  const sourceFiles = [...new Set(subs.map(s => s.file).filter(Boolean))];
196
+ const groupJevUsage = summarizeJevUsage(subs.flatMap(s => Array.isArray(s.steps) ? s.steps : []));
197
+ const groupModelUsage = summarizeModelTokens(subs.flatMap(s => Array.isArray(s.steps) ? s.steps : []));
154
198
 
155
199
  // pre → mission → post
156
200
  const order = { premission: 0, mission: 1, postmission: 2 };
@@ -165,7 +209,7 @@ export function generateHtmlReport(report, outputPath) {
165
209
  <span class="name">${esc(missionName)}</span>
166
210
  <span class="mission-tags">${tags.map(tag => `<span class="tag-small" style="${tagStyle(tag)}">${esc(tag)}</span>`).join('')}</span>
167
211
  <span class="status ${status === 'failed' ? 'bad' : 'ok'}">${badge(status)}</span>
168
- <span class="meta">${sourceFiles.length ? `${sourceFiles.map(esc).join(', ')} • ` : ''}submissions: ${subs.length} • steps: ${totalSteps} • duration: ${groupDur}s</span>
212
+ <span class="meta">${sourceFiles.length ? `${sourceFiles.map(esc).join(', ')} • ` : ''}submissions: ${subs.length} • steps: ${totalSteps} • duration: ${groupDur}s • ${esc(tokenMeta(groupModelUsage, groupJevUsage))}</span>
169
213
  <span class="toolbar">
170
214
  <button class="btn-mini toggle" data-scope="mission" aria-label="Expand">▼</button>
171
215
  </span>
@@ -277,6 +321,7 @@ export function generateHtmlReport(report, outputPath) {
277
321
  .pill:first-child{border-color:rgba(255,138,67,.5);box-shadow:inset 3px 0 var(--orange);}
278
322
  .pill.ok{ background: var(--chip-ok-bg); color: var(--ok); border-color: var(--chip-ok-border); font-weight:700; }
279
323
  .pill.bad{ background: var(--chip-bad-bg); color: var(--bad); border-color: var(--chip-bad-border); font-weight:700; }
324
+ .pill.warn{ background:rgba(251,191,36,.14);color:#fbbf24;border-color:rgba(251,191,36,.4);font-weight:700; }
280
325
  .tag-filter{max-width:928px;margin:0 auto 20px;padding:18px 16px;background:rgba(2,6,23,.30);}
281
326
  .tag-filter-heading{display:flex;align-items:center;gap:7px;margin-bottom:12px;flex-wrap:wrap;}
282
327
  .tag-filter-title{font-size:14px;color:var(--text);font-weight:800;}
@@ -377,7 +422,7 @@ export function generateHtmlReport(report, outputPath) {
377
422
  <div class="eyebrow">Mission Control · Flight Report</div>
378
423
  <h1>🧑‍🚀 <span class="gradient-text">Testronaut Report</span></h1>
379
424
  <div class="run-meta">
380
- <div class="provider-meta"><span class="provider-logo" title="${esc(provider.name)}" aria-label="${esc(provider.name)}">${provider.logo}</span><strong>${esc(llm.model ?? '—')}</strong></div>
425
+ <div class="provider-meta"><span class="provider-logo" title="${esc(provider.name)}" aria-label="${esc(provider.name)}">${provider.logo}</span><strong>${esc((reportModels.length ? reportModels : [llm.model ?? '—']).join(' + '))}</strong>${reportJevUsage.evaluations ? ` <span aria-hidden="true">+</span> <span class="provider-logo" title="Jev" aria-label="Jev">${jevLogo}</span><strong>Jev</strong>` : ''}</div>
381
426
  <div><strong>Run ID:</strong> ${esc(runId ?? '—')}</div>
382
427
  <div><strong>Start:</strong> ${esc(startTime ?? '—')}</div>
383
428
  <div><strong>End:</strong> ${esc(endTime ?? '—')}</div>
@@ -390,6 +435,13 @@ export function generateHtmlReport(report, outputPath) {
390
435
  <div class="pill">Missions: ${esc(summary.totalMissions ?? totals.total)}</div>
391
436
  <div class="pill ok">Passed: ${esc(summary.passed ?? totals.passed)}</div>
392
437
  <div class="pill bad">Failed: ${esc(summary.failed ?? totals.failed)}</div>
438
+ ${optimization?.configured ? `<div class="pill">Optimization: ${esc(optimization.objective)} (${esc(optimization.mode)})</div>` : ''}
439
+ ${Object.entries(reportModelUsage.byModel).length > 1
440
+ ? Object.entries(reportModelUsage.byModel).map(([model, usage]) => `<div class="pill">${esc(model)} tokens: ${esc(formatTokens(usage.total))} (${esc(formatTokens(usage.input))} in / ${esc(formatTokens(usage.output))} out)</div>`).join('')
441
+ : `<div class="pill">Model tokens: ${esc(formatTokens(reportModelUsage.total))} (${esc(formatTokens(reportModelUsage.input))} in / ${esc(formatTokens(reportModelUsage.output))} out)</div>`}
442
+ ${reportJevUsage.completed ? `<div class="pill">Jev tokens: ${esc(formatTokens(reportJevUsage.inputTokens))} in / ${esc(formatTokens(reportJevUsage.outputTokens))} out</div>` : ''}
443
+ ${reportJevUsage.skipped ? `<div class="pill warn">Jev skipped: ${esc(formatTokens(reportJevUsage.skipped))}</div>` : ''}
444
+ ${reportJevUsage.errors ? `<div class="pill warn">Jev errors: ${esc(formatTokens(reportJevUsage.errors))}</div>` : ''}
393
445
  </div>
394
446
 
395
447
  <div class="tag-filter glass">