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.
- package/README.md +198 -0
- package/TODO.md +5 -0
- package/bin/cli.js +215 -2
- package/core/agent.js +1 -0
- package/core/browserEvidence.js +184 -0
- package/core/jevRouting.js +77 -0
- package/core/jevShadow.js +325 -0
- package/core/jevSuiteSelection.js +39 -0
- package/core/jevUsage.js +62 -0
- package/core/optimization.js +98 -0
- package/core/turnLoop.js +174 -10
- package/experiments/CROSS_PROVIDER.md +19 -0
- package/experiments/README.md +62 -0
- package/experiments/buildExperimentPublicationPackage.js +219 -0
- package/experiments/buildOptimizationDataPackage.js +390 -0
- package/experiments/crossProviderBenchmark.js +227 -0
- package/experiments/optimizationBenchmark.js +339 -0
- package/package.json +5 -1
- package/tests/binTests/cli.helpers.test.js +16 -0
- package/tests/binTests/cli.integration.test.js +14 -0
- package/tests/coreTests/browserEvidence.test.js +70 -0
- package/tests/coreTests/jevRouting.test.js +57 -0
- package/tests/coreTests/jevShadow.test.js +158 -0
- package/tests/coreTests/jevSuiteSelection.test.js +24 -0
- package/tests/coreTests/jevUsage.test.js +58 -0
- package/tests/coreTests/optimization.test.js +80 -0
- package/tests/coreTests/turnLoop.test.js +304 -0
- package/tests/experimentTests/buildOptimizationDataPackage.test.js +25 -0
- package/tests/experimentTests/crossProviderBenchmark.test.js +37 -0
- package/tests/experimentTests/optimizationBenchmark.test.js +49 -0
- package/tests/toolsTests/generateHtmlReport.test.js +80 -0
- package/tools/generateHtmlReport.js +57 -5
|
@@ -112,6 +112,7 @@ vi.mock('../../core/turnIntent.js', () => ({
|
|
|
112
112
|
vi.mock('../../core/redaction.js', () => ({
|
|
113
113
|
maskPreview: (value) => `masked:${String(value ?? '').length}`,
|
|
114
114
|
redactArgs: (name, args) => args,
|
|
115
|
+
redactPasswordInText: (value) => String(value ?? ''),
|
|
115
116
|
}));
|
|
116
117
|
|
|
117
118
|
// Import SUT after mocks
|
|
@@ -130,6 +131,13 @@ describe('turnLoop', () => {
|
|
|
130
131
|
|
|
131
132
|
beforeEach(() => {
|
|
132
133
|
delete process.env.TESTRONAUT_SCREENSHOTS;
|
|
134
|
+
delete process.env.TESTRONAUT_JEV_SHADOW;
|
|
135
|
+
delete process.env.TESTRONAUT_JEV_GATE;
|
|
136
|
+
delete process.env.TESTRONAUT_JEV_STRATEGIES;
|
|
137
|
+
delete process.env.TESTRONAUT_JEV_ROUTING;
|
|
138
|
+
delete process.env.TESTRONAUT_JEV_FAST_MODEL;
|
|
139
|
+
delete process.env.TESTRONAUT_JEV_ROUTING_THRESHOLD;
|
|
140
|
+
delete process.env.TESTRONAUT_JEV_API_KEY;
|
|
133
141
|
browser = {};
|
|
134
142
|
// reset spies and chat mock
|
|
135
143
|
shared.chromeToolSpies.fill.mockClear();
|
|
@@ -174,6 +182,302 @@ describe('turnLoop', () => {
|
|
|
174
182
|
expect(shared.chromeToolSpies.get_dom).not.toHaveBeenCalled();
|
|
175
183
|
});
|
|
176
184
|
|
|
185
|
+
it('records Jev shadow telemetry without changing the final result', async () => {
|
|
186
|
+
process.env.TESTRONAUT_JEV_SHADOW = '1';
|
|
187
|
+
process.env.TESTRONAUT_JEV_STRATEGIES = '1';
|
|
188
|
+
process.env.TESTRONAUT_JEV_API_KEY = 'test-key';
|
|
189
|
+
const originalFetch = globalThis.fetch;
|
|
190
|
+
globalThis.fetch = vi.fn(async () => ({
|
|
191
|
+
ok: true,
|
|
192
|
+
json: async () => ({
|
|
193
|
+
model: 'jev-test',
|
|
194
|
+
answers: {
|
|
195
|
+
missionComplete: { type: 'noul', noul: 0.95 },
|
|
196
|
+
modelRoute: { type: 'choice', choice: 'finish', confidence: 0.9, probabilities: { finish: 0.9 } },
|
|
197
|
+
postconditionSatisfied: { type: 'noul', noul: 0.9 },
|
|
198
|
+
recoveryRoute: { type: 'choice', choice: 'continue', confidence: 0.9, probabilities: { continue: 0.9 } },
|
|
199
|
+
missionStage: { type: 'choice', choice: 'complete', confidence: 0.9, probabilities: { complete: 0.9 } },
|
|
200
|
+
prerequisiteSatisfied: { type: 'noul', noul: 0.9 },
|
|
201
|
+
},
|
|
202
|
+
usage: { input_tokens: 75, output_tokens: 2 },
|
|
203
|
+
}),
|
|
204
|
+
}));
|
|
205
|
+
shared.chatMock.mockResolvedValueOnce({
|
|
206
|
+
message: { role: 'assistant', content: 'FINAL: success' },
|
|
207
|
+
usage: { total_tokens: 10 },
|
|
208
|
+
headers: {},
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
const result = await turnLoop(browser, baseMessages(), 1, 0, 0, {}, {
|
|
213
|
+
steps: [],
|
|
214
|
+
missionName: 'demo',
|
|
215
|
+
goal: 'Complete the demo mission',
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
expect(result.success).toBe(true);
|
|
219
|
+
expect(result.steps[0].result).toMatch(/Success/);
|
|
220
|
+
expect(result.steps[0].jevShadow).toMatchObject({
|
|
221
|
+
status: 'ok',
|
|
222
|
+
model: 'jev-test',
|
|
223
|
+
usage: { input_tokens: 75, output_tokens: 2 },
|
|
224
|
+
strategies: { modelRoute: { choice: 'finish' } },
|
|
225
|
+
});
|
|
226
|
+
} finally {
|
|
227
|
+
globalThis.fetch = originalFetch;
|
|
228
|
+
delete process.env.TESTRONAUT_JEV_SHADOW;
|
|
229
|
+
delete process.env.TESTRONAUT_JEV_STRATEGIES;
|
|
230
|
+
delete process.env.TESTRONAUT_JEV_API_KEY;
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('routes the turn after a safe Jev fast-model recommendation', async () => {
|
|
235
|
+
process.env.TESTRONAUT_JEV_ROUTING = 'live';
|
|
236
|
+
process.env.TESTRONAUT_JEV_FAST_MODEL = 'stub-fast';
|
|
237
|
+
process.env.TESTRONAUT_JEV_API_KEY = 'test-key';
|
|
238
|
+
const originalFetch = globalThis.fetch;
|
|
239
|
+
globalThis.fetch = vi.fn(async () => ({
|
|
240
|
+
ok: true,
|
|
241
|
+
json: async () => ({
|
|
242
|
+
model: 'jev-test',
|
|
243
|
+
answers: {
|
|
244
|
+
modelRoute: { type: 'choice', choice: 'fast_model', confidence: 0.25, probabilities: { fast_model: 0.55 } },
|
|
245
|
+
missionStage: { type: 'choice', choice: 'interact', confidence: 0.9 },
|
|
246
|
+
recoveryRoute: { type: 'choice', choice: 'continue', confidence: 0.9 },
|
|
247
|
+
},
|
|
248
|
+
usage: { input_tokens: 50, output_tokens: 2 },
|
|
249
|
+
}),
|
|
250
|
+
}));
|
|
251
|
+
shared.chatMock
|
|
252
|
+
.mockResolvedValueOnce({
|
|
253
|
+
message: {
|
|
254
|
+
role: 'assistant', content: '',
|
|
255
|
+
tool_calls: [{ id: 'route_1', type: 'function', function: { name: 'click_text', arguments: '{"text":"Continue"}' } }],
|
|
256
|
+
},
|
|
257
|
+
usage: { total_tokens: 10 }, headers: {},
|
|
258
|
+
})
|
|
259
|
+
.mockResolvedValueOnce({
|
|
260
|
+
message: { role: 'assistant', content: 'FINAL: success' },
|
|
261
|
+
usage: { total_tokens: 5 }, headers: {},
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
const result = await turnLoop(browser, baseMessages(), 2, 0, 0, {}, { steps: [], missionName: 'demo' });
|
|
266
|
+
expect(result.success).toBe(true);
|
|
267
|
+
expect(shared.chatMock.mock.calls.map(([request]) => request.model)).toEqual(['stub-model', 'stub-fast']);
|
|
268
|
+
expect(result.steps[1]).toMatchObject({
|
|
269
|
+
model: 'stub-fast',
|
|
270
|
+
modelRouting: { recommendation: 'fast_model', applied: true, reason: 'jev_fast_model' },
|
|
271
|
+
});
|
|
272
|
+
} finally {
|
|
273
|
+
globalThis.fetch = originalFetch;
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('falls back to the primary model when a routed request fails', async () => {
|
|
278
|
+
process.env.TESTRONAUT_JEV_ROUTING = 'live';
|
|
279
|
+
process.env.TESTRONAUT_JEV_FAST_MODEL = 'stub-fast';
|
|
280
|
+
process.env.TESTRONAUT_JEV_API_KEY = 'test-key';
|
|
281
|
+
const originalFetch = globalThis.fetch;
|
|
282
|
+
globalThis.fetch = vi.fn(async () => ({
|
|
283
|
+
ok: true,
|
|
284
|
+
json: async () => ({
|
|
285
|
+
model: 'jev-test',
|
|
286
|
+
answers: {
|
|
287
|
+
modelRoute: { type: 'choice', choice: 'fast_model', confidence: 0.25, probabilities: { fast_model: 0.55 } },
|
|
288
|
+
missionStage: { type: 'choice', choice: 'interact', confidence: 0.9 },
|
|
289
|
+
recoveryRoute: { type: 'choice', choice: 'continue', confidence: 0.9 },
|
|
290
|
+
},
|
|
291
|
+
usage: { input_tokens: 50, output_tokens: 2 },
|
|
292
|
+
}),
|
|
293
|
+
}));
|
|
294
|
+
shared.chatMock
|
|
295
|
+
.mockResolvedValueOnce({
|
|
296
|
+
message: {
|
|
297
|
+
role: 'assistant', content: '',
|
|
298
|
+
tool_calls: [{ id: 'route_1', type: 'function', function: { name: 'click_text', arguments: '{"text":"Continue"}' } }],
|
|
299
|
+
},
|
|
300
|
+
usage: { total_tokens: 10 }, headers: {},
|
|
301
|
+
})
|
|
302
|
+
.mockRejectedValueOnce(new Error('fast model unavailable'))
|
|
303
|
+
.mockResolvedValueOnce({
|
|
304
|
+
message: { role: 'assistant', content: 'FINAL: success' },
|
|
305
|
+
usage: { total_tokens: 5 }, headers: {},
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
const result = await turnLoop(browser, baseMessages(), 2, 0, 0, {}, { steps: [], missionName: 'demo' });
|
|
310
|
+
expect(shared.chatMock.mock.calls.map(([request]) => request.model)).toEqual(['stub-model', 'stub-fast', 'stub-model']);
|
|
311
|
+
expect(result.steps[1].modelRouting).toMatchObject({ fallback: true, selectedModel: 'stub-model' });
|
|
312
|
+
expect(result.steps[1].events).toContain('↩️ Jev route fallback to stub-model');
|
|
313
|
+
} finally {
|
|
314
|
+
globalThis.fetch = originalFetch;
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('forces a routed action retry back onto the primary model', async () => {
|
|
319
|
+
process.env.TESTRONAUT_JEV_ROUTING = 'live';
|
|
320
|
+
process.env.TESTRONAUT_JEV_FAST_MODEL = 'stub-fast';
|
|
321
|
+
process.env.TESTRONAUT_JEV_API_KEY = 'test-key';
|
|
322
|
+
const originalFetch = globalThis.fetch;
|
|
323
|
+
globalThis.fetch = vi.fn(async () => ({
|
|
324
|
+
ok: true,
|
|
325
|
+
json: async () => ({
|
|
326
|
+
model: 'jev-test',
|
|
327
|
+
answers: {
|
|
328
|
+
modelRoute: { type: 'choice', choice: 'fast_model', confidence: 0.2, probabilities: { fast_model: 0.55 } },
|
|
329
|
+
missionStage: { type: 'choice', choice: 'interact' },
|
|
330
|
+
recoveryRoute: { type: 'choice', choice: 'continue' },
|
|
331
|
+
},
|
|
332
|
+
usage: { input_tokens: 50, output_tokens: 2 },
|
|
333
|
+
}),
|
|
334
|
+
}));
|
|
335
|
+
shared.chromeToolSpies.click_text
|
|
336
|
+
.mockResolvedValueOnce(JSON.stringify({ ok: true }))
|
|
337
|
+
.mockRejectedValueOnce(new Error('temporary click failure'));
|
|
338
|
+
const toolResponse = id => ({
|
|
339
|
+
message: {
|
|
340
|
+
role: 'assistant', content: '',
|
|
341
|
+
tool_calls: [{ id, type: 'function', function: { name: 'click_text', arguments: '{"text":"Continue"}' } }],
|
|
342
|
+
},
|
|
343
|
+
usage: { total_tokens: 10 }, headers: {},
|
|
344
|
+
});
|
|
345
|
+
shared.chatMock
|
|
346
|
+
.mockResolvedValueOnce(toolResponse('primary_action'))
|
|
347
|
+
.mockResolvedValueOnce(toolResponse('fast_action'))
|
|
348
|
+
.mockResolvedValueOnce({
|
|
349
|
+
message: { role: 'assistant', content: 'FINAL: success' },
|
|
350
|
+
usage: { total_tokens: 5 }, headers: {},
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
const result = await turnLoop(browser, baseMessages(), 3, 0, 0, {}, { steps: [], missionName: 'demo' });
|
|
355
|
+
expect(result.success).toBe(true);
|
|
356
|
+
expect(shared.chatMock.mock.calls.map(([request]) => request.model)).toEqual(['stub-model', 'stub-fast', 'stub-model']);
|
|
357
|
+
expect(result.steps[2].modelRouting).toMatchObject({
|
|
358
|
+
applied: false,
|
|
359
|
+
forcedPrimaryRetry: true,
|
|
360
|
+
reason: 'routed_retry_primary',
|
|
361
|
+
selectedModel: 'stub-model',
|
|
362
|
+
});
|
|
363
|
+
} finally {
|
|
364
|
+
globalThis.fetch = originalFetch;
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('allows the opt-in Jev gate to finish after strong browser evidence', async () => {
|
|
369
|
+
process.env.TESTRONAUT_JEV_GATE = '1';
|
|
370
|
+
process.env.TESTRONAUT_JEV_API_KEY = 'test-key';
|
|
371
|
+
const originalFetch = globalThis.fetch;
|
|
372
|
+
globalThis.fetch = vi.fn(async () => ({
|
|
373
|
+
ok: true,
|
|
374
|
+
json: async () => ({
|
|
375
|
+
model: 'jev-test',
|
|
376
|
+
answers: {
|
|
377
|
+
missionComplete: { type: 'noul', noul: 0.91 },
|
|
378
|
+
progressState: { type: 'choice', choice: 'complete', confidence: 0.9, probabilities: { complete: 0.92 } },
|
|
379
|
+
nextCompute: { type: 'choice', choice: 'finish', confidence: 0.9, probabilities: { finish: 0.93 } },
|
|
380
|
+
},
|
|
381
|
+
usage: { input_tokens: 75, output_tokens: 2 },
|
|
382
|
+
}),
|
|
383
|
+
}));
|
|
384
|
+
shared.chromeToolSpies.get_dom.mockResolvedValueOnce('<html><title>Dashboard</title><h1>Mission complete</h1></html>');
|
|
385
|
+
shared.chromeToolSpies.screenshot.mockResolvedValueOnce('Screenshot saved at: ./screenshots/jev-gate.png');
|
|
386
|
+
shared.chatMock.mockResolvedValueOnce({
|
|
387
|
+
message: {
|
|
388
|
+
role: 'assistant',
|
|
389
|
+
content: '',
|
|
390
|
+
tool_calls: [{
|
|
391
|
+
id: 'tool_gate',
|
|
392
|
+
type: 'function',
|
|
393
|
+
function: { name: 'click_text', arguments: '{"text":"Sign in"}' },
|
|
394
|
+
}],
|
|
395
|
+
},
|
|
396
|
+
usage: { total_tokens: 10 },
|
|
397
|
+
headers: {},
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
try {
|
|
401
|
+
const result = await turnLoop(browser, baseMessages(), 5, 0, 0, {}, {
|
|
402
|
+
steps: [],
|
|
403
|
+
missionName: 'demo',
|
|
404
|
+
goal: 'Reach the dashboard',
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
expect(result).toMatchObject({ success: true, completedBy: 'jev_gate' });
|
|
408
|
+
expect(shared.chatMock).toHaveBeenCalledTimes(1);
|
|
409
|
+
expect(shared.chromeToolSpies.screenshot).toHaveBeenCalledTimes(1);
|
|
410
|
+
expect(result.steps[0]).toMatchObject({
|
|
411
|
+
result: '✅ Mission Success (Jev gate)',
|
|
412
|
+
screenshotPath: './screenshots/jev-gate.png',
|
|
413
|
+
jevGate: { triggered: true, candidate: true, threshold: 0.8 },
|
|
414
|
+
});
|
|
415
|
+
} finally {
|
|
416
|
+
globalThis.fetch = originalFetch;
|
|
417
|
+
delete process.env.TESTRONAUT_JEV_GATE;
|
|
418
|
+
delete process.env.TESTRONAUT_JEV_API_KEY;
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it('fails open to the normal loop when gate finalization cannot capture a screenshot', async () => {
|
|
423
|
+
process.env.TESTRONAUT_JEV_GATE = '1';
|
|
424
|
+
process.env.TESTRONAUT_JEV_API_KEY = 'test-key';
|
|
425
|
+
const originalFetch = globalThis.fetch;
|
|
426
|
+
globalThis.fetch = vi.fn(async () => ({
|
|
427
|
+
ok: true,
|
|
428
|
+
json: async () => ({
|
|
429
|
+
model: 'jev-test',
|
|
430
|
+
answers: {
|
|
431
|
+
missionComplete: { type: 'noul', noul: 0.91 },
|
|
432
|
+
progressState: { type: 'choice', choice: 'complete', confidence: 0.9, probabilities: { complete: 0.92 } },
|
|
433
|
+
nextCompute: { type: 'choice', choice: 'finish', confidence: 0.9, probabilities: { finish: 0.93 } },
|
|
434
|
+
},
|
|
435
|
+
usage: { input_tokens: 75, output_tokens: 2 },
|
|
436
|
+
}),
|
|
437
|
+
}));
|
|
438
|
+
shared.chromeToolSpies.get_dom.mockResolvedValueOnce('<html><title>Dashboard</title><h1>Mission complete</h1></html>');
|
|
439
|
+
shared.chromeToolSpies.screenshot.mockRejectedValueOnce(new Error('disk unavailable'));
|
|
440
|
+
shared.chatMock
|
|
441
|
+
.mockResolvedValueOnce({
|
|
442
|
+
message: {
|
|
443
|
+
role: 'assistant',
|
|
444
|
+
content: '',
|
|
445
|
+
tool_calls: [{
|
|
446
|
+
id: 'tool_gate_fail_open',
|
|
447
|
+
type: 'function',
|
|
448
|
+
function: { name: 'click_text', arguments: '{"text":"Sign in"}' },
|
|
449
|
+
}],
|
|
450
|
+
},
|
|
451
|
+
usage: { total_tokens: 10 },
|
|
452
|
+
headers: {},
|
|
453
|
+
})
|
|
454
|
+
.mockResolvedValueOnce({
|
|
455
|
+
message: { role: 'assistant', content: 'FINAL: success' },
|
|
456
|
+
usage: { total_tokens: 10 },
|
|
457
|
+
headers: {},
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
try {
|
|
461
|
+
const result = await turnLoop(browser, baseMessages(), 5, 0, 0, {}, {
|
|
462
|
+
steps: [],
|
|
463
|
+
missionName: 'demo',
|
|
464
|
+
goal: 'Reach the dashboard',
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
expect(result.success).toBe(true);
|
|
468
|
+
expect(result.completedBy).toBeUndefined();
|
|
469
|
+
expect(shared.chatMock).toHaveBeenCalledTimes(2);
|
|
470
|
+
expect(result.steps[0].jevGate).toMatchObject({
|
|
471
|
+
triggered: false,
|
|
472
|
+
reason: 'screenshot_failed',
|
|
473
|
+
});
|
|
474
|
+
} finally {
|
|
475
|
+
globalThis.fetch = originalFetch;
|
|
476
|
+
delete process.env.TESTRONAUT_JEV_GATE;
|
|
477
|
+
delete process.env.TESTRONAUT_JEV_API_KEY;
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
|
|
177
481
|
it('executes a tool call, injects DOM, then finishes on next turn', async () => {
|
|
178
482
|
shared.chatMock.mockResolvedValueOnce({
|
|
179
483
|
message: {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { __dataPackageInternals } from '../../experiments/buildOptimizationDataPackage.js';
|
|
3
|
+
|
|
4
|
+
describe('optimization data package statistics', () => {
|
|
5
|
+
it('calculates sample statistics', () => {
|
|
6
|
+
expect(__dataPackageInternals.mean([1, 2, 3])).toBe(2);
|
|
7
|
+
expect(__dataPackageInternals.sampleSd([1, 2, 3])).toBe(1);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('calculates paired percentage deltas by block', () => {
|
|
11
|
+
const condition = [
|
|
12
|
+
{ block: 1, tokens: 80 }, { block: 2, tokens: 160 },
|
|
13
|
+
{ block: 3, tokens: 240 }, { block: 4, tokens: 320 }, { block: 5, tokens: 400 },
|
|
14
|
+
];
|
|
15
|
+
const control = [
|
|
16
|
+
{ block: 1, tokens: 100 }, { block: 2, tokens: 200 },
|
|
17
|
+
{ block: 3, tokens: 300 }, { block: 4, tokens: 400 }, { block: 5, tokens: 500 },
|
|
18
|
+
];
|
|
19
|
+
expect(__dataPackageInternals.pairedDelta(condition, control, 'tokens')).toEqual({
|
|
20
|
+
average: -20,
|
|
21
|
+
low: -20,
|
|
22
|
+
high: -20,
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { CROSS_PROVIDER_CONDITIONS, buildCrossProviderSchedule, commandForCrossProviderRun, interpretSelection } from '../../experiments/crossProviderBenchmark.js';
|
|
3
|
+
|
|
4
|
+
describe('cross-provider benchmark', () => {
|
|
5
|
+
it('builds 120 deterministic isolated mission executions', () => {
|
|
6
|
+
const first = buildCrossProviderSchedule();
|
|
7
|
+
const repeat = buildCrossProviderSchedule();
|
|
8
|
+
expect(first).toHaveLength(120);
|
|
9
|
+
expect(first.map(run => run.key)).toEqual(repeat.map(run => run.key));
|
|
10
|
+
for (let block = 1; block <= 5; block += 1) {
|
|
11
|
+
expect(first.filter(run => run.block === block)).toHaveLength(24);
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('limits portfolios to two models and crosses providers', () => {
|
|
16
|
+
for (const condition of CROSS_PROVIDER_CONDITIONS) expect(condition.candidates.length).toBeLessThanOrEqual(2);
|
|
17
|
+
for (const condition of CROSS_PROVIDER_CONDITIONS.filter(item => item.id.startsWith('x'))) {
|
|
18
|
+
expect(new Set(condition.candidates.map(item => item.provider)).size).toBe(2);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('uses a sufficiently confident Jev selection and otherwise falls back', () => {
|
|
23
|
+
const candidates = CROSS_PROVIDER_CONDITIONS.find(item => item.id.startsWith('x')).candidates;
|
|
24
|
+
const chosen = interpretSelection({ status: 'ok', answers: { executionModel: { choice: 'model_2', probabilities: { model_2: 0.8 } } } }, candidates);
|
|
25
|
+
expect(chosen).toMatchObject({ selected: candidates[1], selectedBy: 'jev', score: 0.8 });
|
|
26
|
+
const fallback = interpretSelection({ status: 'ok', answers: { executionModel: { choice: 'model_2', probabilities: { model_2: 0.4 } } } }, candidates);
|
|
27
|
+
expect(fallback).toMatchObject({ selected: candidates[0], selectedBy: 'fallback' });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('runs one mission with guardrails and no intra-mission fast model', () => {
|
|
31
|
+
const run = buildCrossProviderSchedule({ repetitions: 1 })[0];
|
|
32
|
+
const selection = { selected: run.candidates[0] };
|
|
33
|
+
const invocation = commandForCrossProviderRun(run, selection, '/cli.js');
|
|
34
|
+
expect(invocation.args).toEqual(['/cli.js', `--provider=${selection.selected.provider}`, `--model=${selection.selected.model}`, '--optimize=tokens', '--optimization-mode=live', run.mission]);
|
|
35
|
+
expect(invocation.args.filter(arg => arg.endsWith('.mission.js'))).toHaveLength(1);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { BENCHMARK_TARGETS, buildBenchmarkSchedule, commandForRun } from '../../experiments/optimizationBenchmark.js';
|
|
3
|
+
|
|
4
|
+
describe('optimization benchmark matrix', () => {
|
|
5
|
+
it('builds seven conditions and five blocked repetitions per repository', () => {
|
|
6
|
+
const schedule = buildBenchmarkSchedule();
|
|
7
|
+
expect(schedule).toHaveLength(70);
|
|
8
|
+
expect(schedule.filter(run => run.target === 'openai')).toHaveLength(35);
|
|
9
|
+
expect(schedule.filter(run => run.target === 'gemini')).toHaveLength(35);
|
|
10
|
+
for (const target of ['openai', 'gemini']) {
|
|
11
|
+
for (let block = 1; block <= 5; block += 1) {
|
|
12
|
+
const runs = schedule.filter(run => run.target === target && run.block === block);
|
|
13
|
+
expect(runs).toHaveLength(7);
|
|
14
|
+
expect(new Set(runs.map(run => run.id)).size).toBe(7);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('is deterministically randomized by seed', () => {
|
|
20
|
+
const first = buildBenchmarkSchedule({ repetitions: 2, seed: 'one' }).map(run => run.key);
|
|
21
|
+
const repeat = buildBenchmarkSchedule({ repetitions: 2, seed: 'one' }).map(run => run.key);
|
|
22
|
+
const other = buildBenchmarkSchedule({ repetitions: 2, seed: 'two' }).map(run => run.key);
|
|
23
|
+
expect(first).toEqual(repeat);
|
|
24
|
+
expect(first).not.toEqual(other);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('keeps routed conditions to exactly two execution models', () => {
|
|
28
|
+
for (const target of Object.values(BENCHMARK_TARGETS)) {
|
|
29
|
+
for (const condition of target.conditions) {
|
|
30
|
+
expect([condition.primary, condition.secondary].filter(Boolean).length).toBeLessThanOrEqual(2);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('constructs explicit, reproducible CLI arguments', () => {
|
|
36
|
+
const run = buildBenchmarkSchedule({ repetitions: 1, targets: ['openai'] })
|
|
37
|
+
.find(item => item.id === 'l4_terra_gpt55');
|
|
38
|
+
const invocation = commandForRun(run, '/cli/bin.js');
|
|
39
|
+
expect(invocation.args).toEqual(expect.arrayContaining([
|
|
40
|
+
'/cli/bin.js', '--provider=openai', '--model=gpt-5.6-terra',
|
|
41
|
+
'--optimize=cost', '--optimization-mode=live', 'missions/login.mission.js',
|
|
42
|
+
]));
|
|
43
|
+
expect(invocation.env).toMatchObject({
|
|
44
|
+
TESTRONAUT_JEV_FAST_MODEL: 'gpt-5.5',
|
|
45
|
+
TESTRONAUT_JEV_ROUTING_THRESHOLD: '0.4',
|
|
46
|
+
TESTRONAUT_BETA_MODEL_ROUTING: '1',
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -116,4 +116,84 @@ describe('generateHtmlReport', () => {
|
|
|
116
116
|
expect(html).toContain('<strong>gpt-5.6</strong>');
|
|
117
117
|
expect(html).not.toContain('<strong>LLM:</strong>');
|
|
118
118
|
});
|
|
119
|
+
|
|
120
|
+
it('renders run, mission, and step Jev token spend', () => {
|
|
121
|
+
const steps = [{
|
|
122
|
+
turn: 0,
|
|
123
|
+
result: '✅ Mission Success (Jev gate)',
|
|
124
|
+
tokensUsed: 500,
|
|
125
|
+
inputTokens: 450,
|
|
126
|
+
outputTokens: 50,
|
|
127
|
+
events: [],
|
|
128
|
+
jevShadow: {
|
|
129
|
+
status: 'ok',
|
|
130
|
+
usage: { input_tokens: 1000, output_tokens: 25 },
|
|
131
|
+
latencyMs: 100,
|
|
132
|
+
completionGate: { candidate: true },
|
|
133
|
+
},
|
|
134
|
+
jevGate: { triggered: true },
|
|
135
|
+
}];
|
|
136
|
+
const outPath = path.join(tmpDir, 'jev.html');
|
|
137
|
+
generateHtmlReport({
|
|
138
|
+
runId: 'jev_run',
|
|
139
|
+
llm: { provider: 'openai', model: 'gpt-5.6-terra' },
|
|
140
|
+
summary: { totalMissions: 1, passed: 1, failed: 0 },
|
|
141
|
+
jevShadow: {
|
|
142
|
+
evaluations: 1,
|
|
143
|
+
completed: 1,
|
|
144
|
+
errors: 0,
|
|
145
|
+
skipped: 0,
|
|
146
|
+
inputTokens: 1000,
|
|
147
|
+
outputTokens: 25,
|
|
148
|
+
estimatedInputCostUsd: 0.000042,
|
|
149
|
+
},
|
|
150
|
+
missions: [{ missionName: 'Jev Mission', status: 'passed', steps }],
|
|
151
|
+
}, outPath);
|
|
152
|
+
|
|
153
|
+
const html = fs.readFileSync(outPath, 'utf8');
|
|
154
|
+
expect(html).toContain('<strong>gpt-5.6-terra</strong>');
|
|
155
|
+
expect(html).toContain('title="Jev"');
|
|
156
|
+
expect(html).toContain('<strong>Jev</strong>');
|
|
157
|
+
expect(html).toContain('Model tokens: 500 (450 in / 50 out)');
|
|
158
|
+
expect(html).toContain('Jev tokens: 1,000 in / 25 out');
|
|
159
|
+
expect(html).toContain('Jev: 1,000 in / 25 out');
|
|
160
|
+
expect(html).not.toContain('estimated spend');
|
|
161
|
+
expect(html).not.toContain('est. $');
|
|
162
|
+
expect(html).not.toContain('strategy eval');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('breaks dynamically routed token usage down by model', () => {
|
|
166
|
+
const outPath = path.join(tmpDir, 'routed.html');
|
|
167
|
+
generateHtmlReport({
|
|
168
|
+
runId: 'routed_run',
|
|
169
|
+
llm: { provider: 'openai', model: 'gpt-5.6-terra' },
|
|
170
|
+
optimization: { configured: true, objective: 'cost', mode: 'live' },
|
|
171
|
+
missions: [{
|
|
172
|
+
missionName: 'Routed Mission', status: 'passed', steps: [
|
|
173
|
+
{ turn: 0, model: 'gpt-5.6-terra', tokensUsed: 100, inputTokens: 90, outputTokens: 10, result: '✅ Passed' },
|
|
174
|
+
{ turn: 1, model: 'gpt-5.6-luna', tokensUsed: 40, inputTokens: 35, outputTokens: 5, result: '✅ Passed', modelRouting: { applied: true } },
|
|
175
|
+
],
|
|
176
|
+
}],
|
|
177
|
+
}, outPath);
|
|
178
|
+
|
|
179
|
+
const html = fs.readFileSync(outPath, 'utf8');
|
|
180
|
+
expect(html).toContain('<strong>gpt-5.6-terra + gpt-5.6-luna</strong>');
|
|
181
|
+
expect(html).toContain('gpt-5.6-terra tokens: 100 (90 in / 10 out)');
|
|
182
|
+
expect(html).toContain('gpt-5.6-luna tokens: 40 (35 in / 5 out)');
|
|
183
|
+
expect(html).toContain('By model: gpt-5.6-terra 100 / gpt-5.6-luna 40');
|
|
184
|
+
expect(html).toContain('routed by Jev');
|
|
185
|
+
expect(html).toContain('Optimization: cost (live)');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('makes skipped Jev evaluations visible', () => {
|
|
189
|
+
const outPath = path.join(tmpDir, 'jev-skipped.html');
|
|
190
|
+
generateHtmlReport({
|
|
191
|
+
runId: 'jev_skipped',
|
|
192
|
+
jevShadow: { evaluations: 3, completed: 0, errors: 0, skipped: 3, inputTokens: 0, outputTokens: 0 },
|
|
193
|
+
}, outPath);
|
|
194
|
+
|
|
195
|
+
const html = fs.readFileSync(outPath, 'utf8');
|
|
196
|
+
expect(html).toContain('Jev skipped: 3');
|
|
197
|
+
expect(html).not.toContain('Jev estimated spend:');
|
|
198
|
+
});
|
|
119
199
|
});
|