testronaut 1.11.0 → 1.12.1
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 +226 -0
- package/TODO.md +5 -0
- package/bin/cli.js +232 -5
- package/bin/terminalUi.js +9 -1
- package/core/agent.js +7 -2
- 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/missionDiscovery.js +56 -6
- package/core/optimization.js +98 -0
- package/core/turnLoop.js +232 -11
- 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 +18 -1
- package/tests/binTests/terminalUi.test.js +2 -1
- package/tests/coreTests/agent.test.js +2 -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/missionDiscovery.test.js +44 -0
- package/tests/coreTests/optimization.test.js +80 -0
- package/tests/coreTests/turnLoop.test.js +351 -1
- 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 +100 -0
- package/tools/generateHtmlReport.js +67 -10
|
@@ -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
|
+
});
|
|
@@ -78,4 +78,48 @@ describe('discoverMissionFiles', () => {
|
|
|
78
78
|
const res = await discoverMissionFiles({ cwd: tmp });
|
|
79
79
|
expect(res.files).toEqual(['keep.mission.js']);
|
|
80
80
|
});
|
|
81
|
+
|
|
82
|
+
it('orders configured patterns and places remaining missions at the wildcard', async () => {
|
|
83
|
+
const tmp = makeTempDir();
|
|
84
|
+
temps.push(tmp);
|
|
85
|
+
const missionsDir = path.join(tmp, 'missions');
|
|
86
|
+
fs.mkdirSync(path.join(missionsDir, 'account'), { recursive: true });
|
|
87
|
+
for (const file of ['login.mission.js', 'logout.mission.js', 'search.mission.js']) {
|
|
88
|
+
fs.writeFileSync(path.join(missionsDir, file), '// ok');
|
|
89
|
+
}
|
|
90
|
+
fs.writeFileSync(path.join(missionsDir, 'account', 'profile.mission.js'), '// ok');
|
|
91
|
+
|
|
92
|
+
writeJson(path.join(tmp, 'testronaut-config.json'), {
|
|
93
|
+
missions: {
|
|
94
|
+
include: ['**/*.mission.js'],
|
|
95
|
+
order: ['login.mission.js', 'account/**/*.mission.js', '*', 'logout.mission.js'],
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const res = await discoverMissionFiles({ cwd: tmp });
|
|
100
|
+
expect(res.files).toEqual([
|
|
101
|
+
'login.mission.js',
|
|
102
|
+
'account/profile.mission.js',
|
|
103
|
+
'search.mission.js',
|
|
104
|
+
'logout.mission.js',
|
|
105
|
+
]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('warns about missing exact order entries while ignoring unmatched globs', async () => {
|
|
109
|
+
const tmp = makeTempDir();
|
|
110
|
+
temps.push(tmp);
|
|
111
|
+
fs.mkdirSync(path.join(tmp, 'missions'));
|
|
112
|
+
fs.writeFileSync(path.join(tmp, 'missions', 'actual.mission.js'), '// ok');
|
|
113
|
+
writeJson(path.join(tmp, 'testronaut-config.json'), {
|
|
114
|
+
missions: {
|
|
115
|
+
order: ['missing.mission.js', 'optional/**/*.mission.js', '*'],
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const res = await discoverMissionFiles({ cwd: tmp });
|
|
120
|
+
expect(res.files).toEqual(['actual.mission.js']);
|
|
121
|
+
expect(res.warnings).toEqual([
|
|
122
|
+
'Configured mission order entry was not discovered: missing.mission.js',
|
|
123
|
+
]);
|
|
124
|
+
});
|
|
81
125
|
});
|
|
@@ -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
|
+
});
|