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,70 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { buildBrowserEvidence, mergeBrowserControlState } from '../../core/browserEvidence.js';
3
+
4
+ describe('browser evidence', () => {
5
+ it('extracts bounded structure without field values or URL secrets', () => {
6
+ const evidence = buildBrowserEvidence(`
7
+ <html><head><title>Mission Dashboard</title></head><body>
8
+ <main><h1>Mission Tasks</h1><form><label for="access">Access code</label>
9
+ <input id="access" name="access" type="password" value="super-secret" />
10
+ <button>Sign in</button>
11
+ </form></main>
12
+ <a href="https://example.test/tasks?token=secret#private">Tasks</a>
13
+ <p>private body content</p>
14
+ </body></html>
15
+ `, { url: 'https://example.test/dashboard?token=secret#private' });
16
+
17
+ expect(evidence).toMatchObject({
18
+ url: 'https://example.test/dashboard',
19
+ title: 'Mission Dashboard',
20
+ headings: ['Mission Tasks'],
21
+ counts: { headings: 1, forms: 1, buttons: 1, links: 1, inputs: 1 },
22
+ });
23
+ expect(JSON.stringify(evidence)).not.toContain('super-secret');
24
+ expect(JSON.stringify(evidence)).not.toContain('private body content');
25
+ expect(JSON.stringify(evidence)).not.toContain('token=secret');
26
+ expect(evidence.regions).toEqual(expect.arrayContaining([
27
+ expect.objectContaining({ kind: 'main', headings: ['Mission Tasks'] }),
28
+ expect.objectContaining({ kind: 'form', controls: ['Access code', 'Sign in'] }),
29
+ ]));
30
+ });
31
+
32
+ it('tracks route changes and clears stale page structure', () => {
33
+ const previous = buildBrowserEvidence('<title>Login</title><h1>Sign in</h1>', {
34
+ url: 'https://example.test/login',
35
+ });
36
+ const evidence = buildBrowserEvidence(null, {
37
+ url: 'https://example.test/dashboard',
38
+ previous,
39
+ });
40
+
41
+ expect(evidence).toEqual({
42
+ url: 'https://example.test/dashboard',
43
+ previousUrl: 'https://example.test/login',
44
+ routeChanged: true,
45
+ });
46
+ });
47
+
48
+ it('merges only boolean live state without exposing field contents', () => {
49
+ const evidence = { controls: [{ tag: 'input', id: 'password' }] };
50
+ const merged = mergeBrowserControlState(evidence, [{
51
+ hasValue: true,
52
+ checked: false,
53
+ disabled: false,
54
+ readOnly: true,
55
+ visible: true,
56
+ value: 'must-not-copy',
57
+ }]);
58
+
59
+ expect(merged.controls[0]).toEqual({
60
+ tag: 'input',
61
+ id: 'password',
62
+ hasValue: true,
63
+ checked: false,
64
+ disabled: false,
65
+ readOnly: true,
66
+ visible: true,
67
+ });
68
+ expect(JSON.stringify(merged)).not.toContain('must-not-copy');
69
+ });
70
+ });
@@ -0,0 +1,57 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { chooseJevTurnModel, resolveJevRoutingConfig } from '../../core/jevRouting.js';
3
+
4
+ const safeFastRoute = {
5
+ modelRoute: { choice: 'fast_model', confidence: 0.22, probabilities: { fast_model: 0.52 } },
6
+ missionStage: { choice: 'interact' },
7
+ recovery: { choice: 'continue' },
8
+ };
9
+
10
+ describe('Jev dynamic routing', () => {
11
+ it('defaults OpenAI experiments to Luna without enabling routing', () => {
12
+ expect(resolveJevRoutingConfig({}, {
13
+ provider: 'openai', primaryModel: 'gpt-5.6-terra',
14
+ })).toMatchObject({ mode: 'off', fastModel: 'gpt-5.6-luna', confidenceThreshold: 0.4 });
15
+ });
16
+
17
+ it.each([
18
+ ['gemini', 'gemini-3.8-flash', 'gemini-3.5-flash-lite'],
19
+ ['anthropic', 'claude-sonnet-5', 'claude-haiku-4-5'],
20
+ ])('keeps the default fast model within the %s family', (provider, primaryModel, fastModel) => {
21
+ expect(resolveJevRoutingConfig({}, { provider, primaryModel })).toMatchObject({ fastModel });
22
+ });
23
+
24
+ it('records an eligible route without applying it in shadow mode', () => {
25
+ const result = chooseJevTurnModel(safeFastRoute, {
26
+ mode: 'shadow', fastModel: 'gpt-5.6-luna', confidenceThreshold: 0.4,
27
+ }, { provider: 'openai', primaryModel: 'gpt-5.6-terra' });
28
+ expect(result).toMatchObject({ eligible: true, applied: false, selectedModel: 'gpt-5.6-terra', reason: 'shadow_only' });
29
+ });
30
+
31
+ it('applies a high-confidence fast route for one live turn', () => {
32
+ const result = chooseJevTurnModel(safeFastRoute, {
33
+ mode: 'live', fastModel: 'gpt-5.6-luna', confidenceThreshold: 0.4,
34
+ }, { provider: 'openai', primaryModel: 'gpt-5.6-terra' });
35
+ expect(result).toMatchObject({ eligible: true, applied: true, selectedModel: 'gpt-5.6-luna', reason: 'jev_fast_model', score: 0.52, confidence: 0.22 });
36
+ });
37
+
38
+ it.each([
39
+ [{ ...safeFastRoute, missionStage: { choice: 'authenticate' } }, 'stage_authenticate'],
40
+ [{ ...safeFastRoute, recovery: { choice: 'reobserve' } }, 'recovery_reobserve'],
41
+ [{ ...safeFastRoute, modelRoute: { choice: 'fast_model', confidence: 0.5, probabilities: { fast_model: 0.39 } } }, 'low_probability'],
42
+ ])('keeps sensitive or uncertain turns on the primary model', (strategy, reason) => {
43
+ expect(chooseJevTurnModel(strategy, {
44
+ mode: 'live', fastModel: 'gpt-5.6-luna', confidenceThreshold: 0.4,
45
+ }, { provider: 'openai', primaryModel: 'gpt-5.6-terra' })).toMatchObject({ applied: false, reason });
46
+ });
47
+
48
+ it('rejects a configured model from another provider family', () => {
49
+ expect(chooseJevTurnModel(safeFastRoute, {
50
+ mode: 'live', fastModel: 'gpt-5.6-luna', confidenceThreshold: 0.4,
51
+ }, { provider: 'gemini', primaryModel: 'gemini-3.8-flash' })).toMatchObject({
52
+ applied: false,
53
+ reason: 'fast_model_provider_mismatch',
54
+ selectedModel: 'gemini-3.8-flash',
55
+ });
56
+ });
57
+ });
@@ -0,0 +1,158 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import {
3
+ buildJevShadowState,
4
+ buildJevStrategyQuestions,
5
+ evaluateJevCompletionCandidate,
6
+ evaluateJevShadow,
7
+ resolveJevShadowConfig,
8
+ sanitizeJevShadowEvents,
9
+ extractJevStrategyResults,
10
+ } from '../../core/jevShadow.js';
11
+
12
+ describe('Jev shadow evaluation', () => {
13
+ it('is disabled by default and resolves supported environment overrides', () => {
14
+ expect(resolveJevShadowConfig({}).enabled).toBe(false);
15
+ expect(resolveJevShadowConfig({
16
+ TESTRONAUT_JEV_GATE: 'true',
17
+ TYPESAFE_API_KEY: 'secret',
18
+ TESTRONAUT_JEV_MODEL: 'jev-test',
19
+ TESTRONAUT_JEV_TIMEOUT_MS: '1234',
20
+ TESTRONAUT_JEV_GATE_THRESHOLD: '0.85',
21
+ })).toMatchObject({
22
+ enabled: true,
23
+ gateEnabled: true,
24
+ gateThreshold: 0.85,
25
+ apiKey: 'secret',
26
+ model: 'jev-test',
27
+ timeoutMs: 1234,
28
+ });
29
+ });
30
+
31
+ it('requires all completion signals and browser evidence to clear the gate', () => {
32
+ const result = {
33
+ status: 'ok',
34
+ answers: {
35
+ missionComplete: { noul: 0.82 },
36
+ progressState: { probabilities: { complete: 0.91 } },
37
+ nextCompute: { probabilities: { finish: 0.88 } },
38
+ },
39
+ };
40
+
41
+ expect(evaluateJevCompletionCandidate(result, {
42
+ threshold: 0.8,
43
+ browserEvidence: { headings: ['Dashboard'] },
44
+ }).candidate).toBe(true);
45
+ expect(evaluateJevCompletionCandidate(result, {
46
+ threshold: 0.9,
47
+ browserEvidence: { headings: ['Dashboard'] },
48
+ }).candidate).toBe(false);
49
+ expect(evaluateJevCompletionCandidate(result, {
50
+ threshold: 0.8,
51
+ browserEvidence: null,
52
+ }).candidate).toBe(false);
53
+ });
54
+
55
+ it('builds and maps the optional strategy questions', () => {
56
+ const evidence = {
57
+ regions: [{ id: 'region_1', kind: 'main', headings: ['Dashboard'] }],
58
+ controls: [{ tag: 'button', id: 'save', label: 'Save' }],
59
+ };
60
+ const catalog = buildJevStrategyQuestions(evidence);
61
+
62
+ expect(catalog.questions).toMatchObject({
63
+ modelRoute: { type: 'choice' },
64
+ postconditionSatisfied: { type: 'noul' },
65
+ recoveryRoute: { type: 'choice' },
66
+ missionStage: { type: 'choice' },
67
+ prerequisiteSatisfied: { type: 'noul' },
68
+ regionRelevant_region_1: { type: 'noul' },
69
+ actionChoice: { type: 'choice' },
70
+ });
71
+
72
+ const result = extractJevStrategyResults({
73
+ regionRelevant_region_1: { type: 'noul', noul: 0.9 },
74
+ actionChoice: { type: 'choice', choice: 'action_1', confidence: 0.8 },
75
+ modelRoute: { type: 'choice', choice: 'deterministic' },
76
+ }, catalog);
77
+ expect(result.domRelevance[0].probability).toBe(0.9);
78
+ expect(result.actionChoice.selectedControl).toEqual(evidence.controls[0]);
79
+ expect(result.modelRoute.choice).toBe('deterministic');
80
+ });
81
+
82
+ it('builds a bounded state for the decision request', () => {
83
+ const state = buildJevShadowState({
84
+ mission: 'visit the dashboard',
85
+ turn: 1,
86
+ maxTurns: 10,
87
+ phase: 'after_action',
88
+ assistantContent: 'x'.repeat(3000),
89
+ proposedTools: [{ name: 'click_text', arguments: { text: 'Continue' } }],
90
+ events: Array.from({ length: 20 }, (_, index) => `event ${index}`),
91
+ });
92
+
93
+ expect(state.turn).toEqual({ current: 2, maximum: 10 });
94
+ expect(state.assistant.content).toContain('[truncated');
95
+ expect(state.recentEvents).toHaveLength(12);
96
+ });
97
+
98
+ it('excludes raw tool payloads while retaining tool status telemetry', () => {
99
+ expect(sanitizeJevShadowEvents([
100
+ '[tool ] ← get_dom result: OK',
101
+ '[tool ] ← <html>private page text</html>',
102
+ 'Navigation completed',
103
+ ])).toEqual([
104
+ '[tool ] ← get_dom result: OK',
105
+ 'Navigation completed',
106
+ ]);
107
+ });
108
+
109
+ it('returns API answers and usage without exposing the credential', async () => {
110
+ const fetchImpl = vi.fn(async (url, options) => ({
111
+ ok: true,
112
+ json: async () => ({
113
+ model: 'jev-resolved',
114
+ answers: { missionComplete: { type: 'noul', noul: 0.8 } },
115
+ usage: { input_tokens: 100, output_tokens: 3 },
116
+ }),
117
+ }));
118
+ const times = [100, 125];
119
+
120
+ const result = await evaluateJevShadow({ mission: 'test' }, {
121
+ config: {
122
+ enabled: true,
123
+ apiKey: 'top-secret',
124
+ apiBase: 'https://jev.example',
125
+ model: 'jev-latest',
126
+ timeoutMs: 1000,
127
+ },
128
+ fetchImpl,
129
+ now: () => times.shift(),
130
+ });
131
+
132
+ expect(result).toMatchObject({ status: 'ok', model: 'jev-resolved', latencyMs: 25 });
133
+ const [, request] = fetchImpl.mock.calls[0];
134
+ expect(request.headers.authorization).toBe('Bearer top-secret');
135
+ expect(JSON.stringify(result)).not.toContain('top-secret');
136
+ });
137
+
138
+ it('turns API failures into shadow telemetry', async () => {
139
+ const result = await evaluateJevShadow({}, {
140
+ config: {
141
+ enabled: true,
142
+ apiKey: 'secret',
143
+ apiBase: 'https://jev.example',
144
+ model: 'jev-latest',
145
+ timeoutMs: 1000,
146
+ },
147
+ fetchImpl: async () => ({ ok: false, status: 429 }),
148
+ });
149
+
150
+ expect(result).toMatchObject({ status: 'error', error: 'http_429' });
151
+ });
152
+
153
+ it('skips safely when enabled without a key', async () => {
154
+ await expect(evaluateJevShadow({}, {
155
+ config: { enabled: true, apiKey: '' },
156
+ })).resolves.toEqual({ status: 'skipped', reason: 'missing_api_key' });
157
+ });
158
+ });
@@ -0,0 +1,24 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { buildJevSuiteSelectionRequest, interpretJevSuiteSelection } from '../../core/jevSuiteSelection.js';
3
+
4
+ describe('Jev suite selection', () => {
5
+ it('creates one bounded decision per mission and applies a threshold', () => {
6
+ const request = buildJevSuiteSelectionRequest({
7
+ changedFiles: ['src/auth/login.js'],
8
+ missions: [
9
+ { file: 'login.mission.js', tags: ['auth'] },
10
+ { file: 'billing.mission.js', tags: ['billing'] },
11
+ ],
12
+ });
13
+ expect(Object.keys(request.questions)).toEqual(['select_mission_1', 'select_mission_2']);
14
+ expect(request.state.changedFiles).toEqual(['src/auth/login.js']);
15
+
16
+ expect(interpretJevSuiteSelection({
17
+ select_mission_1: { noul: 0.95 },
18
+ select_mission_2: { noul: 0.2 },
19
+ }, request.catalog, 0.7)).toEqual([
20
+ { file: 'login.mission.js', tags: ['auth'], probability: 0.95, selected: true },
21
+ { file: 'billing.mission.js', tags: ['billing'], probability: 0.2, selected: false },
22
+ ]);
23
+ });
24
+ });
@@ -0,0 +1,58 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { resolveJevInputPrice, summarizeJevUsage } from '../../core/jevUsage.js';
3
+
4
+ describe('Jev usage summaries', () => {
5
+ it('aggregates usage, outcomes, gates, latency, and estimated spend', () => {
6
+ const summary = summarizeJevUsage([
7
+ {
8
+ jevShadow: {
9
+ status: 'ok',
10
+ usage: { input_tokens: 1000, output_tokens: 25 },
11
+ latencyMs: 120,
12
+ completionGate: { candidate: true },
13
+ strategies: {
14
+ modelRoute: { choice: 'deterministic' },
15
+ actionChoice: { choice: 'action_1' },
16
+ recovery: { choice: 'continue' },
17
+ missionStage: { choice: 'interact' },
18
+ verification: { noul: 0.9 },
19
+ prerequisite: { noul: 0.85 },
20
+ domRelevance: [{ probability: 0.8 }, { probability: 0.2 }],
21
+ },
22
+ },
23
+ jevGate: { triggered: true },
24
+ },
25
+ { jevShadow: { status: 'error' } },
26
+ { jevShadow: { status: 'skipped' } },
27
+ ], { inputUsdPerMillion: 1 });
28
+
29
+ expect(summary).toEqual({
30
+ evaluations: 3,
31
+ completed: 1,
32
+ errors: 1,
33
+ skipped: 1,
34
+ inputTokens: 1000,
35
+ outputTokens: 25,
36
+ latencyMs: 120,
37
+ completionCandidates: 1,
38
+ triggeredCompletions: 1,
39
+ inputUsdPerMillion: 1,
40
+ estimatedInputCostUsd: 0.001,
41
+ strategies: {
42
+ evaluations: 1,
43
+ modelRoutes: { deterministic: 1 },
44
+ actionChoices: { action_1: 1 },
45
+ recoveryRoutes: { continue: 1 },
46
+ missionStages: { interact: 1 },
47
+ verifiedPostconditions: 1,
48
+ satisfiedPrerequisites: 1,
49
+ regionsEvaluated: 2,
50
+ relevantRegions: 1,
51
+ },
52
+ });
53
+ });
54
+
55
+ it('supports a configured input price', () => {
56
+ expect(resolveJevInputPrice({ TESTRONAUT_JEV_INPUT_USD_PER_MILLION: '0.05' })).toBe(0.05);
57
+ });
58
+ });
@@ -0,0 +1,80 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { applyOptimizationPolicy, resolveOptimizationPolicy } from '../../core/optimization.js';
3
+
4
+ describe('optimization policy', () => {
5
+ it.each([
6
+ ['off', false, false],
7
+ ['cost', false, false],
8
+ ['tokens', false, true],
9
+ ['balanced', false, true],
10
+ ['speed', false, false],
11
+ ])('maps %s to implemented capabilities', (objective, routing, tokenReduction) => {
12
+ expect(resolveOptimizationPolicy({
13
+ env: {}, config: { optimization: { objective, mode: 'live' } },
14
+ })).toMatchObject({ objective, mode: objective === 'off' ? 'off' : 'live', routing, tokenReduction });
15
+ });
16
+
17
+ it('accepts user-facing aliases and defaults configured policies to shadow', () => {
18
+ expect(resolveOptimizationPolicy({
19
+ env: { TESTRONAUT_OPTIMIZE: 'both' }, config: {},
20
+ })).toMatchObject({ objective: 'balanced', mode: 'shadow' });
21
+ });
22
+
23
+ it('gives environment values precedence over config', () => {
24
+ expect(resolveOptimizationPolicy({
25
+ env: { TESTRONAUT_OPTIMIZE: 'cost', TESTRONAUT_OPTIMIZATION_MODE: 'live' },
26
+ config: { optimization: { objective: 'tokens', mode: 'shadow' } },
27
+ })).toMatchObject({ objective: 'cost', mode: 'live', routingRequested: true, routing: false });
28
+ });
29
+
30
+ it('requires an explicit beta opt-in before model routing can activate', () => {
31
+ expect(resolveOptimizationPolicy({
32
+ env: { TESTRONAUT_BETA_MODEL_ROUTING: '1' },
33
+ config: { optimization: { objective: 'balanced', mode: 'live' } },
34
+ })).toMatchObject({ routingRequested: true, betaModelRouting: true, routing: true });
35
+ });
36
+
37
+ it('supports stable guardrails without strategy or model routing calls', () => {
38
+ const env = {};
39
+ applyOptimizationPolicy(resolveOptimizationPolicy({
40
+ env: {}, config: { optimization: { objective: 'tokens', mode: 'live', guardrails: { completionProbability: 0.9 } } },
41
+ }), env);
42
+ expect(env).toMatchObject({
43
+ TESTRONAUT_JEV_ROUTING: 'off',
44
+ TESTRONAUT_JEV_STRATEGIES: '0',
45
+ TESTRONAUT_JEV_GATE: '1',
46
+ TESTRONAUT_JEV_GATE_THRESHOLD: '0.9',
47
+ });
48
+ });
49
+
50
+ it('accepts the top-level opt-in guardrails configuration', () => {
51
+ expect(resolveOptimizationPolicy({
52
+ env: {}, config: { guardrails: { enabled: true, mode: 'live', completionProbability: 0.85 } },
53
+ })).toMatchObject({ objective: 'tokens', mode: 'live', routing: false, completionProbability: 0.85 });
54
+ expect(resolveOptimizationPolicy({
55
+ env: {}, config: { guardrails: { enabled: false } },
56
+ })).toMatchObject({ objective: 'off', mode: 'off' });
57
+ });
58
+
59
+ it('translates policy to legacy Jev controls without replacing explicit overrides', () => {
60
+ const env = { TESTRONAUT_JEV_GATE: '0' };
61
+ applyOptimizationPolicy(resolveOptimizationPolicy({
62
+ env: { TESTRONAUT_BETA_MODEL_ROUTING: '1' }, config: { optimization: { objective: 'balanced', mode: 'live' } },
63
+ }), env);
64
+ expect(env).toMatchObject({
65
+ TESTRONAUT_JEV_ROUTING: 'live',
66
+ TESTRONAUT_JEV_GATE: '0',
67
+ TESTRONAUT_JEV_STRATEGIES: '1',
68
+ TESTRONAUT_JEV_SHADOW: '1',
69
+ TESTRONAUT_JEV_ROUTING_THRESHOLD: '0.4',
70
+ });
71
+ });
72
+
73
+ it('allows explicit CLI policy to replace lower-level overrides', () => {
74
+ const env = { TESTRONAUT_JEV_GATE: '1', TESTRONAUT_JEV_ROUTING: 'live' };
75
+ applyOptimizationPolicy(resolveOptimizationPolicy({
76
+ env: { TESTRONAUT_OPTIMIZE: 'off' }, config: {},
77
+ }), env, { force: true });
78
+ expect(env).toMatchObject({ TESTRONAUT_JEV_GATE: '0', TESTRONAUT_JEV_ROUTING: 'off' });
79
+ });
80
+ });