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,130 @@
1
+ /**
2
+ * Gemini Veo Video Generation Adapter
3
+ *
4
+ * Adapts Google's Veo video generation to work with WAI Championship's agent system.
5
+ *
6
+ * @module agents/adapters/veo
7
+ */
8
+
9
+ import { GoogleGenerativeAI } from '@google/generative-ai';
10
+ import type { AgentConfig } from '../agent.schema.js';
11
+ import type { AgentArtifact } from '../../eval/evaluateOutcome.js';
12
+ import type { ContentCreationPrompt, ContentCreationResult } from './dalle.adapter.js';
13
+
14
+ /**
15
+ * Executes a Veo agent for video generation
16
+ *
17
+ * @param config - WAI agent configuration
18
+ * @param prompt - Content creation prompt
19
+ * @param outcomeId - ID of the outcome being attempted
20
+ * @param attemptNumber - Current attempt number
21
+ * @returns Result of agent execution with artifact if successful
22
+ */
23
+ export async function runVeoAgent(
24
+ config: AgentConfig,
25
+ prompt: ContentCreationPrompt,
26
+ outcomeId: string,
27
+ attemptNumber: number
28
+ ): Promise<ContentCreationResult> {
29
+ try {
30
+ const genAI = new GoogleGenerativeAI(process.env.GOOGLE_API_KEY || '');
31
+
32
+ // Use the latest Veo model
33
+ const model = genAI.getGenerativeModel({ model: 'veo-3.1-generate-preview' });
34
+
35
+ // Build the video generation prompt
36
+ const videoPrompt = `Generate a video: ${prompt.description}${prompt.style ? ` Style: ${prompt.style}` : ''}`;
37
+
38
+ // Note: Veo uses predictLongRunning method, not generateContent
39
+ // This is a placeholder implementation - actual API integration needed
40
+ const result = await model.generateContent(videoPrompt);
41
+ const response = result.response;
42
+ const content = response.text();
43
+
44
+ const artifact: AgentArtifact = {
45
+ agentId: config.id,
46
+ outcomeId,
47
+ attemptNumber,
48
+ content: {
49
+ type: 'video',
50
+ videoUrl: 'https://via.placeholder.com/1280x720/FF4444/FFFFFF?text=Veo+Video+Generated', // Placeholder
51
+ originalPrompt: videoPrompt,
52
+ revisedPrompt: content,
53
+ dimensions: prompt.dimensions || '1280x720',
54
+ model: 'veo-3.1-generate-preview',
55
+ style: prompt.style,
56
+ target: prompt.target,
57
+ generatedText: content,
58
+ duration: '5s', // Default duration
59
+ },
60
+ timestamp: new Date().toISOString(),
61
+ };
62
+
63
+ // Estimate tokens
64
+ const estimatedTokens = Math.ceil((videoPrompt.length + content.length) / 4);
65
+
66
+ return {
67
+ success: true,
68
+ message: 'Veo agent completed successfully',
69
+ tokensUsed: estimatedTokens,
70
+ artifact,
71
+ imageUrl: artifact.content.videoUrl, // Using imageUrl field for video URL
72
+ revisedPrompt: content,
73
+ };
74
+
75
+ } catch (error) {
76
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
77
+ return {
78
+ success: false,
79
+ message: `Veo agent execution failed: ${errorMessage}`,
80
+ tokensUsed: 0,
81
+ error: errorMessage,
82
+ };
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Creates a mock Veo agent run for testing
88
+ *
89
+ * @param config - Agent configuration
90
+ * @param prompt - Content creation prompt
91
+ * @param outcomeId - Outcome ID
92
+ * @param attemptNumber - Attempt number
93
+ * @returns Mock result matching Veo output format
94
+ */
95
+ export function mockVeoAgent(
96
+ config: AgentConfig,
97
+ prompt: ContentCreationPrompt,
98
+ outcomeId: string,
99
+ attemptNumber: number
100
+ ): ContentCreationResult {
101
+ const mockVideoUrl = 'https://via.placeholder.com/1280x720/FF4444/FFFFFF?text=Veo+Mock+Video';
102
+
103
+ const artifact: AgentArtifact = {
104
+ agentId: config.id,
105
+ outcomeId,
106
+ attemptNumber,
107
+ content: {
108
+ type: 'video',
109
+ videoUrl: mockVideoUrl,
110
+ originalPrompt: prompt.description,
111
+ revisedPrompt: `Veo enhanced: ${prompt.description} with cinematic quality and smooth motion`,
112
+ dimensions: prompt.dimensions || '1280x720',
113
+ model: 'veo-3.1-generate-preview',
114
+ style: prompt.style,
115
+ target: prompt.target,
116
+ generatedText: `Created a dynamic video of ${prompt.description} with professional cinematography`,
117
+ duration: '5s',
118
+ },
119
+ timestamp: new Date().toISOString(),
120
+ };
121
+
122
+ return {
123
+ success: true,
124
+ message: 'Mock Veo agent completed',
125
+ tokensUsed: 500,
126
+ artifact,
127
+ imageUrl: mockVideoUrl,
128
+ revisedPrompt: `Veo enhanced: ${prompt.description} with cinematic quality and smooth motion`,
129
+ };
130
+ }
@@ -0,0 +1,379 @@
1
+ /**
2
+ * Property-based tests for Agent Configuration Schema Validation
3
+ *
4
+ * **Feature: earnd-bounty-engine, Property 10: Agent Configuration Schema Validation**
5
+ * **Validates: Requirements 6.5, 8.3, 8.4**
6
+ *
7
+ * Property 10: Agent Configuration Schema Validation
8
+ * *For any* agent configuration object, the agent config validator SHALL return true
9
+ * if and only if all required fields are present with correct types and constraints.
10
+ */
11
+
12
+ import { describe, test, expect } from 'vitest';
13
+ import * as fc from 'fast-check';
14
+ import { readFile } from 'node:fs/promises';
15
+ import { parse } from 'yaml';
16
+ import {
17
+ AgentConfig,
18
+ ModelProvider,
19
+ validateAgentConfig,
20
+ isAgentConfig,
21
+ parseAgentConfigYaml,
22
+ } from './agent.schema.js';
23
+
24
+ /** Valid model providers per schema definition */
25
+ const VALID_MODEL_PROVIDERS: ModelProvider[] = ['claude', 'openai', 'ollama'];
26
+
27
+ /** Custom arbitrary for valid AgentConfig objects */
28
+ const validAgentConfigArb = fc.record({
29
+ id: fc.string({ minLength: 1 }).filter((s) => s.trim().length > 0),
30
+ name: fc.string({ minLength: 1 }).filter((s) => s.trim().length > 0),
31
+ prompt: fc.string({ minLength: 1 }).filter((s) => s.trim().length > 0),
32
+ strategyDescription: fc.string({ minLength: 1 }).filter((s) => s.trim().length > 0),
33
+ toolAccess: fc.array(fc.string()),
34
+ costCeiling: fc.integer({ min: 1, max: 1000000 }),
35
+ modelProvider: fc.constantFrom(...VALID_MODEL_PROVIDERS),
36
+ modelId: fc.string({ minLength: 1 }).filter((s) => s.trim().length > 0),
37
+ });
38
+
39
+ describe('Agent Configuration Schema Validation - Property Tests', () => {
40
+ // **Feature: earnd-bounty-engine, Property 10: Agent Configuration Schema Validation**
41
+ test('valid agent configurations pass validation', () => {
42
+ fc.assert(
43
+ fc.property(validAgentConfigArb, (config) => {
44
+ const result = validateAgentConfig(config);
45
+ expect(result.valid).toBe(true);
46
+ expect(result.errors).toHaveLength(0);
47
+ }),
48
+ { numRuns: 100 }
49
+ );
50
+ });
51
+
52
+ // **Feature: earnd-bounty-engine, Property 10: Agent Configuration Schema Validation**
53
+ test('configurations with empty id are invalid', () => {
54
+ fc.assert(
55
+ fc.property(
56
+ validAgentConfigArb.map((config) => ({ ...config, id: '' })),
57
+ (config) => {
58
+ const result = validateAgentConfig(config);
59
+ expect(result.valid).toBe(false);
60
+ expect(result.errors).toContain('Agent config "id" must not be empty');
61
+ }
62
+ ),
63
+ { numRuns: 100 }
64
+ );
65
+ });
66
+
67
+ // **Feature: earnd-bounty-engine, Property 10: Agent Configuration Schema Validation**
68
+ test('configurations with empty name are invalid', () => {
69
+ fc.assert(
70
+ fc.property(
71
+ validAgentConfigArb.map((config) => ({ ...config, name: '' })),
72
+ (config) => {
73
+ const result = validateAgentConfig(config);
74
+ expect(result.valid).toBe(false);
75
+ expect(result.errors).toContain('Agent config "name" must not be empty');
76
+ }
77
+ ),
78
+ { numRuns: 100 }
79
+ );
80
+ });
81
+
82
+ // **Feature: earnd-bounty-engine, Property 10: Agent Configuration Schema Validation**
83
+ test('configurations with empty prompt are invalid', () => {
84
+ fc.assert(
85
+ fc.property(
86
+ validAgentConfigArb.map((config) => ({ ...config, prompt: '' })),
87
+ (config) => {
88
+ const result = validateAgentConfig(config);
89
+ expect(result.valid).toBe(false);
90
+ expect(result.errors).toContain('Agent config "prompt" must not be empty');
91
+ }
92
+ ),
93
+ { numRuns: 100 }
94
+ );
95
+ });
96
+
97
+ // **Feature: earnd-bounty-engine, Property 10: Agent Configuration Schema Validation**
98
+ test('configurations with empty strategyDescription are invalid', () => {
99
+ fc.assert(
100
+ fc.property(
101
+ validAgentConfigArb.map((config) => ({ ...config, strategyDescription: '' })),
102
+ (config) => {
103
+ const result = validateAgentConfig(config);
104
+ expect(result.valid).toBe(false);
105
+ expect(result.errors).toContain('Agent config "strategyDescription" must not be empty');
106
+ }
107
+ ),
108
+ { numRuns: 100 }
109
+ );
110
+ });
111
+
112
+ // **Feature: earnd-bounty-engine, Property 10: Agent Configuration Schema Validation**
113
+ test('configurations with non-positive costCeiling are invalid', () => {
114
+ fc.assert(
115
+ fc.property(
116
+ fc.integer({ min: -1000, max: 0 }),
117
+ validAgentConfigArb,
118
+ (invalidCeiling, config) => {
119
+ const invalidConfig = { ...config, costCeiling: invalidCeiling };
120
+ const result = validateAgentConfig(invalidConfig);
121
+ expect(result.valid).toBe(false);
122
+ expect(result.errors).toContain('Agent config "costCeiling" must be positive');
123
+ }
124
+ ),
125
+ { numRuns: 100 }
126
+ );
127
+ });
128
+
129
+ // **Feature: earnd-bounty-engine, Property 10: Agent Configuration Schema Validation**
130
+ test('configurations with invalid modelProvider are invalid', () => {
131
+ const invalidProviders = ['gemini', 'gpt', 'llama', 'invalid', ''];
132
+
133
+ fc.assert(
134
+ fc.property(
135
+ fc.constantFrom(...invalidProviders),
136
+ validAgentConfigArb,
137
+ (invalidProvider, config) => {
138
+ const invalidConfig = { ...config, modelProvider: invalidProvider };
139
+ const result = validateAgentConfig(invalidConfig);
140
+ expect(result.valid).toBe(false);
141
+ expect(result.errors).toContain('Agent config "modelProvider" must be "claude", "openai", or "ollama"');
142
+ }
143
+ ),
144
+ { numRuns: 100 }
145
+ );
146
+ });
147
+
148
+ // **Feature: earnd-bounty-engine, Property 10: Agent Configuration Schema Validation**
149
+ test('configurations with empty modelId are invalid', () => {
150
+ fc.assert(
151
+ fc.property(
152
+ validAgentConfigArb.map((config) => ({ ...config, modelId: '' })),
153
+ (config) => {
154
+ const result = validateAgentConfig(config);
155
+ expect(result.valid).toBe(false);
156
+ expect(result.errors).toContain('Agent config "modelId" must not be empty');
157
+ }
158
+ ),
159
+ { numRuns: 100 }
160
+ );
161
+ });
162
+
163
+ // **Feature: earnd-bounty-engine, Property 10: Agent Configuration Schema Validation**
164
+ test('configurations with non-array toolAccess are invalid', () => {
165
+ const nonArrayValues = [null, undefined, 'string', 123, {}, true];
166
+
167
+ fc.assert(
168
+ fc.property(
169
+ fc.constantFrom(...nonArrayValues),
170
+ validAgentConfigArb,
171
+ (invalidToolAccess, config) => {
172
+ const invalidConfig = { ...config, toolAccess: invalidToolAccess };
173
+ const result = validateAgentConfig(invalidConfig);
174
+ expect(result.valid).toBe(false);
175
+ expect(result.errors).toContain('Agent config must have an array "toolAccess"');
176
+ }
177
+ ),
178
+ { numRuns: 100 }
179
+ );
180
+ });
181
+
182
+ // **Feature: earnd-bounty-engine, Property 10: Agent Configuration Schema Validation**
183
+ test('configurations with non-string tools in toolAccess are invalid', () => {
184
+ fc.assert(
185
+ fc.property(
186
+ fc.array(fc.oneof(fc.integer(), fc.boolean(), fc.object())),
187
+ validAgentConfigArb,
188
+ (invalidTools, config) => {
189
+ if (invalidTools.length === 0) return; // Skip empty arrays
190
+
191
+ const invalidConfig = { ...config, toolAccess: invalidTools };
192
+ const result = validateAgentConfig(invalidConfig);
193
+ expect(result.valid).toBe(false);
194
+ expect(result.errors.some(error => error.includes('must be a string'))).toBe(true);
195
+ }
196
+ ),
197
+ { numRuns: 100 }
198
+ );
199
+ });
200
+
201
+ // **Feature: earnd-bounty-engine, Property 10: Agent Configuration Schema Validation**
202
+ test('non-object values are invalid', () => {
203
+ const nonObjectValues = [null, undefined, 'string', 123, [], true, false];
204
+
205
+ fc.assert(
206
+ fc.property(
207
+ fc.constantFrom(...nonObjectValues),
208
+ (nonObject) => {
209
+ const result = validateAgentConfig(nonObject);
210
+ expect(result.valid).toBe(false);
211
+ expect(result.errors).toContain('Agent config must be an object');
212
+ }
213
+ ),
214
+ { numRuns: 100 }
215
+ );
216
+ });
217
+
218
+ // **Feature: earnd-bounty-engine, Property 10: Agent Configuration Schema Validation**
219
+ test('validation is deterministic - same input produces same output', () => {
220
+ fc.assert(
221
+ fc.property(validAgentConfigArb, (config) => {
222
+ const result1 = validateAgentConfig(config);
223
+ const result2 = validateAgentConfig(config);
224
+ expect(result1.valid).toBe(result2.valid);
225
+ expect(result1.errors).toEqual(result2.errors);
226
+ }),
227
+ { numRuns: 100 }
228
+ );
229
+ });
230
+
231
+ // **Feature: earnd-bounty-engine, Property 10: Agent Configuration Schema Validation**
232
+ test('validation result structure is correct', () => {
233
+ fc.assert(
234
+ fc.property(fc.anything(), (input) => {
235
+ const result = validateAgentConfig(input);
236
+ // Result must have valid boolean and errors array
237
+ expect(typeof result.valid).toBe('boolean');
238
+ expect(Array.isArray(result.errors)).toBe(true);
239
+ // If valid, errors should be empty; if invalid, errors should have content
240
+ if (result.valid) {
241
+ expect(result.errors).toHaveLength(0);
242
+ } else {
243
+ expect(result.errors.length).toBeGreaterThan(0);
244
+ }
245
+ }),
246
+ { numRuns: 100 }
247
+ );
248
+ });
249
+
250
+ // **Feature: earnd-bounty-engine, Property 10: Agent Configuration Schema Validation**
251
+ test('isAgentConfig type guard matches validateAgentConfig', () => {
252
+ fc.assert(
253
+ fc.property(fc.anything(), (input) => {
254
+ const validationResult = validateAgentConfig(input);
255
+ const typeGuardResult = isAgentConfig(input);
256
+ expect(typeGuardResult).toBe(validationResult.valid);
257
+ }),
258
+ { numRuns: 100 }
259
+ );
260
+ });
261
+
262
+ // **Feature: earnd-bounty-engine, Property 10: Agent Configuration Schema Validation**
263
+ test('configurations with infinite or NaN costCeiling are invalid', () => {
264
+ // Test positive infinity and NaN (which should trigger finite number check)
265
+ fc.assert(
266
+ fc.property(
267
+ fc.constantFrom(Infinity, NaN),
268
+ validAgentConfigArb,
269
+ (invalidNumber, config) => {
270
+ const invalidConfig = { ...config, costCeiling: invalidNumber };
271
+ const result = validateAgentConfig(invalidConfig);
272
+ expect(result.valid).toBe(false);
273
+ expect(result.errors).toContain('Agent config "costCeiling" must be a finite number');
274
+ }
275
+ ),
276
+ { numRuns: 100 }
277
+ );
278
+
279
+ // Test negative infinity (which should trigger positive check first)
280
+ const negInfinityConfig = {
281
+ id: 'test',
282
+ name: 'test',
283
+ prompt: 'test',
284
+ strategyDescription: 'test',
285
+ toolAccess: [],
286
+ costCeiling: -Infinity,
287
+ modelProvider: 'claude' as const,
288
+ modelId: 'test'
289
+ };
290
+ const result = validateAgentConfig(negInfinityConfig);
291
+ expect(result.valid).toBe(false);
292
+ expect(result.errors).toContain('Agent config "costCeiling" must be positive');
293
+ });
294
+
295
+ // **Feature: earnd-bounty-engine, Property 10: Agent Configuration Schema Validation**
296
+ test('configurations with whitespace-only strings are invalid', () => {
297
+ const whitespaceStrings = [' ', '\t\t', '\n\n', ' \t\n '];
298
+
299
+ fc.assert(
300
+ fc.property(
301
+ fc.constantFrom(...whitespaceStrings),
302
+ validAgentConfigArb,
303
+ (whitespace, config) => {
304
+ // Test each string field with whitespace
305
+ const fields = ['id', 'name', 'prompt', 'strategyDescription', 'modelId'];
306
+
307
+ fields.forEach(field => {
308
+ const invalidConfig = { ...config, [field]: whitespace };
309
+ const result = validateAgentConfig(invalidConfig);
310
+ expect(result.valid).toBe(false);
311
+ expect(result.errors.some(error => error.includes(`"${field}" must not be empty`))).toBe(true);
312
+ });
313
+ }
314
+ ),
315
+ { numRuns: 100 }
316
+ );
317
+ });
318
+ });
319
+
320
+ describe('Tournament Agent Configuration Files - Property Tests', () => {
321
+ const tournamentAgentFiles = [
322
+ 'src/agents/configs/tournament-coderabbit.yaml',
323
+ 'src/agents/configs/tournament-claude-sonnet.yaml',
324
+ 'src/agents/configs/tournament-gpt4o.yaml',
325
+ 'src/agents/configs/tournament-llama3.yaml',
326
+ ];
327
+
328
+ // **Feature: earnd-bounty-engine, Property 10: Agent Configuration Schema Validation**
329
+ test('all tournament agent configurations are valid', async () => {
330
+ for (const filePath of tournamentAgentFiles) {
331
+ const yamlContent = await readFile(filePath, 'utf-8');
332
+ const config = parse(yamlContent);
333
+
334
+ const result = validateAgentConfig(config);
335
+ expect(result.valid).toBe(true);
336
+ expect(result.errors).toHaveLength(0);
337
+
338
+ // Verify it's a proper AgentConfig
339
+ expect(isAgentConfig(config)).toBe(true);
340
+ }
341
+ });
342
+
343
+ // **Feature: earnd-bounty-engine, Property 10: Agent Configuration Schema Validation**
344
+ test('tournament agents have required tournament-specific properties', async () => {
345
+ for (const filePath of tournamentAgentFiles) {
346
+ const yamlContent = await readFile(filePath, 'utf-8');
347
+ const config = parse(yamlContent) as AgentConfig;
348
+
349
+ // Tournament agents should have specific naming pattern
350
+ expect(config.id).toMatch(/^tournament-/);
351
+
352
+ // Should have reasonable cost ceilings
353
+ expect(config.costCeiling).toBeGreaterThan(0);
354
+ expect(config.costCeiling).toBeLessThanOrEqual(15000);
355
+
356
+ // Should have valid model providers
357
+ expect(VALID_MODEL_PROVIDERS).toContain(config.modelProvider);
358
+
359
+ // Should have non-empty strategy descriptions
360
+ expect(config.strategyDescription.trim().length).toBeGreaterThan(0);
361
+
362
+ // Should have tool access arrays
363
+ expect(Array.isArray(config.toolAccess)).toBe(true);
364
+ }
365
+ });
366
+
367
+ // **Feature: earnd-bounty-engine, Property 10: Agent Configuration Schema Validation**
368
+ test('parseAgentConfigYaml works correctly for tournament configs', async () => {
369
+ for (const filePath of tournamentAgentFiles) {
370
+ const yamlContent = await readFile(filePath, 'utf-8');
371
+ const { config, validation } = await parseAgentConfigYaml(yamlContent);
372
+
373
+ expect(validation.valid).toBe(true);
374
+ expect(validation.errors).toHaveLength(0);
375
+ expect(config).not.toBeNull();
376
+ expect(isAgentConfig(config)).toBe(true);
377
+ }
378
+ });
379
+ });
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Agent Schema Tests
3
+ *
4
+ * Unit tests for agent configuration validation.
5
+ */
6
+
7
+ import { describe, it, expect } from 'vitest';
8
+ import { readFile } from 'node:fs/promises';
9
+ import { parse } from 'yaml';
10
+ import {
11
+ AgentConfig,
12
+ validateAgentConfig,
13
+ isAgentConfig,
14
+ } from './agent.schema.js';
15
+
16
+ describe('validateAgentConfig', () => {
17
+ it('accepts a valid agent config', () => {
18
+ const config: AgentConfig = {
19
+ id: 'test-agent-001',
20
+ name: 'Test Agent',
21
+ prompt: 'You are a test agent.',
22
+ strategyDescription: 'Test strategy',
23
+ toolAccess: ['tool1', 'tool2'],
24
+ costCeiling: 10000,
25
+ modelProvider: 'claude',
26
+ modelId: 'claude-3-sonnet-20240229',
27
+ };
28
+
29
+ const result = validateAgentConfig(config);
30
+ expect(result.valid).toBe(true);
31
+ expect(result.errors).toHaveLength(0);
32
+ });
33
+
34
+ it('rejects non-object values', () => {
35
+ expect(validateAgentConfig(null).valid).toBe(false);
36
+ expect(validateAgentConfig(undefined).valid).toBe(false);
37
+ expect(validateAgentConfig('string').valid).toBe(false);
38
+ expect(validateAgentConfig(123).valid).toBe(false);
39
+ });
40
+
41
+ it('rejects empty id', () => {
42
+ const config = {
43
+ id: '',
44
+ name: 'Test',
45
+ prompt: 'Test',
46
+ strategyDescription: 'Test',
47
+ toolAccess: [],
48
+ costCeiling: 100,
49
+ modelProvider: 'claude',
50
+ modelId: 'test',
51
+ };
52
+
53
+ const result = validateAgentConfig(config);
54
+ expect(result.valid).toBe(false);
55
+ expect(result.errors).toContain('Agent config "id" must not be empty');
56
+ });
57
+
58
+ it('rejects invalid modelProvider', () => {
59
+ const config = {
60
+ id: 'test',
61
+ name: 'Test',
62
+ prompt: 'Test',
63
+ strategyDescription: 'Test',
64
+ toolAccess: [],
65
+ costCeiling: 100,
66
+ modelProvider: 'invalid',
67
+ modelId: 'test',
68
+ };
69
+
70
+ const result = validateAgentConfig(config);
71
+ expect(result.valid).toBe(false);
72
+ expect(result.errors).toContain(
73
+ 'Agent config "modelProvider" must be "claude", "openai", or "ollama"'
74
+ );
75
+ });
76
+
77
+ it('rejects negative costCeiling', () => {
78
+ const config = {
79
+ id: 'test',
80
+ name: 'Test',
81
+ prompt: 'Test',
82
+ strategyDescription: 'Test',
83
+ toolAccess: [],
84
+ costCeiling: -100,
85
+ modelProvider: 'claude',
86
+ modelId: 'test',
87
+ };
88
+
89
+ const result = validateAgentConfig(config);
90
+ expect(result.valid).toBe(false);
91
+ expect(result.errors).toContain(
92
+ 'Agent config "costCeiling" must be positive'
93
+ );
94
+ });
95
+ });
96
+
97
+ describe('isAgentConfig', () => {
98
+ it('returns true for valid config', () => {
99
+ const config: AgentConfig = {
100
+ id: 'test-agent-001',
101
+ name: 'Test Agent',
102
+ prompt: 'You are a test agent.',
103
+ strategyDescription: 'Test strategy',
104
+ toolAccess: [],
105
+ costCeiling: 10000,
106
+ modelProvider: 'openai',
107
+ modelId: 'gpt-4-turbo-preview',
108
+ };
109
+
110
+ expect(isAgentConfig(config)).toBe(true);
111
+ });
112
+
113
+ it('returns false for invalid config', () => {
114
+ expect(isAgentConfig(null)).toBe(false);
115
+ expect(isAgentConfig({})).toBe(false);
116
+ });
117
+ });
118
+
119
+ describe('sales_agent.yaml', () => {
120
+ it('is a valid agent configuration', async () => {
121
+ const yamlContent = await readFile(
122
+ './src/agents/sales_agent.yaml',
123
+ 'utf-8'
124
+ );
125
+ const config = parse(yamlContent);
126
+
127
+ const result = validateAgentConfig(config);
128
+ expect(result.valid).toBe(true);
129
+ expect(result.errors).toHaveLength(0);
130
+ });
131
+
132
+ it('has all required fields', async () => {
133
+ const yamlContent = await readFile(
134
+ './src/agents/sales_agent.yaml',
135
+ 'utf-8'
136
+ );
137
+ const config = parse(yamlContent) as AgentConfig;
138
+
139
+ expect(config.id).toBe('sales-agent-001');
140
+ expect(config.name).toBe('Sales Qualifier Agent');
141
+ expect(config.prompt).toContain('sales qualification');
142
+ expect(config.strategyDescription).toContain('consultative');
143
+ expect(config.toolAccess).toContain('email_lookup');
144
+ expect(config.costCeiling).toBe(10000);
145
+ expect(config.modelProvider).toBe('claude');
146
+ expect(config.modelId).toBe('claude-3-sonnet-20240229');
147
+ });
148
+ });