outcome-cli 1.0.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 +261 -0
- package/package.json +95 -0
- package/src/agents/README.md +139 -0
- package/src/agents/adapters/anthropic.adapter.ts +166 -0
- package/src/agents/adapters/dalle.adapter.ts +145 -0
- package/src/agents/adapters/gemini.adapter.ts +134 -0
- package/src/agents/adapters/imagen.adapter.ts +106 -0
- package/src/agents/adapters/nano-banana.adapter.ts +129 -0
- package/src/agents/adapters/openai.adapter.ts +165 -0
- package/src/agents/adapters/veo.adapter.ts +130 -0
- package/src/agents/agent.schema.property.test.ts +379 -0
- package/src/agents/agent.schema.test.ts +148 -0
- package/src/agents/agent.schema.ts +263 -0
- package/src/agents/index.ts +60 -0
- package/src/agents/registered-agent.schema.ts +356 -0
- package/src/agents/registry.ts +97 -0
- package/src/agents/tournament-configs.property.test.ts +266 -0
- package/src/cli/README.md +145 -0
- package/src/cli/commands/define.ts +79 -0
- package/src/cli/commands/list.ts +46 -0
- package/src/cli/commands/logs.ts +83 -0
- package/src/cli/commands/run.ts +416 -0
- package/src/cli/commands/verify.ts +110 -0
- package/src/cli/index.ts +81 -0
- package/src/config/README.md +128 -0
- package/src/config/env.ts +262 -0
- package/src/config/index.ts +19 -0
- package/src/eval/README.md +318 -0
- package/src/eval/ai-judge.test.ts +435 -0
- package/src/eval/ai-judge.ts +368 -0
- package/src/eval/code-validators.ts +414 -0
- package/src/eval/evaluateOutcome.property.test.ts +1174 -0
- package/src/eval/evaluateOutcome.ts +591 -0
- package/src/eval/immigration-validators.ts +122 -0
- package/src/eval/index.ts +90 -0
- package/src/eval/judge-cache.ts +402 -0
- package/src/eval/tournament-validators.property.test.ts +439 -0
- package/src/eval/validators.property.test.ts +1118 -0
- package/src/eval/validators.ts +1199 -0
- package/src/eval/weighted-scorer.ts +285 -0
- package/src/index.ts +17 -0
- package/src/league/README.md +188 -0
- package/src/league/health-check.ts +353 -0
- package/src/league/index.ts +93 -0
- package/src/league/killAgent.ts +151 -0
- package/src/league/league.test.ts +1151 -0
- package/src/league/runLeague.ts +843 -0
- package/src/league/scoreAgent.ts +175 -0
- package/src/modules/omnibridge/__tests__/.gitkeep +1 -0
- package/src/modules/omnibridge/__tests__/auth-tunnel.property.test.ts +524 -0
- package/src/modules/omnibridge/__tests__/deterministic-logger.property.test.ts +965 -0
- package/src/modules/omnibridge/__tests__/ghost-api.property.test.ts +461 -0
- package/src/modules/omnibridge/__tests__/omnibridge-integration.test.ts +542 -0
- package/src/modules/omnibridge/__tests__/parallel-executor.property.test.ts +671 -0
- package/src/modules/omnibridge/__tests__/semantic-normalizer.property.test.ts +521 -0
- package/src/modules/omnibridge/__tests__/semantic-normalizer.test.ts +254 -0
- package/src/modules/omnibridge/__tests__/session-vault.property.test.ts +367 -0
- package/src/modules/omnibridge/__tests__/shadow-session.property.test.ts +523 -0
- package/src/modules/omnibridge/__tests__/triangulation-engine.property.test.ts +292 -0
- package/src/modules/omnibridge/__tests__/verification-engine.property.test.ts +769 -0
- package/src/modules/omnibridge/api/.gitkeep +1 -0
- package/src/modules/omnibridge/api/ghost-api.ts +1087 -0
- package/src/modules/omnibridge/auth/.gitkeep +1 -0
- package/src/modules/omnibridge/auth/auth-tunnel.ts +843 -0
- package/src/modules/omnibridge/auth/session-vault.ts +577 -0
- package/src/modules/omnibridge/core/.gitkeep +1 -0
- package/src/modules/omnibridge/core/semantic-normalizer.ts +702 -0
- package/src/modules/omnibridge/core/triangulation-engine.ts +530 -0
- package/src/modules/omnibridge/core/types.ts +610 -0
- package/src/modules/omnibridge/execution/.gitkeep +1 -0
- package/src/modules/omnibridge/execution/deterministic-logger.ts +629 -0
- package/src/modules/omnibridge/execution/parallel-executor.ts +542 -0
- package/src/modules/omnibridge/execution/shadow-session.ts +794 -0
- package/src/modules/omnibridge/index.ts +212 -0
- package/src/modules/omnibridge/omnibridge.ts +510 -0
- package/src/modules/omnibridge/verification/.gitkeep +1 -0
- package/src/modules/omnibridge/verification/verification-engine.ts +783 -0
- package/src/outcomes/README.md +75 -0
- package/src/outcomes/acquire-pilot-customer.ts +297 -0
- package/src/outcomes/code-delivery-outcomes.ts +89 -0
- package/src/outcomes/code-outcomes.ts +256 -0
- package/src/outcomes/code_review_battle.test.ts +135 -0
- package/src/outcomes/code_review_battle.ts +135 -0
- package/src/outcomes/cold_email_battle.ts +97 -0
- package/src/outcomes/content_creation_battle.ts +160 -0
- package/src/outcomes/f1_stem_opt_compliance.ts +61 -0
- package/src/outcomes/index.ts +107 -0
- package/src/outcomes/lead_gen_battle.test.ts +113 -0
- package/src/outcomes/lead_gen_battle.ts +99 -0
- package/src/outcomes/outcome.schema.property.test.ts +229 -0
- package/src/outcomes/outcome.schema.ts +187 -0
- package/src/outcomes/qualified_sales_interest.ts +118 -0
- package/src/outcomes/swarm_planner.property.test.ts +370 -0
- package/src/outcomes/swarm_planner.ts +96 -0
- package/src/outcomes/web_extraction.ts +234 -0
- package/src/runtime/README.md +220 -0
- package/src/runtime/agentRunner.test.ts +341 -0
- package/src/runtime/agentRunner.ts +746 -0
- package/src/runtime/claudeAdapter.ts +232 -0
- package/src/runtime/costTracker.ts +123 -0
- package/src/runtime/index.ts +34 -0
- package/src/runtime/modelAdapter.property.test.ts +305 -0
- package/src/runtime/modelAdapter.ts +144 -0
- package/src/runtime/openaiAdapter.ts +235 -0
- package/src/utils/README.md +122 -0
- package/src/utils/command-runner.ts +134 -0
- package/src/utils/cost-guard.ts +379 -0
- package/src/utils/errors.test.ts +290 -0
- package/src/utils/errors.ts +442 -0
- package/src/utils/index.ts +37 -0
- package/src/utils/logger.test.ts +361 -0
- package/src/utils/logger.ts +419 -0
- package/src/utils/output-parsers.ts +216 -0
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shadow Session Orchestrator Property Tests
|
|
3
|
+
*
|
|
4
|
+
* Property-based tests for session isolation and resumption.
|
|
5
|
+
*
|
|
6
|
+
* Requirements: 4.4, 5.5
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, test, expect, beforeEach, afterEach } from 'vitest';
|
|
10
|
+
import * as fc from 'fast-check';
|
|
11
|
+
import {
|
|
12
|
+
ShadowSessionOrchestrator,
|
|
13
|
+
createShadowSessionOrchestrator,
|
|
14
|
+
generateFingerprint,
|
|
15
|
+
} from '../execution/shadow-session.js';
|
|
16
|
+
import { createSessionVault, type SessionVault } from '../auth/session-vault.js';
|
|
17
|
+
import type { SessionConfig } from '../core/types.js';
|
|
18
|
+
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// Test Setup
|
|
21
|
+
// =============================================================================
|
|
22
|
+
|
|
23
|
+
// Note: Each property test creates its own orchestrator and vault instances
|
|
24
|
+
// to ensure proper isolation between test iterations
|
|
25
|
+
|
|
26
|
+
let orchestrator: ShadowSessionOrchestrator;
|
|
27
|
+
let vault: SessionVault;
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
vault = createSessionVault();
|
|
31
|
+
orchestrator = createShadowSessionOrchestrator({
|
|
32
|
+
vault,
|
|
33
|
+
heartbeatIntervalMs: 60000, // Long interval to avoid interference
|
|
34
|
+
sessionTimeoutMs: 3600000,
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
orchestrator.clear();
|
|
40
|
+
vault.clear();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// =============================================================================
|
|
44
|
+
// Arbitraries
|
|
45
|
+
// =============================================================================
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Generate arbitrary domain names.
|
|
49
|
+
*/
|
|
50
|
+
const domainArbitrary = fc.stringMatching(/^[a-z][a-z0-9-]{2,20}\.(com|org|net|io)$/);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Generate arbitrary agent IDs.
|
|
54
|
+
*/
|
|
55
|
+
const agentIdArbitrary = fc.stringMatching(/^agent_[a-z0-9]{8,16}$/);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Generate arbitrary storage keys.
|
|
59
|
+
*/
|
|
60
|
+
const storageKeyArbitrary = fc.stringMatching(/^[a-zA-Z_][a-zA-Z0-9_]{0,30}$/);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Generate arbitrary storage values.
|
|
64
|
+
*/
|
|
65
|
+
const storageValueArbitrary = fc.string({ minLength: 1, maxLength: 100 });
|
|
66
|
+
|
|
67
|
+
// =============================================================================
|
|
68
|
+
// Property 9: Session Isolation
|
|
69
|
+
// =============================================================================
|
|
70
|
+
|
|
71
|
+
describe('Property 9: Session Isolation', () => {
|
|
72
|
+
/**
|
|
73
|
+
* **Feature: omnibridge, Property 9: Session Isolation**
|
|
74
|
+
*
|
|
75
|
+
* *For any* two Shadow_Sessions created for different agents,
|
|
76
|
+
* modifications to one session's state SHALL NOT affect the other session's state.
|
|
77
|
+
*
|
|
78
|
+
* **Validates: Requirements 4.4**
|
|
79
|
+
*/
|
|
80
|
+
test.each([{ numRuns: 100 }])(
|
|
81
|
+
'Property 9: modifications to one session SHALL NOT affect another session',
|
|
82
|
+
async () => {
|
|
83
|
+
await fc.assert(
|
|
84
|
+
fc.asyncProperty(
|
|
85
|
+
domainArbitrary,
|
|
86
|
+
agentIdArbitrary,
|
|
87
|
+
agentIdArbitrary,
|
|
88
|
+
storageKeyArbitrary,
|
|
89
|
+
storageValueArbitrary,
|
|
90
|
+
storageValueArbitrary,
|
|
91
|
+
async (domain, agentId1, agentId2, key, value1, value2) => {
|
|
92
|
+
// Ensure different agent IDs
|
|
93
|
+
const agent1 = agentId1;
|
|
94
|
+
const agent2 = agentId1 === agentId2 ? `${agentId2}_different` : agentId2;
|
|
95
|
+
|
|
96
|
+
// Create two sessions for different agents
|
|
97
|
+
const config1: SessionConfig = {
|
|
98
|
+
targetDomain: domain,
|
|
99
|
+
fingerprint: generateFingerprint(`seed1_${agent1}`),
|
|
100
|
+
isolationLevel: 'strict',
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const config2: SessionConfig = {
|
|
104
|
+
targetDomain: domain,
|
|
105
|
+
fingerprint: generateFingerprint(`seed2_${agent2}`),
|
|
106
|
+
isolationLevel: 'strict',
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const result1 = await orchestrator.create(config1, agent1);
|
|
110
|
+
const result2 = await orchestrator.create(config2, agent2);
|
|
111
|
+
|
|
112
|
+
expect(result1.success).toBe(true);
|
|
113
|
+
expect(result2.success).toBe(true);
|
|
114
|
+
expect(result1.session).toBeDefined();
|
|
115
|
+
expect(result2.session).toBeDefined();
|
|
116
|
+
|
|
117
|
+
const sessionId1 = result1.session!.id;
|
|
118
|
+
const sessionId2 = result2.session!.id;
|
|
119
|
+
|
|
120
|
+
// Verify sessions are isolated
|
|
121
|
+
expect(orchestrator.areSessionsIsolated(sessionId1, sessionId2)).toBe(true);
|
|
122
|
+
|
|
123
|
+
// Modify session 1's localStorage
|
|
124
|
+
orchestrator.modifySessionState(sessionId1, key, value1, 'localStorage');
|
|
125
|
+
|
|
126
|
+
// Modify session 2's localStorage with different value
|
|
127
|
+
orchestrator.modifySessionState(sessionId2, key, value2, 'localStorage');
|
|
128
|
+
|
|
129
|
+
// Verify session 1 still has its original value (not affected by session 2)
|
|
130
|
+
const session1Value = orchestrator.getSessionStateValue(sessionId1, key, 'localStorage');
|
|
131
|
+
expect(session1Value).toBe(value1);
|
|
132
|
+
|
|
133
|
+
// Verify session 2 has its own value
|
|
134
|
+
const session2Value = orchestrator.getSessionStateValue(sessionId2, key, 'localStorage');
|
|
135
|
+
expect(session2Value).toBe(value2);
|
|
136
|
+
|
|
137
|
+
// Values should be independent
|
|
138
|
+
if (value1 !== value2) {
|
|
139
|
+
expect(session1Value).not.toBe(session2Value);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Clean up
|
|
143
|
+
await orchestrator.destroy(sessionId1);
|
|
144
|
+
await orchestrator.destroy(sessionId2);
|
|
145
|
+
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
),
|
|
149
|
+
{ numRuns: 100 }
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Sessions for the same agent should also be isolated from each other.
|
|
156
|
+
*/
|
|
157
|
+
test.each([{ numRuns: 100 }])(
|
|
158
|
+
'multiple sessions for same agent are also isolated',
|
|
159
|
+
async () => {
|
|
160
|
+
await fc.assert(
|
|
161
|
+
fc.asyncProperty(
|
|
162
|
+
domainArbitrary,
|
|
163
|
+
agentIdArbitrary,
|
|
164
|
+
storageKeyArbitrary,
|
|
165
|
+
storageValueArbitrary,
|
|
166
|
+
async (domain, agentId, key, value) => {
|
|
167
|
+
// Create two sessions for the same agent
|
|
168
|
+
const config: SessionConfig = {
|
|
169
|
+
targetDomain: domain,
|
|
170
|
+
fingerprint: generateFingerprint(),
|
|
171
|
+
isolationLevel: 'strict',
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const result1 = await orchestrator.create(config, agentId);
|
|
175
|
+
const result2 = await orchestrator.create(
|
|
176
|
+
{ ...config, fingerprint: generateFingerprint() },
|
|
177
|
+
agentId
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
expect(result1.success).toBe(true);
|
|
181
|
+
expect(result2.success).toBe(true);
|
|
182
|
+
|
|
183
|
+
const sessionId1 = result1.session!.id;
|
|
184
|
+
const sessionId2 = result2.session!.id;
|
|
185
|
+
|
|
186
|
+
// Sessions should be isolated even for same agent
|
|
187
|
+
expect(orchestrator.areSessionsIsolated(sessionId1, sessionId2)).toBe(true);
|
|
188
|
+
|
|
189
|
+
// Modify only session 1
|
|
190
|
+
orchestrator.modifySessionState(sessionId1, key, value, 'localStorage');
|
|
191
|
+
|
|
192
|
+
// Session 2 should not have the value
|
|
193
|
+
const session2Value = orchestrator.getSessionStateValue(sessionId2, key, 'localStorage');
|
|
194
|
+
expect(session2Value).toBeUndefined();
|
|
195
|
+
|
|
196
|
+
// Session 1 should have the value
|
|
197
|
+
const session1Value = orchestrator.getSessionStateValue(sessionId1, key, 'localStorage');
|
|
198
|
+
expect(session1Value).toBe(value);
|
|
199
|
+
|
|
200
|
+
// Clean up
|
|
201
|
+
await orchestrator.destroy(sessionId1);
|
|
202
|
+
await orchestrator.destroy(sessionId2);
|
|
203
|
+
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
),
|
|
207
|
+
{ numRuns: 100 }
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Session isolation extends to sessionStorage as well.
|
|
214
|
+
*/
|
|
215
|
+
test.each([{ numRuns: 100 }])(
|
|
216
|
+
'sessionStorage is also isolated between sessions',
|
|
217
|
+
async () => {
|
|
218
|
+
await fc.assert(
|
|
219
|
+
fc.asyncProperty(
|
|
220
|
+
domainArbitrary,
|
|
221
|
+
agentIdArbitrary,
|
|
222
|
+
agentIdArbitrary,
|
|
223
|
+
storageKeyArbitrary,
|
|
224
|
+
storageValueArbitrary,
|
|
225
|
+
async (domain, agentId1, agentId2, key, value) => {
|
|
226
|
+
const agent1 = agentId1;
|
|
227
|
+
const agent2 = agentId1 === agentId2 ? `${agentId2}_alt` : agentId2;
|
|
228
|
+
|
|
229
|
+
const result1 = await orchestrator.create(
|
|
230
|
+
{ targetDomain: domain, fingerprint: generateFingerprint(), isolationLevel: 'strict' },
|
|
231
|
+
agent1
|
|
232
|
+
);
|
|
233
|
+
const result2 = await orchestrator.create(
|
|
234
|
+
{ targetDomain: domain, fingerprint: generateFingerprint(), isolationLevel: 'strict' },
|
|
235
|
+
agent2
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
expect(result1.success).toBe(true);
|
|
239
|
+
expect(result2.success).toBe(true);
|
|
240
|
+
|
|
241
|
+
const sessionId1 = result1.session!.id;
|
|
242
|
+
const sessionId2 = result2.session!.id;
|
|
243
|
+
|
|
244
|
+
// Modify session 1's sessionStorage
|
|
245
|
+
orchestrator.modifySessionState(sessionId1, key, value, 'sessionStorage');
|
|
246
|
+
|
|
247
|
+
// Session 2's sessionStorage should be unaffected
|
|
248
|
+
const session2Value = orchestrator.getSessionStateValue(sessionId2, key, 'sessionStorage');
|
|
249
|
+
expect(session2Value).toBeUndefined();
|
|
250
|
+
|
|
251
|
+
// Session 1 should have the value
|
|
252
|
+
const session1Value = orchestrator.getSessionStateValue(sessionId1, key, 'sessionStorage');
|
|
253
|
+
expect(session1Value).toBe(value);
|
|
254
|
+
|
|
255
|
+
// Clean up
|
|
256
|
+
await orchestrator.destroy(sessionId1);
|
|
257
|
+
await orchestrator.destroy(sessionId2);
|
|
258
|
+
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
),
|
|
262
|
+
{ numRuns: 100 }
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// =============================================================================
|
|
269
|
+
// Property 10: Session Resumption
|
|
270
|
+
// =============================================================================
|
|
271
|
+
|
|
272
|
+
describe('Property 10: Session Resumption', () => {
|
|
273
|
+
/**
|
|
274
|
+
* **Feature: omnibridge, Property 10: Session Resumption**
|
|
275
|
+
*
|
|
276
|
+
* *For any* vaulted session that has not expired,
|
|
277
|
+
* resuming the session SHALL restore the authenticated state without requiring re-authentication.
|
|
278
|
+
*
|
|
279
|
+
* **Validates: Requirements 5.5**
|
|
280
|
+
*/
|
|
281
|
+
test.each([{ numRuns: 100 }])(
|
|
282
|
+
'Property 10: resuming vaulted session restores state without re-auth',
|
|
283
|
+
async () => {
|
|
284
|
+
await fc.assert(
|
|
285
|
+
fc.asyncProperty(
|
|
286
|
+
domainArbitrary,
|
|
287
|
+
agentIdArbitrary,
|
|
288
|
+
storageKeyArbitrary,
|
|
289
|
+
storageValueArbitrary,
|
|
290
|
+
async (domain, agentId, key, value) => {
|
|
291
|
+
// Create initial session
|
|
292
|
+
const config: SessionConfig = {
|
|
293
|
+
targetDomain: domain,
|
|
294
|
+
fingerprint: generateFingerprint(),
|
|
295
|
+
isolationLevel: 'strict',
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const createResult = await orchestrator.create(config, agentId);
|
|
299
|
+
expect(createResult.success).toBe(true);
|
|
300
|
+
expect(createResult.session).toBeDefined();
|
|
301
|
+
|
|
302
|
+
const originalSessionId = createResult.session!.id;
|
|
303
|
+
|
|
304
|
+
// Add some state to the session
|
|
305
|
+
orchestrator.modifySessionState(originalSessionId, key, value, 'localStorage');
|
|
306
|
+
|
|
307
|
+
// Save session to vault
|
|
308
|
+
const saved = await orchestrator.saveToVault(originalSessionId);
|
|
309
|
+
expect(saved).toBe(true);
|
|
310
|
+
|
|
311
|
+
// Destroy the original session
|
|
312
|
+
await orchestrator.destroy(originalSessionId);
|
|
313
|
+
|
|
314
|
+
// Verify original session is gone
|
|
315
|
+
expect(orchestrator.getSession(originalSessionId)).toBeNull();
|
|
316
|
+
|
|
317
|
+
// Resume from vault
|
|
318
|
+
const resumeResult = await orchestrator.resume(domain, agentId);
|
|
319
|
+
|
|
320
|
+
// Should succeed without requiring re-auth
|
|
321
|
+
expect(resumeResult.success).toBe(true);
|
|
322
|
+
expect(resumeResult.reAuthRequired).toBe(false);
|
|
323
|
+
expect(resumeResult.session).toBeDefined();
|
|
324
|
+
|
|
325
|
+
const resumedSessionId = resumeResult.session!.id;
|
|
326
|
+
|
|
327
|
+
// Resumed session should have the stored state
|
|
328
|
+
const resumedValue = orchestrator.getSessionStateValue(resumedSessionId, key, 'localStorage');
|
|
329
|
+
expect(resumedValue).toBe(value);
|
|
330
|
+
|
|
331
|
+
// Clean up
|
|
332
|
+
await orchestrator.destroy(resumedSessionId);
|
|
333
|
+
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
),
|
|
337
|
+
{ numRuns: 100 }
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Resuming a non-existent vaulted session should require re-auth.
|
|
344
|
+
*/
|
|
345
|
+
test.each([{ numRuns: 100 }])(
|
|
346
|
+
'resuming non-existent session requires re-auth',
|
|
347
|
+
async () => {
|
|
348
|
+
await fc.assert(
|
|
349
|
+
fc.asyncProperty(domainArbitrary, agentIdArbitrary, async (domain, agentId) => {
|
|
350
|
+
// Try to resume without any vaulted session
|
|
351
|
+
const resumeResult = await orchestrator.resume(domain, agentId);
|
|
352
|
+
|
|
353
|
+
expect(resumeResult.success).toBe(false);
|
|
354
|
+
expect(resumeResult.reAuthRequired).toBe(true);
|
|
355
|
+
expect(resumeResult.session).toBeUndefined();
|
|
356
|
+
|
|
357
|
+
return true;
|
|
358
|
+
}),
|
|
359
|
+
{ numRuns: 100 }
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Valid vaulted session check should return true for non-expired sessions.
|
|
366
|
+
*/
|
|
367
|
+
test.each([{ numRuns: 100 }])(
|
|
368
|
+
'hasValidVaultedSession returns true for non-expired sessions',
|
|
369
|
+
async () => {
|
|
370
|
+
await fc.assert(
|
|
371
|
+
fc.asyncProperty(domainArbitrary, agentIdArbitrary, async (domain, agentId) => {
|
|
372
|
+
// Initially no vaulted session
|
|
373
|
+
const hasSessionBefore = await orchestrator.hasValidVaultedSession(domain);
|
|
374
|
+
expect(hasSessionBefore).toBe(false);
|
|
375
|
+
|
|
376
|
+
// Create and save a session
|
|
377
|
+
const config: SessionConfig = {
|
|
378
|
+
targetDomain: domain,
|
|
379
|
+
fingerprint: generateFingerprint(),
|
|
380
|
+
isolationLevel: 'strict',
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const createResult = await orchestrator.create(config, agentId);
|
|
384
|
+
expect(createResult.success).toBe(true);
|
|
385
|
+
|
|
386
|
+
await orchestrator.saveToVault(createResult.session!.id);
|
|
387
|
+
|
|
388
|
+
// Now should have valid vaulted session
|
|
389
|
+
const hasSessionAfter = await orchestrator.hasValidVaultedSession(domain);
|
|
390
|
+
expect(hasSessionAfter).toBe(true);
|
|
391
|
+
|
|
392
|
+
// Clean up
|
|
393
|
+
await orchestrator.destroy(createResult.session!.id);
|
|
394
|
+
|
|
395
|
+
return true;
|
|
396
|
+
}),
|
|
397
|
+
{ numRuns: 100 }
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// =============================================================================
|
|
404
|
+
// Additional Property Tests
|
|
405
|
+
// =============================================================================
|
|
406
|
+
|
|
407
|
+
describe('Fingerprint Generation', () => {
|
|
408
|
+
/**
|
|
409
|
+
* Fingerprints should be deterministic when seeded.
|
|
410
|
+
*/
|
|
411
|
+
test.each([{ numRuns: 100 }])('seeded fingerprints are deterministic', async () => {
|
|
412
|
+
await fc.assert(
|
|
413
|
+
fc.property(fc.string({ minLength: 1, maxLength: 50 }), (seed) => {
|
|
414
|
+
const fp1 = generateFingerprint(seed);
|
|
415
|
+
const fp2 = generateFingerprint(seed);
|
|
416
|
+
|
|
417
|
+
expect(fp1.userAgent).toBe(fp2.userAgent);
|
|
418
|
+
expect(fp1.resolution).toEqual(fp2.resolution);
|
|
419
|
+
expect(fp1.timezone).toBe(fp2.timezone);
|
|
420
|
+
expect(fp1.language).toBe(fp2.language);
|
|
421
|
+
|
|
422
|
+
return true;
|
|
423
|
+
}),
|
|
424
|
+
{ numRuns: 100 }
|
|
425
|
+
);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Different seeds should produce different fingerprints (with high probability).
|
|
430
|
+
*/
|
|
431
|
+
test.each([{ numRuns: 100 }])('different seeds produce different fingerprints', async () => {
|
|
432
|
+
await fc.assert(
|
|
433
|
+
fc.property(
|
|
434
|
+
fc.string({ minLength: 5, maxLength: 50 }),
|
|
435
|
+
fc.string({ minLength: 5, maxLength: 50 }),
|
|
436
|
+
(seed1, seed2) => {
|
|
437
|
+
// Skip if seeds are the same
|
|
438
|
+
if (seed1 === seed2) return true;
|
|
439
|
+
|
|
440
|
+
const fp1 = generateFingerprint(seed1);
|
|
441
|
+
const fp2 = generateFingerprint(seed2);
|
|
442
|
+
|
|
443
|
+
// At least one property should differ (with very high probability)
|
|
444
|
+
const allSame =
|
|
445
|
+
fp1.userAgent === fp2.userAgent &&
|
|
446
|
+
fp1.resolution.width === fp2.resolution.width &&
|
|
447
|
+
fp1.resolution.height === fp2.resolution.height &&
|
|
448
|
+
fp1.timezone === fp2.timezone &&
|
|
449
|
+
fp1.language === fp2.language;
|
|
450
|
+
|
|
451
|
+
// This could theoretically fail but is extremely unlikely
|
|
452
|
+
// We allow it to pass if all are same since it's probabilistic
|
|
453
|
+
expect(typeof allSame).toBe('boolean');
|
|
454
|
+
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
),
|
|
458
|
+
{ numRuns: 100 }
|
|
459
|
+
);
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
describe('Session Lifecycle', () => {
|
|
464
|
+
/**
|
|
465
|
+
* Created sessions should be retrievable.
|
|
466
|
+
*/
|
|
467
|
+
test.each([{ numRuns: 100 }])('created sessions are retrievable', async () => {
|
|
468
|
+
await fc.assert(
|
|
469
|
+
fc.asyncProperty(domainArbitrary, agentIdArbitrary, async (domain, agentId) => {
|
|
470
|
+
const config: SessionConfig = {
|
|
471
|
+
targetDomain: domain,
|
|
472
|
+
fingerprint: generateFingerprint(),
|
|
473
|
+
isolationLevel: 'strict',
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
const result = await orchestrator.create(config, agentId);
|
|
477
|
+
expect(result.success).toBe(true);
|
|
478
|
+
expect(result.session).toBeDefined();
|
|
479
|
+
|
|
480
|
+
const retrieved = orchestrator.getSession(result.session!.id);
|
|
481
|
+
expect(retrieved).not.toBeNull();
|
|
482
|
+
expect(retrieved!.id).toBe(result.session!.id);
|
|
483
|
+
expect(retrieved!.domain).toBe(domain);
|
|
484
|
+
|
|
485
|
+
// Clean up
|
|
486
|
+
await orchestrator.destroy(result.session!.id);
|
|
487
|
+
|
|
488
|
+
return true;
|
|
489
|
+
}),
|
|
490
|
+
{ numRuns: 100 }
|
|
491
|
+
);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Destroyed sessions should not be retrievable.
|
|
496
|
+
*/
|
|
497
|
+
test.each([{ numRuns: 100 }])('destroyed sessions are not retrievable', async () => {
|
|
498
|
+
await fc.assert(
|
|
499
|
+
fc.asyncProperty(domainArbitrary, agentIdArbitrary, async (domain, agentId) => {
|
|
500
|
+
const config: SessionConfig = {
|
|
501
|
+
targetDomain: domain,
|
|
502
|
+
fingerprint: generateFingerprint(),
|
|
503
|
+
isolationLevel: 'strict',
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
const result = await orchestrator.create(config, agentId);
|
|
507
|
+
expect(result.success).toBe(true);
|
|
508
|
+
|
|
509
|
+
const sessionId = result.session!.id;
|
|
510
|
+
|
|
511
|
+
// Destroy the session
|
|
512
|
+
await orchestrator.destroy(sessionId);
|
|
513
|
+
|
|
514
|
+
// Should not be retrievable
|
|
515
|
+
const retrieved = orchestrator.getSession(sessionId);
|
|
516
|
+
expect(retrieved).toBeNull();
|
|
517
|
+
|
|
518
|
+
return true;
|
|
519
|
+
}),
|
|
520
|
+
{ numRuns: 100 }
|
|
521
|
+
);
|
|
522
|
+
});
|
|
523
|
+
});
|