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.
Files changed (113) hide show
  1. package/README.md +261 -0
  2. package/package.json +95 -0
  3. package/src/agents/README.md +139 -0
  4. package/src/agents/adapters/anthropic.adapter.ts +166 -0
  5. package/src/agents/adapters/dalle.adapter.ts +145 -0
  6. package/src/agents/adapters/gemini.adapter.ts +134 -0
  7. package/src/agents/adapters/imagen.adapter.ts +106 -0
  8. package/src/agents/adapters/nano-banana.adapter.ts +129 -0
  9. package/src/agents/adapters/openai.adapter.ts +165 -0
  10. package/src/agents/adapters/veo.adapter.ts +130 -0
  11. package/src/agents/agent.schema.property.test.ts +379 -0
  12. package/src/agents/agent.schema.test.ts +148 -0
  13. package/src/agents/agent.schema.ts +263 -0
  14. package/src/agents/index.ts +60 -0
  15. package/src/agents/registered-agent.schema.ts +356 -0
  16. package/src/agents/registry.ts +97 -0
  17. package/src/agents/tournament-configs.property.test.ts +266 -0
  18. package/src/cli/README.md +145 -0
  19. package/src/cli/commands/define.ts +79 -0
  20. package/src/cli/commands/list.ts +46 -0
  21. package/src/cli/commands/logs.ts +83 -0
  22. package/src/cli/commands/run.ts +416 -0
  23. package/src/cli/commands/verify.ts +110 -0
  24. package/src/cli/index.ts +81 -0
  25. package/src/config/README.md +128 -0
  26. package/src/config/env.ts +262 -0
  27. package/src/config/index.ts +19 -0
  28. package/src/eval/README.md +318 -0
  29. package/src/eval/ai-judge.test.ts +435 -0
  30. package/src/eval/ai-judge.ts +368 -0
  31. package/src/eval/code-validators.ts +414 -0
  32. package/src/eval/evaluateOutcome.property.test.ts +1174 -0
  33. package/src/eval/evaluateOutcome.ts +591 -0
  34. package/src/eval/immigration-validators.ts +122 -0
  35. package/src/eval/index.ts +90 -0
  36. package/src/eval/judge-cache.ts +402 -0
  37. package/src/eval/tournament-validators.property.test.ts +439 -0
  38. package/src/eval/validators.property.test.ts +1118 -0
  39. package/src/eval/validators.ts +1199 -0
  40. package/src/eval/weighted-scorer.ts +285 -0
  41. package/src/index.ts +17 -0
  42. package/src/league/README.md +188 -0
  43. package/src/league/health-check.ts +353 -0
  44. package/src/league/index.ts +93 -0
  45. package/src/league/killAgent.ts +151 -0
  46. package/src/league/league.test.ts +1151 -0
  47. package/src/league/runLeague.ts +843 -0
  48. package/src/league/scoreAgent.ts +175 -0
  49. package/src/modules/omnibridge/__tests__/.gitkeep +1 -0
  50. package/src/modules/omnibridge/__tests__/auth-tunnel.property.test.ts +524 -0
  51. package/src/modules/omnibridge/__tests__/deterministic-logger.property.test.ts +965 -0
  52. package/src/modules/omnibridge/__tests__/ghost-api.property.test.ts +461 -0
  53. package/src/modules/omnibridge/__tests__/omnibridge-integration.test.ts +542 -0
  54. package/src/modules/omnibridge/__tests__/parallel-executor.property.test.ts +671 -0
  55. package/src/modules/omnibridge/__tests__/semantic-normalizer.property.test.ts +521 -0
  56. package/src/modules/omnibridge/__tests__/semantic-normalizer.test.ts +254 -0
  57. package/src/modules/omnibridge/__tests__/session-vault.property.test.ts +367 -0
  58. package/src/modules/omnibridge/__tests__/shadow-session.property.test.ts +523 -0
  59. package/src/modules/omnibridge/__tests__/triangulation-engine.property.test.ts +292 -0
  60. package/src/modules/omnibridge/__tests__/verification-engine.property.test.ts +769 -0
  61. package/src/modules/omnibridge/api/.gitkeep +1 -0
  62. package/src/modules/omnibridge/api/ghost-api.ts +1087 -0
  63. package/src/modules/omnibridge/auth/.gitkeep +1 -0
  64. package/src/modules/omnibridge/auth/auth-tunnel.ts +843 -0
  65. package/src/modules/omnibridge/auth/session-vault.ts +577 -0
  66. package/src/modules/omnibridge/core/.gitkeep +1 -0
  67. package/src/modules/omnibridge/core/semantic-normalizer.ts +702 -0
  68. package/src/modules/omnibridge/core/triangulation-engine.ts +530 -0
  69. package/src/modules/omnibridge/core/types.ts +610 -0
  70. package/src/modules/omnibridge/execution/.gitkeep +1 -0
  71. package/src/modules/omnibridge/execution/deterministic-logger.ts +629 -0
  72. package/src/modules/omnibridge/execution/parallel-executor.ts +542 -0
  73. package/src/modules/omnibridge/execution/shadow-session.ts +794 -0
  74. package/src/modules/omnibridge/index.ts +212 -0
  75. package/src/modules/omnibridge/omnibridge.ts +510 -0
  76. package/src/modules/omnibridge/verification/.gitkeep +1 -0
  77. package/src/modules/omnibridge/verification/verification-engine.ts +783 -0
  78. package/src/outcomes/README.md +75 -0
  79. package/src/outcomes/acquire-pilot-customer.ts +297 -0
  80. package/src/outcomes/code-delivery-outcomes.ts +89 -0
  81. package/src/outcomes/code-outcomes.ts +256 -0
  82. package/src/outcomes/code_review_battle.test.ts +135 -0
  83. package/src/outcomes/code_review_battle.ts +135 -0
  84. package/src/outcomes/cold_email_battle.ts +97 -0
  85. package/src/outcomes/content_creation_battle.ts +160 -0
  86. package/src/outcomes/f1_stem_opt_compliance.ts +61 -0
  87. package/src/outcomes/index.ts +107 -0
  88. package/src/outcomes/lead_gen_battle.test.ts +113 -0
  89. package/src/outcomes/lead_gen_battle.ts +99 -0
  90. package/src/outcomes/outcome.schema.property.test.ts +229 -0
  91. package/src/outcomes/outcome.schema.ts +187 -0
  92. package/src/outcomes/qualified_sales_interest.ts +118 -0
  93. package/src/outcomes/swarm_planner.property.test.ts +370 -0
  94. package/src/outcomes/swarm_planner.ts +96 -0
  95. package/src/outcomes/web_extraction.ts +234 -0
  96. package/src/runtime/README.md +220 -0
  97. package/src/runtime/agentRunner.test.ts +341 -0
  98. package/src/runtime/agentRunner.ts +746 -0
  99. package/src/runtime/claudeAdapter.ts +232 -0
  100. package/src/runtime/costTracker.ts +123 -0
  101. package/src/runtime/index.ts +34 -0
  102. package/src/runtime/modelAdapter.property.test.ts +305 -0
  103. package/src/runtime/modelAdapter.ts +144 -0
  104. package/src/runtime/openaiAdapter.ts +235 -0
  105. package/src/utils/README.md +122 -0
  106. package/src/utils/command-runner.ts +134 -0
  107. package/src/utils/cost-guard.ts +379 -0
  108. package/src/utils/errors.test.ts +290 -0
  109. package/src/utils/errors.ts +442 -0
  110. package/src/utils/index.ts +37 -0
  111. package/src/utils/logger.test.ts +361 -0
  112. package/src/utils/logger.ts +419 -0
  113. 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
+ });