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,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
|
+
});
|