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,1151 @@
1
+ /**
2
+ * League Module Tests
3
+ *
4
+ * Tests for parallel agent competition including:
5
+ * - League runner
6
+ * - Agent termination
7
+ * - Agent scoring
8
+ * - Property tests for requirements
9
+ *
10
+ * @module league/league.test
11
+ */
12
+
13
+ import { describe, it, expect, beforeEach } from 'vitest';
14
+ import * as fc from 'fast-check';
15
+ import {
16
+ runLeagueMock,
17
+ type LeagueConfig,
18
+ type LeagueResult,
19
+ } from './runLeague.js';
20
+ import {
21
+ shouldKillAgent,
22
+ type RunningAgent,
23
+ type AgentLimits,
24
+ type KillReason,
25
+ } from './killAgent.js';
26
+ import {
27
+ scoreAgent,
28
+ determineWinner,
29
+ rankAgents,
30
+ calculateLeagueStats,
31
+ type AgentScore,
32
+ type AgentMetrics,
33
+ } from './scoreAgent.js';
34
+ import type { AgentConfig } from '../agents/agent.schema.js';
35
+ import type { Outcome } from '../outcomes/outcome.schema.js';
36
+ import type { Lead } from '../jobs/job.interface.js';
37
+ import type { EvaluationResult } from '../eval/evaluateOutcome.js';
38
+ import { createCostTracker } from '../runtime/costTracker.js';
39
+ import { clearAllLogs } from '../utils/logger.js';
40
+
41
+ /**
42
+ * Test fixtures
43
+ */
44
+ const createTestAgent = (id: string, overrides?: Partial<AgentConfig>): AgentConfig => ({
45
+ id,
46
+ name: `Test Agent ${id}`,
47
+ prompt: 'You are a test agent.',
48
+ strategyDescription: 'Test strategy',
49
+ toolAccess: [],
50
+ costCeiling: 10000,
51
+ modelProvider: 'claude',
52
+ modelId: 'claude-3-sonnet-20240229',
53
+ ...overrides,
54
+ });
55
+
56
+ const createTestOutcome = (overrides?: Partial<Outcome>): Outcome => ({
57
+ name: 'test_outcome',
58
+ description: 'Test outcome',
59
+ payoutAmount: 100,
60
+ maxAttempts: 3,
61
+ timeLimitMs: 60000,
62
+ successCriteria: [
63
+ {
64
+ name: 'message_length',
65
+ validator: 'validateMessageLength',
66
+ params: { minWords: 5 },
67
+ },
68
+ ],
69
+ failureReasons: ['Test failure'],
70
+ ...overrides,
71
+ });
72
+
73
+ const createTestLead = (overrides?: Partial<Lead>): Lead => ({
74
+ email: 'test@example.com',
75
+ company: 'Test Corp',
76
+ companySize: 100,
77
+ role: 'Manager',
78
+ previousInteractions: [],
79
+ ...overrides,
80
+ });
81
+
82
+ describe('runLeagueMock', () => {
83
+ beforeEach(() => {
84
+ clearAllLogs();
85
+ });
86
+
87
+ it('should run league with specified number of agents', async () => {
88
+ const agentCount = 3;
89
+ const config: LeagueConfig = {
90
+ outcomeId: 'test_outcome',
91
+ agentCount,
92
+ globalSpendCeiling: 50000,
93
+ agentConfigs: [
94
+ createTestAgent('agent-1'),
95
+ createTestAgent('agent-2'),
96
+ createTestAgent('agent-3'),
97
+ ],
98
+ outcome: createTestOutcome(),
99
+ lead: createTestLead(),
100
+ mockMode: true,
101
+ };
102
+
103
+ const result = await runLeagueMock(config);
104
+
105
+ expect(result.agents).toHaveLength(agentCount);
106
+ expect(result.duration).toBeGreaterThan(0);
107
+ expect(result.totalCost).toBeGreaterThan(0);
108
+ });
109
+
110
+ it('should track total cost across all agents', async () => {
111
+ const config: LeagueConfig = {
112
+ outcomeId: 'test_outcome',
113
+ agentCount: 2,
114
+ globalSpendCeiling: 50000,
115
+ agentConfigs: [createTestAgent('agent-1'), createTestAgent('agent-2')],
116
+ outcome: createTestOutcome(),
117
+ lead: createTestLead(),
118
+ mockMode: true,
119
+ };
120
+
121
+ const result = await runLeagueMock(config);
122
+
123
+ const sumOfAgentCosts = result.agents.reduce((sum, a) => sum + a.tokensSpent, 0);
124
+ expect(result.totalCost).toBe(sumOfAgentCosts);
125
+ });
126
+
127
+ it('should throw error for invalid agent count', async () => {
128
+ const config: LeagueConfig = {
129
+ outcomeId: 'test_outcome',
130
+ agentCount: 0,
131
+ globalSpendCeiling: 50000,
132
+ agentConfigs: [],
133
+ outcome: createTestOutcome(),
134
+ lead: createTestLead(),
135
+ mockMode: true,
136
+ };
137
+
138
+ await expect(runLeagueMock(config)).rejects.toThrow('Agent count must be positive');
139
+ });
140
+
141
+ it('should throw error for insufficient agent configs', async () => {
142
+ const config: LeagueConfig = {
143
+ outcomeId: 'test_outcome',
144
+ agentCount: 3,
145
+ globalSpendCeiling: 50000,
146
+ agentConfigs: [createTestAgent('agent-1')], // Only 1 config for 3 agents
147
+ outcome: createTestOutcome(),
148
+ lead: createTestLead(),
149
+ mockMode: true,
150
+ };
151
+
152
+ await expect(runLeagueMock(config)).rejects.toThrow('Not enough agent configs');
153
+ });
154
+ });
155
+
156
+ describe('shouldKillAgent', () => {
157
+ const createRunningAgent = (overrides?: Partial<RunningAgent>): RunningAgent => ({
158
+ agentId: 'test-agent',
159
+ attempts: 0,
160
+ costTracker: createCostTracker('test-agent', 10000),
161
+ startTime: Date.now(),
162
+ competitorWon: false,
163
+ ...overrides,
164
+ });
165
+
166
+ const defaultLimits: AgentLimits = {
167
+ maxTokens: 10000,
168
+ maxAttempts: 5,
169
+ maxRuntimeMs: 60000,
170
+ };
171
+
172
+ it('should return null for agent within limits', () => {
173
+ const agent = createRunningAgent();
174
+ const result = shouldKillAgent(agent, defaultLimits);
175
+ expect(result).toBeNull();
176
+ });
177
+
178
+ it('should return competitor_won when competitor has won', () => {
179
+ const agent = createRunningAgent({ competitorWon: true });
180
+ const result = shouldKillAgent(agent, defaultLimits);
181
+
182
+ expect(result).not.toBeNull();
183
+ expect(result?.type).toBe('competitor_won');
184
+ });
185
+
186
+ it('should return cost_exceeded when over token limit', () => {
187
+ const costTracker = createCostTracker('test-agent', 10000);
188
+ costTracker.tokensSpent = 15000; // Over limit
189
+ const agent = createRunningAgent({ costTracker });
190
+
191
+ const result = shouldKillAgent(agent, defaultLimits);
192
+
193
+ expect(result).not.toBeNull();
194
+ expect(result?.type).toBe('cost_exceeded');
195
+ });
196
+
197
+ it('should return attempts_exceeded when at max attempts', () => {
198
+ const agent = createRunningAgent({ attempts: 5 });
199
+ const result = shouldKillAgent(agent, defaultLimits);
200
+
201
+ expect(result).not.toBeNull();
202
+ expect(result?.type).toBe('attempts_exceeded');
203
+ });
204
+
205
+ it('should return timeout when over runtime limit', () => {
206
+ const agent = createRunningAgent({
207
+ startTime: Date.now() - 70000, // 70 seconds ago
208
+ });
209
+ const result = shouldKillAgent(agent, defaultLimits);
210
+
211
+ expect(result).not.toBeNull();
212
+ expect(result?.type).toBe('timeout');
213
+ });
214
+ });
215
+
216
+ describe('scoreAgent', () => {
217
+ it('should create score from successful evaluation', () => {
218
+ const evalResult: EvaluationResult = {
219
+ status: 'SUCCESS',
220
+ reason: 'All criteria passed',
221
+ criteriaResults: [],
222
+ };
223
+ const metrics: AgentMetrics = {
224
+ agentId: 'agent-1',
225
+ tokensSpent: 500,
226
+ attempts: 2,
227
+ durationMs: 5000,
228
+ };
229
+
230
+ const score = scoreAgent(evalResult, metrics);
231
+
232
+ expect(score.agentId).toBe('agent-1');
233
+ expect(score.success).toBe(true);
234
+ expect(score.cost).toBe(500);
235
+ expect(score.attempts).toBe(2);
236
+ expect(score.duration).toBe(5000);
237
+ expect(score.successTimestamp).toBeDefined();
238
+ });
239
+
240
+ it('should create score from failed evaluation', () => {
241
+ const evalResult: EvaluationResult = {
242
+ status: 'FAILURE',
243
+ reason: 'Company too small',
244
+ criteriaResults: [],
245
+ };
246
+ const metrics: AgentMetrics = {
247
+ agentId: 'agent-2',
248
+ tokensSpent: 300,
249
+ attempts: 1,
250
+ durationMs: 3000,
251
+ };
252
+
253
+ const score = scoreAgent(evalResult, metrics);
254
+
255
+ expect(score.success).toBe(false);
256
+ expect(score.successTimestamp).toBeUndefined();
257
+ });
258
+ });
259
+
260
+ describe('determineWinner', () => {
261
+ it('should return null for empty scores', () => {
262
+ expect(determineWinner([])).toBeNull();
263
+ });
264
+
265
+ it('should return null when no successful agents', () => {
266
+ const scores: AgentScore[] = [
267
+ { agentId: 'a1', success: false, cost: 100, attempts: 1, duration: 1000 },
268
+ { agentId: 'a2', success: false, cost: 200, attempts: 2, duration: 2000 },
269
+ ];
270
+
271
+ expect(determineWinner(scores)).toBeNull();
272
+ });
273
+
274
+ it('should return the successful agent', () => {
275
+ const scores: AgentScore[] = [
276
+ { agentId: 'a1', success: false, cost: 100, attempts: 1, duration: 1000 },
277
+ { agentId: 'a2', success: true, cost: 200, attempts: 2, duration: 2000, successTimestamp: 1000 },
278
+ ];
279
+
280
+ const winner = determineWinner(scores);
281
+ expect(winner?.agentId).toBe('a2');
282
+ });
283
+
284
+ it('should return earliest successful agent when multiple succeed', () => {
285
+ const scores: AgentScore[] = [
286
+ { agentId: 'a1', success: true, cost: 100, attempts: 1, duration: 1000, successTimestamp: 2000 },
287
+ { agentId: 'a2', success: true, cost: 200, attempts: 2, duration: 2000, successTimestamp: 1000 },
288
+ ];
289
+
290
+ const winner = determineWinner(scores);
291
+ expect(winner?.agentId).toBe('a2'); // Earlier timestamp
292
+ });
293
+ });
294
+
295
+ describe('rankAgents', () => {
296
+ it('should rank successful agents before failed', () => {
297
+ const scores: AgentScore[] = [
298
+ { agentId: 'failed', success: false, cost: 50, attempts: 1, duration: 500 },
299
+ { agentId: 'success', success: true, cost: 100, attempts: 2, duration: 1000, successTimestamp: 1000 },
300
+ ];
301
+
302
+ const ranked = rankAgents(scores);
303
+ expect(ranked[0].agentId).toBe('success');
304
+ expect(ranked[1].agentId).toBe('failed');
305
+ });
306
+
307
+ it('should rank by timestamp for successful agents', () => {
308
+ const scores: AgentScore[] = [
309
+ { agentId: 'later', success: true, cost: 100, attempts: 1, duration: 1000, successTimestamp: 2000 },
310
+ { agentId: 'earlier', success: true, cost: 100, attempts: 1, duration: 1000, successTimestamp: 1000 },
311
+ ];
312
+
313
+ const ranked = rankAgents(scores);
314
+ expect(ranked[0].agentId).toBe('earlier');
315
+ });
316
+
317
+ it('should rank failed agents by attempts then cost', () => {
318
+ const scores: AgentScore[] = [
319
+ { agentId: 'a3', success: false, cost: 100, attempts: 3, duration: 1000 },
320
+ { agentId: 'a1', success: false, cost: 200, attempts: 1, duration: 1000 },
321
+ { agentId: 'a2', success: false, cost: 100, attempts: 1, duration: 1000 },
322
+ ];
323
+
324
+ const ranked = rankAgents(scores);
325
+ expect(ranked[0].agentId).toBe('a2'); // 1 attempt, 100 cost
326
+ expect(ranked[1].agentId).toBe('a1'); // 1 attempt, 200 cost
327
+ expect(ranked[2].agentId).toBe('a3'); // 3 attempts
328
+ });
329
+ });
330
+
331
+ describe('calculateLeagueStats', () => {
332
+ it('should calculate stats correctly', () => {
333
+ const scores: AgentScore[] = [
334
+ { agentId: 'a1', success: true, cost: 100, attempts: 2, duration: 1000, successTimestamp: 1000 },
335
+ { agentId: 'a2', success: false, cost: 200, attempts: 3, duration: 2000 },
336
+ { agentId: 'a3', success: true, cost: 150, attempts: 1, duration: 1500, successTimestamp: 1500 },
337
+ ];
338
+
339
+ const stats = calculateLeagueStats(scores);
340
+
341
+ expect(stats.totalAgents).toBe(3);
342
+ expect(stats.successfulAgents).toBe(2);
343
+ expect(stats.failedAgents).toBe(1);
344
+ expect(stats.totalCost).toBe(450);
345
+ expect(stats.averageCost).toBe(150);
346
+ expect(stats.totalAttempts).toBe(6);
347
+ expect(stats.averageAttempts).toBe(2);
348
+ });
349
+
350
+ it('should handle empty scores', () => {
351
+ const stats = calculateLeagueStats([]);
352
+
353
+ expect(stats.totalAgents).toBe(0);
354
+ expect(stats.averageCost).toBe(0);
355
+ expect(stats.averageAttempts).toBe(0);
356
+ });
357
+ });
358
+
359
+ describe('League Property Tests', () => {
360
+ beforeEach(() => {
361
+ clearAllLogs();
362
+ });
363
+
364
+ /**
365
+ * **Feature: earnd-bounty-engine, Property 5: League Agent Count**
366
+ * For any league configuration with agentCount N, starting the league
367
+ * SHALL instantiate exactly N agents.
368
+ * **Validates: Requirements 4.1**
369
+ */
370
+ it('Property 5: League Agent Count', async () => {
371
+ // Test with various agent counts
372
+ for (const agentCount of [1, 2, 3, 5]) {
373
+ clearAllLogs();
374
+
375
+ const agentConfigs = Array.from({ length: agentCount }, (_, i) =>
376
+ createTestAgent(`agent-${i}`)
377
+ );
378
+
379
+ const config: LeagueConfig = {
380
+ outcomeId: 'test_outcome',
381
+ agentCount,
382
+ globalSpendCeiling: 100000,
383
+ agentConfigs,
384
+ outcome: createTestOutcome(),
385
+ lead: createTestLead(),
386
+ mockMode: true,
387
+ };
388
+
389
+ const result = await runLeagueMock(config);
390
+
391
+ expect(result.agents).toHaveLength(agentCount);
392
+ }
393
+ });
394
+
395
+ /**
396
+ * **Feature: earnd-bounty-engine, Property 9: Winner Promotion**
397
+ * For any agent that achieves SUCCESS evaluation, the league SHALL mark
398
+ * that agent as winner and signal termination to all other agents.
399
+ * **Validates: Requirements 4.5**
400
+ */
401
+ it('Property 9: Winner Promotion', async () => {
402
+ const config: LeagueConfig = {
403
+ outcomeId: 'test_outcome',
404
+ agentCount: 3,
405
+ globalSpendCeiling: 100000,
406
+ agentConfigs: [
407
+ createTestAgent('agent-1'),
408
+ createTestAgent('agent-2'),
409
+ createTestAgent('agent-3'),
410
+ ],
411
+ outcome: createTestOutcome(),
412
+ lead: createTestLead(),
413
+ mockMode: true,
414
+ };
415
+
416
+ const result = await runLeagueMock(config);
417
+
418
+ // In mock mode, agents should complete and be evaluated
419
+ // At most one winner should exist
420
+ const winners = result.agents.filter((a) => a.status === 'winner');
421
+ expect(winners.length).toBeLessThanOrEqual(1);
422
+
423
+ // If there's a winner, winnerId should match
424
+ if (winners.length === 1) {
425
+ expect(result.winnerId).toBe(winners[0].agentId);
426
+ }
427
+ });
428
+
429
+ /**
430
+ * **Feature: earnd-bounty-engine, Property 21: Global Spend Ceiling**
431
+ * For any league run where total spend exceeds the global ceiling,
432
+ * the league SHALL halt all agents and return with no winner.
433
+ * **Validates: Requirements 10.3**
434
+ */
435
+ it('Property 21: Global Spend Ceiling tracking', async () => {
436
+ const globalSpendCeiling = 50000;
437
+
438
+ const config: LeagueConfig = {
439
+ outcomeId: 'test_outcome',
440
+ agentCount: 3,
441
+ globalSpendCeiling,
442
+ agentConfigs: [
443
+ createTestAgent('agent-1'),
444
+ createTestAgent('agent-2'),
445
+ createTestAgent('agent-3'),
446
+ ],
447
+ outcome: createTestOutcome(),
448
+ lead: createTestLead(),
449
+ mockMode: true,
450
+ };
451
+
452
+ const result = await runLeagueMock(config);
453
+
454
+ // Total cost should be tracked
455
+ expect(result.totalCost).toBeGreaterThan(0);
456
+
457
+ // globalCeilingHit should be true if ceiling was exceeded
458
+ if (result.totalCost >= globalSpendCeiling) {
459
+ expect(result.globalCeilingHit).toBe(true);
460
+ }
461
+ });
462
+
463
+ /**
464
+ * Property: Cost tracking isolation
465
+ * Each agent's cost should be tracked independently.
466
+ */
467
+ it('Property: Cost tracking isolation', async () => {
468
+ const config: LeagueConfig = {
469
+ outcomeId: 'test_outcome',
470
+ agentCount: 3,
471
+ globalSpendCeiling: 100000,
472
+ agentConfigs: [
473
+ createTestAgent('agent-1'),
474
+ createTestAgent('agent-2'),
475
+ createTestAgent('agent-3'),
476
+ ],
477
+ outcome: createTestOutcome(),
478
+ lead: createTestLead(),
479
+ mockMode: true,
480
+ };
481
+
482
+ const result = await runLeagueMock(config);
483
+
484
+ // Each agent should have independent cost tracking
485
+ for (const agent of result.agents) {
486
+ expect(agent.tokensSpent).toBeGreaterThanOrEqual(0);
487
+ }
488
+
489
+ // Sum should equal total
490
+ const sum = result.agents.reduce((s, a) => s + a.tokensSpent, 0);
491
+ expect(result.totalCost).toBe(sum);
492
+ });
493
+
494
+ /**
495
+ * **Feature: earnd-bounty-engine, Property 7: Attempt Limit Termination**
496
+ * For any agent that exceeds the maxAttempts limit, the league SHALL
497
+ * terminate that agent with reason 'attempts_exceeded'.
498
+ * **Validates: Requirements 4.3**
499
+ */
500
+ it('Property 7: Attempt Limit Termination', () => {
501
+ const limits: AgentLimits = {
502
+ maxTokens: 10000,
503
+ maxAttempts: 3,
504
+ maxRuntimeMs: 60000,
505
+ };
506
+
507
+ // Agent at max attempts should be killed
508
+ const agent: RunningAgent = {
509
+ agentId: 'test-agent',
510
+ attempts: 3,
511
+ costTracker: createCostTracker('test-agent', 10000),
512
+ startTime: Date.now(),
513
+ competitorWon: false,
514
+ };
515
+
516
+ const reason = shouldKillAgent(agent, limits);
517
+ expect(reason).not.toBeNull();
518
+ expect(reason?.type).toBe('attempts_exceeded');
519
+ });
520
+
521
+ /**
522
+ * **Feature: earnd-bounty-engine, Property 8: Cost Ceiling Termination**
523
+ * For any agent that exceeds its cost ceiling, the league SHALL
524
+ * terminate that agent with reason 'cost_exceeded'.
525
+ * **Validates: Requirements 4.4, 10.1**
526
+ */
527
+ it('Property 8: Cost Ceiling Termination', () => {
528
+ const limits: AgentLimits = {
529
+ maxTokens: 10000,
530
+ maxAttempts: 5,
531
+ maxRuntimeMs: 60000,
532
+ };
533
+
534
+ // Agent over cost should be killed
535
+ const costTracker = createCostTracker('test-agent', 10000);
536
+ costTracker.tokensSpent = 15000; // Over limit
537
+
538
+ const agent: RunningAgent = {
539
+ agentId: 'test-agent',
540
+ attempts: 1,
541
+ costTracker,
542
+ startTime: Date.now(),
543
+ competitorWon: false,
544
+ };
545
+
546
+ const reason = shouldKillAgent(agent, limits);
547
+ expect(reason).not.toBeNull();
548
+ expect(reason?.type).toBe('cost_exceeded');
549
+ });
550
+
551
+ /**
552
+ * **Feature: earnd-bounty-engine, Property 20: Runtime Limit Termination**
553
+ * For any agent that exceeds the max runtime limit, the league SHALL
554
+ * terminate that agent with reason 'timeout'.
555
+ * **Validates: Requirements 10.2**
556
+ */
557
+ it('Property 20: Runtime Limit Termination', () => {
558
+ const limits: AgentLimits = {
559
+ maxTokens: 10000,
560
+ maxAttempts: 5,
561
+ maxRuntimeMs: 30000, // 30 seconds
562
+ };
563
+
564
+ // Agent over runtime should be killed
565
+ const agent: RunningAgent = {
566
+ agentId: 'test-agent',
567
+ attempts: 1,
568
+ costTracker: createCostTracker('test-agent', 10000),
569
+ startTime: Date.now() - 35000, // 35 seconds ago
570
+ competitorWon: false,
571
+ };
572
+
573
+ const reason = shouldKillAgent(agent, limits);
574
+ expect(reason).not.toBeNull();
575
+ expect(reason?.type).toBe('timeout');
576
+ });
577
+
578
+ /**
579
+ * **Feature: earnd-bounty-engine, Property 22: Fail-Closed on Limit Exceeded**
580
+ * For any agent termination due to cost, attempt, or time limits,
581
+ * no payout SHALL be triggered for that agent.
582
+ * **Validates: Requirements 10.4**
583
+ */
584
+ it('Property 22: Fail-Closed on Limit Exceeded', () => {
585
+ const limits: AgentLimits = {
586
+ maxTokens: 10000,
587
+ maxAttempts: 5,
588
+ maxRuntimeMs: 60000,
589
+ };
590
+
591
+ // Test all limit types result in non-recoverable termination
592
+ const testCases = [
593
+ { attempts: 5, tokensSpent: 100, elapsed: 0, expectedType: 'attempts_exceeded' },
594
+ { attempts: 1, tokensSpent: 15000, elapsed: 0, expectedType: 'cost_exceeded' },
595
+ { attempts: 1, tokensSpent: 100, elapsed: 70000, expectedType: 'timeout' },
596
+ ];
597
+
598
+ for (const tc of testCases) {
599
+ const costTracker = createCostTracker('test-agent', 10000);
600
+ costTracker.tokensSpent = tc.tokensSpent;
601
+
602
+ const agent: RunningAgent = {
603
+ agentId: 'test-agent',
604
+ attempts: tc.attempts,
605
+ costTracker,
606
+ startTime: Date.now() - tc.elapsed,
607
+ competitorWon: false,
608
+ };
609
+
610
+ const reason = shouldKillAgent(agent, limits);
611
+ expect(reason).not.toBeNull();
612
+ expect(reason?.type).toBe(tc.expectedType);
613
+
614
+ // All limit-based kills mean no payout - verify by checking type is a limit type
615
+ const limitTypes = ['cost_exceeded', 'attempts_exceeded', 'timeout'];
616
+ expect(limitTypes).toContain(reason?.type);
617
+ }
618
+ });
619
+ });
620
+
621
+ /**
622
+ * Property-based tests for Tournament System Determinism
623
+ *
624
+ * **Feature: launch-readiness-checklist, Property 3: Tournament system determinism**
625
+ * **Validates: Requirements 1.5**
626
+ *
627
+ * Property 3: Tournament system determinism
628
+ * *For any* identical tournament input (same agents, same outcome, same data),
629
+ * running the tournament multiple times SHALL produce identical results.
630
+ */
631
+
632
+ describe('Tournament System Determinism - Property Tests', () => {
633
+ beforeEach(() => {
634
+ clearAllLogs();
635
+ });
636
+
637
+ // **Feature: launch-readiness-checklist, Property 3: Tournament system determinism**
638
+ test('identical tournament inputs produce identical results', async () => {
639
+ // Create a deterministic tournament configuration
640
+ const createDeterministicConfig = (): LeagueConfig => ({
641
+ outcomeId: 'test_outcome',
642
+ agentCount: 3,
643
+ globalSpendCeiling: 50000,
644
+ agentConfigs: [
645
+ createTestAgent('agent-1', { costCeiling: 10000 }),
646
+ createTestAgent('agent-2', { costCeiling: 15000 }),
647
+ createTestAgent('agent-3', { costCeiling: 12000 }),
648
+ ],
649
+ outcome: createTestOutcome({
650
+ payoutAmount: 250,
651
+ maxAttempts: 5,
652
+ timeLimitMs: 30000,
653
+ }),
654
+ lead: createTestLead({
655
+ email: 'test@example.com',
656
+ company: 'Test Corp',
657
+ companySize: 100,
658
+ role: 'Manager',
659
+ }),
660
+ mockMode: true, // Use mock mode for deterministic behavior
661
+ });
662
+
663
+ // Run the same tournament configuration multiple times
664
+ const config1 = createDeterministicConfig();
665
+ const config2 = createDeterministicConfig();
666
+ const config3 = createDeterministicConfig();
667
+
668
+ const result1 = await runLeagueMock(config1);
669
+ const result2 = await runLeagueMock(config2);
670
+ const result3 = await runLeagueMock(config3);
671
+
672
+ // All results must have identical structure
673
+ expect(result1.agents).toHaveLength(result2.agents.length);
674
+ expect(result2.agents).toHaveLength(result3.agents.length);
675
+
676
+ // All results must have identical winner (or lack thereof)
677
+ expect(result1.winnerId).toBe(result2.winnerId);
678
+ expect(result2.winnerId).toBe(result3.winnerId);
679
+
680
+ // All results must have identical global ceiling hit status
681
+ expect(result1.globalCeilingHit).toBe(result2.globalCeilingHit);
682
+ expect(result2.globalCeilingHit).toBe(result3.globalCeilingHit);
683
+
684
+ // Tournament metrics must be identical
685
+ expect(result1.tournamentMetrics.netProfitability).toBe(result2.tournamentMetrics.netProfitability);
686
+ expect(result2.tournamentMetrics.netProfitability).toBe(result3.tournamentMetrics.netProfitability);
687
+
688
+ expect(result1.tournamentMetrics.costPerSuccess).toBe(result2.tournamentMetrics.costPerSuccess);
689
+ expect(result2.tournamentMetrics.costPerSuccess).toBe(result3.tournamentMetrics.costPerSuccess);
690
+
691
+ expect(result1.tournamentMetrics.successCount).toBe(result2.tournamentMetrics.successCount);
692
+ expect(result2.tournamentMetrics.successCount).toBe(result3.tournamentMetrics.successCount);
693
+
694
+ // Agent results must have identical structure and status
695
+ for (let i = 0; i < result1.agents.length; i++) {
696
+ const agent1 = result1.agents[i];
697
+ const agent2 = result2.agents[i];
698
+ const agent3 = result3.agents[i];
699
+
700
+ expect(agent1.agentId).toBe(agent2.agentId);
701
+ expect(agent2.agentId).toBe(agent3.agentId);
702
+
703
+ expect(agent1.status).toBe(agent2.status);
704
+ expect(agent2.status).toBe(agent3.status);
705
+
706
+ expect(agent1.attempts).toBe(agent2.attempts);
707
+ expect(agent2.attempts).toBe(agent3.attempts);
708
+
709
+ // In mock mode, token costs should be deterministic
710
+ expect(agent1.tokensSpent).toBe(agent2.tokensSpent);
711
+ expect(agent2.tokensSpent).toBe(agent3.tokensSpent);
712
+ }
713
+ });
714
+
715
+ // **Feature: launch-readiness-checklist, Property 3: Tournament system determinism**
716
+ test('tournament results are deterministic across different agent configurations', async () => {
717
+ // Test with a few specific configurations to avoid timeout
718
+ const testConfigurations = [
719
+ { agentCount: 1, globalSpendCeiling: 10000, payoutAmount: 50, maxAttempts: 1 },
720
+ { agentCount: 2, globalSpendCeiling: 50000, payoutAmount: 250, maxAttempts: 3 },
721
+ { agentCount: 3, globalSpendCeiling: 30000, payoutAmount: 100, maxAttempts: 2 },
722
+ ];
723
+
724
+ for (const config of testConfigurations) {
725
+ // Create identical configurations
726
+ const agentConfigs = Array.from({ length: config.agentCount }, (_, i) =>
727
+ createTestAgent(`agent-${i}`, { costCeiling: 10000 })
728
+ );
729
+
730
+ const baseConfig: LeagueConfig = {
731
+ outcomeId: 'determinism_test',
732
+ agentCount: config.agentCount,
733
+ globalSpendCeiling: config.globalSpendCeiling,
734
+ agentConfigs,
735
+ outcome: createTestOutcome({
736
+ payoutAmount: config.payoutAmount,
737
+ maxAttempts: config.maxAttempts,
738
+ timeLimitMs: 60000,
739
+ }),
740
+ lead: createTestLead(),
741
+ mockMode: true,
742
+ };
743
+
744
+ // Run tournament twice with identical inputs
745
+ const result1 = await runLeagueMock({ ...baseConfig });
746
+ const result2 = await runLeagueMock({ ...baseConfig });
747
+
748
+ // Results must be identical
749
+ expect(result1.winnerId).toBe(result2.winnerId);
750
+ expect(result1.agents.length).toBe(result2.agents.length);
751
+ expect(result1.globalCeilingHit).toBe(result2.globalCeilingHit);
752
+
753
+ // Tournament metrics must be identical
754
+ expect(result1.tournamentMetrics.netProfitability).toBe(result2.tournamentMetrics.netProfitability);
755
+ expect(result1.tournamentMetrics.costPerSuccess).toBe(result2.tournamentMetrics.costPerSuccess);
756
+ expect(result1.tournamentMetrics.successCount).toBe(result2.tournamentMetrics.successCount);
757
+
758
+ // Agent results must be identical
759
+ for (let i = 0; i < result1.agents.length; i++) {
760
+ expect(result1.agents[i].status).toBe(result2.agents[i].status);
761
+ expect(result1.agents[i].attempts).toBe(result2.agents[i].attempts);
762
+ expect(result1.agents[i].tokensSpent).toBe(result2.agents[i].tokensSpent);
763
+ }
764
+ }
765
+ }, 10000); // Increase timeout to 10 seconds
766
+
767
+ // **Feature: launch-readiness-checklist, Property 3: Tournament system determinism**
768
+ test('tournament system produces consistent results structure', async () => {
769
+ const config: LeagueConfig = {
770
+ outcomeId: 'structure_test',
771
+ agentCount: 2,
772
+ globalSpendCeiling: 30000,
773
+ agentConfigs: [
774
+ createTestAgent('agent-1'),
775
+ createTestAgent('agent-2'),
776
+ ],
777
+ outcome: createTestOutcome(),
778
+ lead: createTestLead(),
779
+ mockMode: true,
780
+ };
781
+
782
+ // Run tournament multiple times
783
+ const results = await Promise.all([
784
+ runLeagueMock(config),
785
+ runLeagueMock(config),
786
+ runLeagueMock(config),
787
+ ]);
788
+
789
+ // All results must have consistent structure
790
+ for (const result of results) {
791
+ // Required fields must exist
792
+ expect(typeof result.winnerId).toBe('string' || result.winnerId === null);
793
+ expect(Array.isArray(result.agents)).toBe(true);
794
+ expect(typeof result.totalCost).toBe('number');
795
+ expect(typeof result.duration).toBe('number');
796
+ expect(typeof result.globalCeilingHit).toBe('boolean');
797
+ expect(typeof result.tournamentMetrics).toBe('object');
798
+
799
+ // Tournament metrics structure
800
+ expect(typeof result.tournamentMetrics.netProfitability).toBe('number');
801
+ expect(typeof result.tournamentMetrics.costPerSuccess).toBe('number');
802
+ expect(typeof result.tournamentMetrics.successCount).toBe('number');
803
+
804
+ // Agent results structure
805
+ for (const agent of result.agents) {
806
+ expect(typeof agent.agentId).toBe('string');
807
+ expect(['winner', 'killed', 'failed']).toContain(agent.status);
808
+ expect(typeof agent.attempts).toBe('number');
809
+ expect(typeof agent.tokensSpent).toBe('number');
810
+ expect(typeof agent.durationMs).toBe('number');
811
+ expect(agent.attempts).toBeGreaterThan(0);
812
+ expect(agent.tokensSpent).toBeGreaterThanOrEqual(0);
813
+ expect(agent.durationMs).toBeGreaterThan(0);
814
+ }
815
+ }
816
+
817
+ // All results must have identical structure
818
+ expect(results[0].agents.length).toBe(results[1].agents.length);
819
+ expect(results[1].agents.length).toBe(results[2].agents.length);
820
+ });
821
+
822
+ // **Feature: launch-readiness-checklist, Property 3: Tournament system determinism**
823
+ test('tournament determinism holds with different lead data', async () => {
824
+ // Test with a few specific lead data configurations
825
+ const leadConfigurations = [
826
+ { email: 'test@example.com', company: 'Test Corp', companySize: 100, role: 'Manager' },
827
+ { email: 'user@company.com', company: 'Big Company', companySize: 500, role: 'Director' },
828
+ { email: 'contact@startup.io', company: 'Startup Inc', companySize: 50, role: 'CEO' },
829
+ ];
830
+
831
+ for (const leadData of leadConfigurations) {
832
+ const config: LeagueConfig = {
833
+ outcomeId: 'lead_determinism_test',
834
+ agentCount: 2,
835
+ globalSpendCeiling: 40000,
836
+ agentConfigs: [
837
+ createTestAgent('agent-1'),
838
+ createTestAgent('agent-2'),
839
+ ],
840
+ outcome: createTestOutcome(),
841
+ lead: createTestLead(leadData),
842
+ mockMode: true,
843
+ };
844
+
845
+ // Run tournament twice with identical lead data
846
+ const result1 = await runLeagueMock(config);
847
+ const result2 = await runLeagueMock(config);
848
+
849
+ // Results must be deterministic regardless of lead data
850
+ expect(result1.winnerId).toBe(result2.winnerId);
851
+ expect(result1.agents.length).toBe(result2.agents.length);
852
+ expect(result1.tournamentMetrics.netProfitability).toBe(result2.tournamentMetrics.netProfitability);
853
+ expect(result1.tournamentMetrics.costPerSuccess).toBe(result2.tournamentMetrics.costPerSuccess);
854
+ expect(result1.tournamentMetrics.successCount).toBe(result2.tournamentMetrics.successCount);
855
+ }
856
+ });
857
+
858
+ // **Feature: launch-readiness-checklist, Property 3: Tournament system determinism**
859
+ test('tournament system maintains determinism under resource constraints', async () => {
860
+ // Test with very low global spend ceiling to trigger constraint behavior
861
+ const constrainedConfig: LeagueConfig = {
862
+ outcomeId: 'constraint_test',
863
+ agentCount: 3,
864
+ globalSpendCeiling: 1000, // Very low ceiling
865
+ agentConfigs: [
866
+ createTestAgent('agent-1', { costCeiling: 500 }),
867
+ createTestAgent('agent-2', { costCeiling: 600 }),
868
+ createTestAgent('agent-3', { costCeiling: 400 }),
869
+ ],
870
+ outcome: createTestOutcome({ maxAttempts: 2 }),
871
+ lead: createTestLead(),
872
+ mockMode: true,
873
+ };
874
+
875
+ // Run multiple times
876
+ const result1 = await runLeagueMock(constrainedConfig);
877
+ const result2 = await runLeagueMock(constrainedConfig);
878
+ const result3 = await runLeagueMock(constrainedConfig);
879
+
880
+ // Even under constraints, results must be deterministic
881
+ expect(result1.globalCeilingHit).toBe(result2.globalCeilingHit);
882
+ expect(result2.globalCeilingHit).toBe(result3.globalCeilingHit);
883
+
884
+ expect(result1.winnerId).toBe(result2.winnerId);
885
+ expect(result2.winnerId).toBe(result3.winnerId);
886
+
887
+ expect(result1.tournamentMetrics.successCount).toBe(result2.tournamentMetrics.successCount);
888
+ expect(result2.tournamentMetrics.successCount).toBe(result3.tournamentMetrics.successCount);
889
+
890
+ // Agent statuses must be consistent
891
+ for (let i = 0; i < result1.agents.length; i++) {
892
+ expect(result1.agents[i].status).toBe(result2.agents[i].status);
893
+ expect(result2.agents[i].status).toBe(result3.agents[i].status);
894
+ }
895
+ });
896
+ });
897
+
898
+ /**
899
+ * Property-based tests for Tournament Metrics Calculation
900
+ *
901
+ * **Feature: launch-readiness-checklist, Property 2: Tournament metrics calculation accuracy**
902
+ * **Validates: Requirements 1.2**
903
+ *
904
+ * Property 2: Tournament metrics calculation accuracy
905
+ * *For any* tournament with payout amount, total cost, and success count,
906
+ * the tournament metrics SHALL calculate NetProfitability as (payout - cost) / cost
907
+ * and CostPerSuccess as totalSpend / successCount.
908
+ */
909
+
910
+ import { calculateTournamentMetrics } from './runLeague.js';
911
+
912
+ describe('Tournament Metrics Calculation - Property Tests', () => {
913
+ // **Feature: launch-readiness-checklist, Property 2: Tournament metrics calculation accuracy**
914
+ test('net profitability calculation is correct for positive costs', () => {
915
+ fc.assert(
916
+ fc.property(
917
+ fc.float({ min: 1, max: 10000, noNaN: true }), // payoutAmount
918
+ fc.float({ min: 1, max: 10000, noNaN: true }), // totalCost
919
+ fc.integer({ min: 1, max: 100 }), // successCount
920
+ (payoutAmount, totalCost, successCount) => {
921
+ const metrics = calculateTournamentMetrics(payoutAmount, totalCost, successCount);
922
+
923
+ // Net profitability should be (payout - cost) / cost
924
+ const expectedNetProfitability = (payoutAmount - totalCost) / totalCost;
925
+ expect(metrics.netProfitability).toBeCloseTo(expectedNetProfitability, 10);
926
+
927
+ // Success count should be preserved
928
+ expect(metrics.successCount).toBe(successCount);
929
+ }
930
+ ),
931
+ { numRuns: 100 }
932
+ );
933
+ });
934
+
935
+ // **Feature: launch-readiness-checklist, Property 2: Tournament metrics calculation accuracy**
936
+ test('cost per success calculation is correct for positive success count', () => {
937
+ fc.assert(
938
+ fc.property(
939
+ fc.float({ min: 1, max: 10000, noNaN: true }), // payoutAmount
940
+ fc.float({ min: 1, max: 10000, noNaN: true }), // totalCost
941
+ fc.integer({ min: 1, max: 100 }), // successCount
942
+ (payoutAmount, totalCost, successCount) => {
943
+ const metrics = calculateTournamentMetrics(payoutAmount, totalCost, successCount);
944
+
945
+ // Cost per success should be totalCost / successCount
946
+ const expectedCostPerSuccess = totalCost / successCount;
947
+ expect(metrics.costPerSuccess).toBeCloseTo(expectedCostPerSuccess, 10);
948
+ }
949
+ ),
950
+ { numRuns: 100 }
951
+ );
952
+ });
953
+
954
+ // **Feature: launch-readiness-checklist, Property 2: Tournament metrics calculation accuracy**
955
+ test('zero cost edge case returns zero profitability', () => {
956
+ fc.assert(
957
+ fc.property(
958
+ fc.float({ min: 1, max: 10000 }), // payoutAmount
959
+ fc.integer({ min: 0, max: 100 }), // successCount
960
+ (payoutAmount, successCount) => {
961
+ const metrics = calculateTournamentMetrics(payoutAmount, 0, successCount);
962
+
963
+ // Zero cost should result in zero profitability (edge case handling)
964
+ expect(metrics.netProfitability).toBe(0);
965
+
966
+ // Cost per success should be 0 / successCount = 0 (if successCount > 0) or Infinity (if successCount = 0)
967
+ if (successCount === 0) {
968
+ expect(metrics.costPerSuccess).toBe(Infinity);
969
+ } else {
970
+ expect(metrics.costPerSuccess).toBe(0);
971
+ }
972
+ }
973
+ ),
974
+ { numRuns: 100 }
975
+ );
976
+ });
977
+
978
+ // **Feature: launch-readiness-checklist, Property 2: Tournament metrics calculation accuracy**
979
+ test('zero success count edge case returns infinity cost per success', () => {
980
+ fc.assert(
981
+ fc.property(
982
+ fc.float({ min: 1, max: 10000, noNaN: true }), // payoutAmount
983
+ fc.float({ min: 1, max: 10000, noNaN: true }), // totalCost
984
+ (payoutAmount, totalCost) => {
985
+ const metrics = calculateTournamentMetrics(payoutAmount, totalCost, 0);
986
+
987
+ // Zero successes should result in Infinity cost per success
988
+ expect(metrics.costPerSuccess).toBe(Infinity);
989
+ expect(metrics.successCount).toBe(0);
990
+
991
+ // Net profitability should still be calculated normally
992
+ const expectedNetProfitability = (payoutAmount - totalCost) / totalCost;
993
+ expect(metrics.netProfitability).toBeCloseTo(expectedNetProfitability, 10);
994
+ }
995
+ ),
996
+ { numRuns: 100 }
997
+ );
998
+ });
999
+
1000
+ // **Feature: launch-readiness-checklist, Property 2: Tournament metrics calculation accuracy**
1001
+ test('profitability is positive when payout exceeds cost', () => {
1002
+ fc.assert(
1003
+ fc.property(
1004
+ fc.float({ min: 100, max: 10000, noNaN: true }), // payoutAmount
1005
+ fc.float({ min: 1, max: 99, noNaN: true }), // totalCost (less than payout)
1006
+ fc.integer({ min: 1, max: 100 }), // successCount
1007
+ (payoutAmount, totalCost, successCount) => {
1008
+ const metrics = calculateTournamentMetrics(payoutAmount, totalCost, successCount);
1009
+
1010
+ // When payout > cost, profitability should be positive
1011
+ expect(metrics.netProfitability).toBeGreaterThan(0);
1012
+ }
1013
+ ),
1014
+ { numRuns: 100 }
1015
+ );
1016
+ });
1017
+
1018
+ // **Feature: launch-readiness-checklist, Property 2: Tournament metrics calculation accuracy**
1019
+ test('profitability is negative when cost exceeds payout', () => {
1020
+ fc.assert(
1021
+ fc.property(
1022
+ fc.float({ min: 1, max: 99, noNaN: true }), // payoutAmount
1023
+ fc.float({ min: 100, max: 10000, noNaN: true }), // totalCost (greater than payout)
1024
+ fc.integer({ min: 1, max: 100 }), // successCount
1025
+ (payoutAmount, totalCost, successCount) => {
1026
+ const metrics = calculateTournamentMetrics(payoutAmount, totalCost, successCount);
1027
+
1028
+ // When cost > payout, profitability should be negative
1029
+ expect(metrics.netProfitability).toBeLessThan(0);
1030
+ }
1031
+ ),
1032
+ { numRuns: 100 }
1033
+ );
1034
+ });
1035
+
1036
+ // **Feature: launch-readiness-checklist, Property 2: Tournament metrics calculation accuracy**
1037
+ test('calculation is deterministic - same input produces same output', () => {
1038
+ fc.assert(
1039
+ fc.property(
1040
+ fc.float({ min: 0, max: 10000 }), // payoutAmount
1041
+ fc.float({ min: 0, max: 10000 }), // totalCost
1042
+ fc.integer({ min: 0, max: 100 }), // successCount
1043
+ (payoutAmount, totalCost, successCount) => {
1044
+ const metrics1 = calculateTournamentMetrics(payoutAmount, totalCost, successCount);
1045
+ const metrics2 = calculateTournamentMetrics(payoutAmount, totalCost, successCount);
1046
+
1047
+ expect(metrics1.netProfitability).toBe(metrics2.netProfitability);
1048
+ expect(metrics1.costPerSuccess).toBe(metrics2.costPerSuccess);
1049
+ expect(metrics1.successCount).toBe(metrics2.successCount);
1050
+ }
1051
+ ),
1052
+ { numRuns: 100 }
1053
+ );
1054
+ });
1055
+
1056
+ // **Feature: launch-readiness-checklist, Property 2: Tournament metrics calculation accuracy**
1057
+ test('metrics structure is correct', () => {
1058
+ fc.assert(
1059
+ fc.property(
1060
+ fc.float({ min: 0, max: 10000, noNaN: true }), // payoutAmount
1061
+ fc.float({ min: 0, max: 10000, noNaN: true }), // totalCost
1062
+ fc.integer({ min: 0, max: 100 }), // successCount
1063
+ (payoutAmount, totalCost, successCount) => {
1064
+ const metrics = calculateTournamentMetrics(payoutAmount, totalCost, successCount);
1065
+
1066
+ // Metrics must have correct structure
1067
+ expect(typeof metrics.netProfitability).toBe('number');
1068
+ expect(typeof metrics.costPerSuccess).toBe('number');
1069
+ expect(typeof metrics.successCount).toBe('number');
1070
+ expect(metrics.successCount).toBe(successCount);
1071
+
1072
+ // Values should be finite or Infinity (for zero success case)
1073
+ expect(Number.isFinite(metrics.netProfitability) || metrics.netProfitability === 0).toBe(true);
1074
+ expect(Number.isFinite(metrics.costPerSuccess) || metrics.costPerSuccess === Infinity).toBe(true);
1075
+ }
1076
+ ),
1077
+ { numRuns: 100 }
1078
+ );
1079
+ });
1080
+
1081
+ // **Feature: launch-readiness-checklist, Property 2: Tournament metrics calculation accuracy**
1082
+ test('boundary values are handled correctly', () => {
1083
+ // Test specific boundary cases
1084
+ const boundaryTests = [
1085
+ { payout: 0, cost: 0, success: 0, expectedProfit: 0, expectedCostPerSuccess: Infinity },
1086
+ { payout: 0, cost: 0, success: 1, expectedProfit: 0, expectedCostPerSuccess: 0 },
1087
+ { payout: 100, cost: 0, success: 1, expectedProfit: 0, expectedCostPerSuccess: 0 },
1088
+ { payout: 100, cost: 100, success: 1, expectedProfit: 0, expectedCostPerSuccess: 100 },
1089
+ { payout: 200, cost: 100, success: 1, expectedProfit: 1, expectedCostPerSuccess: 100 },
1090
+ { payout: 50, cost: 100, success: 1, expectedProfit: -0.5, expectedCostPerSuccess: 100 },
1091
+ ];
1092
+
1093
+ for (const test of boundaryTests) {
1094
+ const metrics = calculateTournamentMetrics(test.payout, test.cost, test.success);
1095
+
1096
+ if (Number.isFinite(test.expectedProfit)) {
1097
+ expect(metrics.netProfitability).toBeCloseTo(test.expectedProfit, 10);
1098
+ } else {
1099
+ expect(metrics.netProfitability).toBe(test.expectedProfit);
1100
+ }
1101
+
1102
+ if (Number.isFinite(test.expectedCostPerSuccess)) {
1103
+ expect(metrics.costPerSuccess).toBeCloseTo(test.expectedCostPerSuccess, 10);
1104
+ } else {
1105
+ expect(metrics.costPerSuccess).toBe(test.expectedCostPerSuccess);
1106
+ }
1107
+
1108
+ expect(metrics.successCount).toBe(test.success);
1109
+ }
1110
+ });
1111
+
1112
+ // **Feature: launch-readiness-checklist, Property 2: Tournament metrics calculation accuracy**
1113
+ test('cost per success scales linearly with total cost', () => {
1114
+ fc.assert(
1115
+ fc.property(
1116
+ fc.float({ min: 1, max: 10000, noNaN: true }), // payoutAmount
1117
+ fc.float({ min: 1, max: 1000, noNaN: true }), // baseCost
1118
+ fc.integer({ min: 1, max: 100 }), // successCount
1119
+ fc.float({ min: 2, max: 10, noNaN: true }), // multiplier
1120
+ (payoutAmount, baseCost, successCount, multiplier) => {
1121
+ const metrics1 = calculateTournamentMetrics(payoutAmount, baseCost, successCount);
1122
+ const metrics2 = calculateTournamentMetrics(payoutAmount, baseCost * multiplier, successCount);
1123
+
1124
+ // Cost per success should scale linearly with total cost
1125
+ expect(metrics2.costPerSuccess).toBeCloseTo(metrics1.costPerSuccess * multiplier, 8);
1126
+ }
1127
+ ),
1128
+ { numRuns: 100 }
1129
+ );
1130
+ });
1131
+
1132
+ // **Feature: launch-readiness-checklist, Property 2: Tournament metrics calculation accuracy**
1133
+ test('cost per success scales inversely with success count', () => {
1134
+ fc.assert(
1135
+ fc.property(
1136
+ fc.float({ min: 1, max: 10000, noNaN: true }), // payoutAmount
1137
+ fc.float({ min: 1, max: 10000, noNaN: true }), // totalCost
1138
+ fc.integer({ min: 1, max: 50 }), // baseSuccessCount
1139
+ fc.integer({ min: 2, max: 10 }), // multiplier
1140
+ (payoutAmount, totalCost, baseSuccessCount, multiplier) => {
1141
+ const metrics1 = calculateTournamentMetrics(payoutAmount, totalCost, baseSuccessCount);
1142
+ const metrics2 = calculateTournamentMetrics(payoutAmount, totalCost, baseSuccessCount * multiplier);
1143
+
1144
+ // Cost per success should scale inversely with success count
1145
+ expect(metrics2.costPerSuccess).toBeCloseTo(metrics1.costPerSuccess / multiplier, 8);
1146
+ }
1147
+ ),
1148
+ { numRuns: 100 }
1149
+ );
1150
+ });
1151
+ });