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,1118 @@
1
+ /**
2
+ * Property-based tests for Buying Intent Validation
3
+ *
4
+ * **Feature: earnd-bounty-engine, Property 14: Buying Intent Validation**
5
+ * **Validates: Requirements 8.1**
6
+ *
7
+ * Property 14: Buying Intent Validation
8
+ * *For any* message string, the buying intent validator SHALL return true
9
+ * if and only if the message contains at least one of the keywords:
10
+ * "pricing", "demo", "next steps".
11
+ */
12
+
13
+ import { describe, test, expect } from 'vitest';
14
+ import * as fc from 'fast-check';
15
+ import { validateBuyingIntent, validateCompanySize, validateRole } from './validators.js';
16
+
17
+ /** The standard buying intent keywords per Requirements 8.1 */
18
+ const BUYING_INTENT_KEYWORDS = ['pricing', 'demo', 'next steps'];
19
+
20
+ describe('Buying Intent Validation - Property Tests', () => {
21
+ // **Feature: earnd-bounty-engine, Property 14: Buying Intent Validation**
22
+ test('messages containing at least one keyword are valid', () => {
23
+ fc.assert(
24
+ fc.property(
25
+ fc.string(),
26
+ fc.constantFrom(...BUYING_INTENT_KEYWORDS),
27
+ fc.string(),
28
+ (prefix, keyword, suffix) => {
29
+ // Construct a message that definitely contains the keyword
30
+ const message = `${prefix}${keyword}${suffix}`;
31
+ const result = validateBuyingIntent(message, BUYING_INTENT_KEYWORDS);
32
+ expect(result.valid).toBe(true);
33
+ expect(result.errors).toHaveLength(0);
34
+ }
35
+ ),
36
+ { numRuns: 100 }
37
+ );
38
+ });
39
+
40
+ // **Feature: earnd-bounty-engine, Property 14: Buying Intent Validation**
41
+ test('messages without any keywords are invalid', () => {
42
+ // Generate strings that definitely don't contain any keywords
43
+ const noKeywordStringArb = fc
44
+ .string()
45
+ .filter((s) => {
46
+ const lower = s.toLowerCase();
47
+ return !BUYING_INTENT_KEYWORDS.some((kw) => lower.includes(kw.toLowerCase()));
48
+ });
49
+
50
+ fc.assert(
51
+ fc.property(noKeywordStringArb, (message) => {
52
+ const result = validateBuyingIntent(message, BUYING_INTENT_KEYWORDS);
53
+ expect(result.valid).toBe(false);
54
+ expect(result.errors.length).toBeGreaterThan(0);
55
+ }),
56
+ { numRuns: 100 }
57
+ );
58
+ });
59
+
60
+ // **Feature: earnd-bounty-engine, Property 14: Buying Intent Validation**
61
+ test('keyword matching is case-insensitive', () => {
62
+ // Generate case variations of keywords
63
+ const caseVariationArb = fc
64
+ .constantFrom(...BUYING_INTENT_KEYWORDS)
65
+ .chain((keyword) =>
66
+ fc.array(fc.boolean(), { minLength: keyword.length, maxLength: keyword.length }).map(
67
+ (upperFlags) =>
68
+ keyword
69
+ .split('')
70
+ .map((char, i) => (upperFlags[i] ? char.toUpperCase() : char.toLowerCase()))
71
+ .join('')
72
+ )
73
+ );
74
+
75
+ fc.assert(
76
+ fc.property(fc.string(), caseVariationArb, fc.string(), (prefix, keyword, suffix) => {
77
+ const message = `${prefix}${keyword}${suffix}`;
78
+ const result = validateBuyingIntent(message, BUYING_INTENT_KEYWORDS);
79
+ expect(result.valid).toBe(true);
80
+ expect(result.errors).toHaveLength(0);
81
+ }),
82
+ { numRuns: 100 }
83
+ );
84
+ });
85
+
86
+ // **Feature: earnd-bounty-engine, Property 14: Buying Intent Validation**
87
+ test('validation is deterministic - same input produces same output', () => {
88
+ fc.assert(
89
+ fc.property(fc.string(), (message) => {
90
+ const result1 = validateBuyingIntent(message, BUYING_INTENT_KEYWORDS);
91
+ const result2 = validateBuyingIntent(message, BUYING_INTENT_KEYWORDS);
92
+ expect(result1.valid).toBe(result2.valid);
93
+ expect(result1.errors).toEqual(result2.errors);
94
+ }),
95
+ { numRuns: 100 }
96
+ );
97
+ });
98
+
99
+ // **Feature: earnd-bounty-engine, Property 14: Buying Intent Validation**
100
+ test('empty message without keywords is invalid', () => {
101
+ const result = validateBuyingIntent('', BUYING_INTENT_KEYWORDS);
102
+ expect(result.valid).toBe(false);
103
+ expect(result.errors.length).toBeGreaterThan(0);
104
+ });
105
+
106
+ // **Feature: earnd-bounty-engine, Property 14: Buying Intent Validation**
107
+ test('validation result structure is correct', () => {
108
+ fc.assert(
109
+ fc.property(fc.string(), (message) => {
110
+ const result = validateBuyingIntent(message, BUYING_INTENT_KEYWORDS);
111
+ // Result must have valid boolean and errors array
112
+ expect(typeof result.valid).toBe('boolean');
113
+ expect(Array.isArray(result.errors)).toBe(true);
114
+ // If valid, errors should be empty; if invalid, errors should have content
115
+ if (result.valid) {
116
+ expect(result.errors).toHaveLength(0);
117
+ } else {
118
+ expect(result.errors.length).toBeGreaterThan(0);
119
+ }
120
+ }),
121
+ { numRuns: 100 }
122
+ );
123
+ });
124
+ });
125
+
126
+
127
+ /**
128
+ * Property-based tests for Company Size Validation
129
+ *
130
+ * **Feature: earnd-bounty-engine, Property 15: Company Size Validation**
131
+ * **Validates: Requirements 8.2**
132
+ *
133
+ * Property 15: Company Size Validation
134
+ * *For any* company size value, the company size validator SHALL return true
135
+ * if and only if the value is greater than or equal to 50.
136
+ */
137
+
138
+ /** The minimum company size threshold per Requirements 8.2 */
139
+ const MINIMUM_COMPANY_SIZE = 50;
140
+
141
+ describe('Company Size Validation - Property Tests', () => {
142
+ // **Feature: earnd-bounty-engine, Property 15: Company Size Validation**
143
+ test('company sizes >= minimum are valid', () => {
144
+ fc.assert(
145
+ fc.property(
146
+ fc.integer({ min: MINIMUM_COMPANY_SIZE, max: 1000000 }),
147
+ (size) => {
148
+ const result = validateCompanySize(size, MINIMUM_COMPANY_SIZE);
149
+ expect(result.valid).toBe(true);
150
+ expect(result.errors).toHaveLength(0);
151
+ }
152
+ ),
153
+ { numRuns: 100 }
154
+ );
155
+ });
156
+
157
+ // **Feature: earnd-bounty-engine, Property 15: Company Size Validation**
158
+ test('company sizes < minimum are invalid', () => {
159
+ fc.assert(
160
+ fc.property(
161
+ fc.integer({ min: 0, max: MINIMUM_COMPANY_SIZE - 1 }),
162
+ (size) => {
163
+ const result = validateCompanySize(size, MINIMUM_COMPANY_SIZE);
164
+ expect(result.valid).toBe(false);
165
+ expect(result.errors.length).toBeGreaterThan(0);
166
+ }
167
+ ),
168
+ { numRuns: 100 }
169
+ );
170
+ });
171
+
172
+ // **Feature: earnd-bounty-engine, Property 15: Company Size Validation**
173
+ test('boundary value: exactly minimum is valid', () => {
174
+ const result = validateCompanySize(MINIMUM_COMPANY_SIZE, MINIMUM_COMPANY_SIZE);
175
+ expect(result.valid).toBe(true);
176
+ expect(result.errors).toHaveLength(0);
177
+ });
178
+
179
+ // **Feature: earnd-bounty-engine, Property 15: Company Size Validation**
180
+ test('boundary value: one below minimum is invalid', () => {
181
+ const result = validateCompanySize(MINIMUM_COMPANY_SIZE - 1, MINIMUM_COMPANY_SIZE);
182
+ expect(result.valid).toBe(false);
183
+ expect(result.errors.length).toBeGreaterThan(0);
184
+ });
185
+
186
+ // **Feature: earnd-bounty-engine, Property 15: Company Size Validation**
187
+ test('validation is deterministic - same input produces same output', () => {
188
+ fc.assert(
189
+ fc.property(
190
+ fc.integer({ min: 0, max: 1000000 }),
191
+ (size) => {
192
+ const result1 = validateCompanySize(size, MINIMUM_COMPANY_SIZE);
193
+ const result2 = validateCompanySize(size, MINIMUM_COMPANY_SIZE);
194
+ expect(result1.valid).toBe(result2.valid);
195
+ expect(result1.errors).toEqual(result2.errors);
196
+ }
197
+ ),
198
+ { numRuns: 100 }
199
+ );
200
+ });
201
+
202
+ // **Feature: earnd-bounty-engine, Property 15: Company Size Validation**
203
+ test('validation result structure is correct', () => {
204
+ fc.assert(
205
+ fc.property(
206
+ fc.integer({ min: 0, max: 1000000 }),
207
+ (size) => {
208
+ const result = validateCompanySize(size, MINIMUM_COMPANY_SIZE);
209
+ // Result must have valid boolean and errors array
210
+ expect(typeof result.valid).toBe('boolean');
211
+ expect(Array.isArray(result.errors)).toBe(true);
212
+ // If valid, errors should be empty; if invalid, errors should have content
213
+ if (result.valid) {
214
+ expect(result.errors).toHaveLength(0);
215
+ } else {
216
+ expect(result.errors.length).toBeGreaterThan(0);
217
+ }
218
+ }
219
+ ),
220
+ { numRuns: 100 }
221
+ );
222
+ });
223
+
224
+ // **Feature: earnd-bounty-engine, Property 15: Company Size Validation**
225
+ test('works with different minimum thresholds', () => {
226
+ fc.assert(
227
+ fc.property(
228
+ fc.integer({ min: 1, max: 10000 }),
229
+ fc.integer({ min: 0, max: 100000 }),
230
+ (minimum, size) => {
231
+ const result = validateCompanySize(size, minimum);
232
+ // The property: valid iff size >= minimum
233
+ expect(result.valid).toBe(size >= minimum);
234
+ }
235
+ ),
236
+ { numRuns: 100 }
237
+ );
238
+ });
239
+ });
240
+
241
+
242
+ /**
243
+ * Property-based tests for Role Exclusion Validation
244
+ *
245
+ * **Feature: earnd-bounty-engine, Property 16: Role Exclusion Validation**
246
+ * **Validates: Requirements 8.3**
247
+ *
248
+ * Property 16: Role Exclusion Validation
249
+ * *For any* role string, the role validator SHALL return true
250
+ * if and only if the role (case-insensitive) is not "intern" or "student".
251
+ */
252
+
253
+ /** The excluded roles per Requirements 8.3 */
254
+ const EXCLUDED_ROLES = ['intern', 'student'];
255
+
256
+ describe('Role Exclusion Validation - Property Tests', () => {
257
+ // **Feature: earnd-bounty-engine, Property 16: Role Exclusion Validation**
258
+ test('roles not in excluded list are valid', () => {
259
+ // Generate role strings that are not excluded
260
+ const validRoleArb = fc
261
+ .string({ minLength: 1 })
262
+ .filter((role) => {
263
+ const lowerRole = role.toLowerCase().trim();
264
+ return !EXCLUDED_ROLES.some((excluded) => lowerRole === excluded.toLowerCase());
265
+ });
266
+
267
+ fc.assert(
268
+ fc.property(validRoleArb, (role) => {
269
+ const result = validateRole(role, EXCLUDED_ROLES);
270
+ expect(result.valid).toBe(true);
271
+ expect(result.errors).toHaveLength(0);
272
+ }),
273
+ { numRuns: 100 }
274
+ );
275
+ });
276
+
277
+ // **Feature: earnd-bounty-engine, Property 16: Role Exclusion Validation**
278
+ test('excluded roles are invalid', () => {
279
+ fc.assert(
280
+ fc.property(
281
+ fc.constantFrom(...EXCLUDED_ROLES),
282
+ (role) => {
283
+ const result = validateRole(role, EXCLUDED_ROLES);
284
+ expect(result.valid).toBe(false);
285
+ expect(result.errors.length).toBeGreaterThan(0);
286
+ }
287
+ ),
288
+ { numRuns: 100 }
289
+ );
290
+ });
291
+
292
+ // **Feature: earnd-bounty-engine, Property 16: Role Exclusion Validation**
293
+ test('role matching is case-insensitive', () => {
294
+ // Generate case variations of excluded roles
295
+ const caseVariationArb = fc
296
+ .constantFrom(...EXCLUDED_ROLES)
297
+ .chain((role) =>
298
+ fc.array(fc.boolean(), { minLength: role.length, maxLength: role.length }).map(
299
+ (upperFlags) =>
300
+ role
301
+ .split('')
302
+ .map((char, i) => (upperFlags[i] ? char.toUpperCase() : char.toLowerCase()))
303
+ .join('')
304
+ )
305
+ );
306
+
307
+ fc.assert(
308
+ fc.property(caseVariationArb, (role) => {
309
+ const result = validateRole(role, EXCLUDED_ROLES);
310
+ expect(result.valid).toBe(false);
311
+ expect(result.errors.length).toBeGreaterThan(0);
312
+ }),
313
+ { numRuns: 100 }
314
+ );
315
+ });
316
+
317
+ // **Feature: earnd-bounty-engine, Property 16: Role Exclusion Validation**
318
+ test('validation is deterministic - same input produces same output', () => {
319
+ fc.assert(
320
+ fc.property(fc.string(), (role) => {
321
+ const result1 = validateRole(role, EXCLUDED_ROLES);
322
+ const result2 = validateRole(role, EXCLUDED_ROLES);
323
+ expect(result1.valid).toBe(result2.valid);
324
+ expect(result1.errors).toEqual(result2.errors);
325
+ }),
326
+ { numRuns: 100 }
327
+ );
328
+ });
329
+
330
+ // **Feature: earnd-bounty-engine, Property 16: Role Exclusion Validation**
331
+ test('validation result structure is correct', () => {
332
+ fc.assert(
333
+ fc.property(fc.string(), (role) => {
334
+ const result = validateRole(role, EXCLUDED_ROLES);
335
+ // Result must have valid boolean and errors array
336
+ expect(typeof result.valid).toBe('boolean');
337
+ expect(Array.isArray(result.errors)).toBe(true);
338
+ // If valid, errors should be empty; if invalid, errors should have content
339
+ if (result.valid) {
340
+ expect(result.errors).toHaveLength(0);
341
+ } else {
342
+ expect(result.errors.length).toBeGreaterThan(0);
343
+ }
344
+ }),
345
+ { numRuns: 100 }
346
+ );
347
+ });
348
+
349
+ // **Feature: earnd-bounty-engine, Property 16: Role Exclusion Validation**
350
+ test('common valid roles pass validation', () => {
351
+ const validRoles = [
352
+ 'Engineering Manager',
353
+ 'Software Engineer',
354
+ 'CEO',
355
+ 'CTO',
356
+ 'Product Manager',
357
+ 'Director',
358
+ 'VP of Sales',
359
+ 'Founder',
360
+ ];
361
+
362
+ fc.assert(
363
+ fc.property(fc.constantFrom(...validRoles), (role) => {
364
+ const result = validateRole(role, EXCLUDED_ROLES);
365
+ expect(result.valid).toBe(true);
366
+ expect(result.errors).toHaveLength(0);
367
+ }),
368
+ { numRuns: 100 }
369
+ );
370
+ });
371
+
372
+ // **Feature: earnd-bounty-engine, Property 16: Role Exclusion Validation**
373
+ test('roles containing excluded words as substrings are valid', () => {
374
+ // Roles that contain "intern" or "student" as substrings but are not exact matches
375
+ const substringRoles = [
376
+ 'Internal Affairs',
377
+ 'International Sales',
378
+ 'Student Affairs Director',
379
+ 'Internship Coordinator',
380
+ ];
381
+
382
+ fc.assert(
383
+ fc.property(fc.constantFrom(...substringRoles), (role) => {
384
+ const result = validateRole(role, EXCLUDED_ROLES);
385
+ expect(result.valid).toBe(true);
386
+ expect(result.errors).toHaveLength(0);
387
+ }),
388
+ { numRuns: 100 }
389
+ );
390
+ });
391
+
392
+ // **Feature: earnd-bounty-engine, Property 16: Role Exclusion Validation**
393
+ test('whitespace-trimmed excluded roles are invalid', () => {
394
+ // Excluded roles with leading/trailing whitespace
395
+ const paddedExcludedRoles = [
396
+ ' intern',
397
+ 'intern ',
398
+ ' intern ',
399
+ ' student',
400
+ 'student ',
401
+ ' student ',
402
+ ];
403
+
404
+ fc.assert(
405
+ fc.property(fc.constantFrom(...paddedExcludedRoles), (role) => {
406
+ const result = validateRole(role, EXCLUDED_ROLES);
407
+ expect(result.valid).toBe(false);
408
+ expect(result.errors.length).toBeGreaterThan(0);
409
+ }),
410
+ { numRuns: 100 }
411
+ );
412
+ });
413
+ });
414
+
415
+
416
+ /**
417
+ * Property-based tests for Message Length Validation
418
+ *
419
+ * **Feature: earnd-bounty-engine, Property 17: Message Length Validation**
420
+ * **Validates: Requirements 8.4**
421
+ *
422
+ * Property 17: Message Length Validation
423
+ * *For any* message string, the message length validator SHALL return true
424
+ * if and only if the word count is greater than or equal to 20.
425
+ */
426
+
427
+ import { validateMessageLength } from './validators.js';
428
+
429
+ /** The minimum word count per Requirements 8.4 */
430
+ const MINIMUM_WORD_COUNT = 20;
431
+
432
+ describe('Message Length Validation - Property Tests', () => {
433
+ // **Feature: earnd-bounty-engine, Property 17: Message Length Validation**
434
+ test('messages with >= minimum words are valid', () => {
435
+ // Generate arrays of words with at least MINIMUM_WORD_COUNT words
436
+ const validMessageArb = fc
437
+ .array(fc.string({ minLength: 1 }).filter((s) => s.trim().length > 0 && !/\s/.test(s)), {
438
+ minLength: MINIMUM_WORD_COUNT,
439
+ maxLength: 100,
440
+ })
441
+ .map((words) => words.join(' '));
442
+
443
+ fc.assert(
444
+ fc.property(validMessageArb, (message) => {
445
+ const result = validateMessageLength(message, MINIMUM_WORD_COUNT);
446
+ expect(result.valid).toBe(true);
447
+ expect(result.errors).toHaveLength(0);
448
+ }),
449
+ { numRuns: 100 }
450
+ );
451
+ });
452
+
453
+ // **Feature: earnd-bounty-engine, Property 17: Message Length Validation**
454
+ test('messages with < minimum words are invalid', () => {
455
+ // Generate arrays of words with fewer than MINIMUM_WORD_COUNT words
456
+ const shortMessageArb = fc
457
+ .array(fc.string({ minLength: 1 }).filter((s) => s.trim().length > 0 && !/\s/.test(s)), {
458
+ minLength: 0,
459
+ maxLength: MINIMUM_WORD_COUNT - 1,
460
+ })
461
+ .map((words) => words.join(' '));
462
+
463
+ fc.assert(
464
+ fc.property(shortMessageArb, (message) => {
465
+ const result = validateMessageLength(message, MINIMUM_WORD_COUNT);
466
+ expect(result.valid).toBe(false);
467
+ expect(result.errors.length).toBeGreaterThan(0);
468
+ }),
469
+ { numRuns: 100 }
470
+ );
471
+ });
472
+
473
+ // **Feature: earnd-bounty-engine, Property 17: Message Length Validation**
474
+ test('boundary value: exactly minimum words is valid', () => {
475
+ const words = Array(MINIMUM_WORD_COUNT).fill('word');
476
+ const message = words.join(' ');
477
+ const result = validateMessageLength(message, MINIMUM_WORD_COUNT);
478
+ expect(result.valid).toBe(true);
479
+ expect(result.errors).toHaveLength(0);
480
+ });
481
+
482
+ // **Feature: earnd-bounty-engine, Property 17: Message Length Validation**
483
+ test('boundary value: one below minimum words is invalid', () => {
484
+ const words = Array(MINIMUM_WORD_COUNT - 1).fill('word');
485
+ const message = words.join(' ');
486
+ const result = validateMessageLength(message, MINIMUM_WORD_COUNT);
487
+ expect(result.valid).toBe(false);
488
+ expect(result.errors.length).toBeGreaterThan(0);
489
+ });
490
+
491
+ // **Feature: earnd-bounty-engine, Property 17: Message Length Validation**
492
+ test('validation is deterministic - same input produces same output', () => {
493
+ fc.assert(
494
+ fc.property(fc.string(), (message) => {
495
+ const result1 = validateMessageLength(message, MINIMUM_WORD_COUNT);
496
+ const result2 = validateMessageLength(message, MINIMUM_WORD_COUNT);
497
+ expect(result1.valid).toBe(result2.valid);
498
+ expect(result1.errors).toEqual(result2.errors);
499
+ }),
500
+ { numRuns: 100 }
501
+ );
502
+ });
503
+
504
+ // **Feature: earnd-bounty-engine, Property 17: Message Length Validation**
505
+ test('validation result structure is correct', () => {
506
+ fc.assert(
507
+ fc.property(fc.string(), (message) => {
508
+ const result = validateMessageLength(message, MINIMUM_WORD_COUNT);
509
+ // Result must have valid boolean and errors array
510
+ expect(typeof result.valid).toBe('boolean');
511
+ expect(Array.isArray(result.errors)).toBe(true);
512
+ // If valid, errors should be empty; if invalid, errors should have content
513
+ if (result.valid) {
514
+ expect(result.errors).toHaveLength(0);
515
+ } else {
516
+ expect(result.errors.length).toBeGreaterThan(0);
517
+ }
518
+ }),
519
+ { numRuns: 100 }
520
+ );
521
+ });
522
+
523
+ // **Feature: earnd-bounty-engine, Property 17: Message Length Validation**
524
+ test('empty message is invalid', () => {
525
+ const result = validateMessageLength('', MINIMUM_WORD_COUNT);
526
+ expect(result.valid).toBe(false);
527
+ expect(result.errors.length).toBeGreaterThan(0);
528
+ });
529
+
530
+ // **Feature: earnd-bounty-engine, Property 17: Message Length Validation**
531
+ test('whitespace-only message is invalid', () => {
532
+ fc.assert(
533
+ fc.property(
534
+ fc.array(fc.constantFrom(' ', '\t', '\n', '\r'), { minLength: 1, maxLength: 50 }).map(
535
+ (chars) => chars.join('')
536
+ ),
537
+ (whitespace) => {
538
+ const result = validateMessageLength(whitespace, MINIMUM_WORD_COUNT);
539
+ expect(result.valid).toBe(false);
540
+ expect(result.errors.length).toBeGreaterThan(0);
541
+ }
542
+ ),
543
+ { numRuns: 100 }
544
+ );
545
+ });
546
+
547
+ // **Feature: earnd-bounty-engine, Property 17: Message Length Validation**
548
+ test('works with different minimum thresholds', () => {
549
+ fc.assert(
550
+ fc.property(
551
+ fc.integer({ min: 1, max: 50 }),
552
+ fc.array(fc.string({ minLength: 1 }).filter((s) => s.trim().length > 0 && !/\s/.test(s)), {
553
+ minLength: 0,
554
+ maxLength: 100,
555
+ }),
556
+ (minWords, words) => {
557
+ const message = words.join(' ');
558
+ const result = validateMessageLength(message, minWords);
559
+ // The property: valid iff word count >= minWords
560
+ expect(result.valid).toBe(words.length >= minWords);
561
+ }
562
+ ),
563
+ { numRuns: 100 }
564
+ );
565
+ });
566
+ });
567
+
568
+
569
+ /**
570
+ * Property-based tests for Email Syntax Validation
571
+ *
572
+ * **Feature: earnd-bounty-engine, Property 18: Email Syntax Validation**
573
+ * **Validates: Requirements 8.5**
574
+ *
575
+ * Property 18: Email Syntax Validation
576
+ * *For any* email string, the email validator SHALL return true
577
+ * if and only if the string matches a valid email syntax pattern.
578
+ */
579
+
580
+ import { validateEmail } from './validators.js';
581
+
582
+ /**
583
+ * Custom arbitrary for emails matching the validator's regex pattern:
584
+ * /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
585
+ */
586
+ const validEmailArb = fc
587
+ .tuple(
588
+ // Local part: alphanumeric, dots, underscores, percent, plus, hyphens
589
+ fc.stringMatching(/^[a-zA-Z0-9._%+-]+$/).filter((s) => s.length > 0),
590
+ // Domain: alphanumeric and hyphens with dots
591
+ fc.stringMatching(/^[a-zA-Z0-9-]+$/).filter((s) => s.length > 0),
592
+ // TLD: at least 2 alphabetic characters
593
+ fc.stringMatching(/^[a-zA-Z]{2,6}$/)
594
+ )
595
+ .map(([local, domain, tld]) => `${local}@${domain}.${tld}`);
596
+
597
+ describe('Email Syntax Validation - Property Tests', () => {
598
+ // **Feature: earnd-bounty-engine, Property 18: Email Syntax Validation**
599
+ test('valid email addresses pass validation', () => {
600
+ fc.assert(
601
+ fc.property(validEmailArb, (email) => {
602
+ const result = validateEmail(email);
603
+ expect(result.valid).toBe(true);
604
+ expect(result.errors).toHaveLength(0);
605
+ }),
606
+ { numRuns: 100 }
607
+ );
608
+ });
609
+
610
+ // **Feature: earnd-bounty-engine, Property 18: Email Syntax Validation**
611
+ test('strings without @ symbol are invalid', () => {
612
+ const noAtSymbolArb = fc.string().filter((s) => !s.includes('@'));
613
+
614
+ fc.assert(
615
+ fc.property(noAtSymbolArb, (email) => {
616
+ const result = validateEmail(email);
617
+ expect(result.valid).toBe(false);
618
+ expect(result.errors.length).toBeGreaterThan(0);
619
+ }),
620
+ { numRuns: 100 }
621
+ );
622
+ });
623
+
624
+ // **Feature: earnd-bounty-engine, Property 18: Email Syntax Validation**
625
+ test('strings with multiple @ symbols are invalid', () => {
626
+ // Generate strings with at least 2 @ symbols
627
+ const multipleAtArb = fc
628
+ .tuple(fc.string(), fc.string(), fc.string())
629
+ .map(([a, b, c]) => `${a}@${b}@${c}`);
630
+
631
+ fc.assert(
632
+ fc.property(multipleAtArb, (email) => {
633
+ const result = validateEmail(email);
634
+ expect(result.valid).toBe(false);
635
+ expect(result.errors.length).toBeGreaterThan(0);
636
+ }),
637
+ { numRuns: 100 }
638
+ );
639
+ });
640
+
641
+ // **Feature: earnd-bounty-engine, Property 18: Email Syntax Validation**
642
+ test('strings without domain TLD are invalid', () => {
643
+ // Generate strings with @ but no dot in domain
644
+ const noDotDomainArb = fc
645
+ .tuple(
646
+ fc.stringMatching(/^[a-zA-Z0-9._%+-]+$/),
647
+ fc.stringMatching(/^[a-zA-Z0-9-]+$/)
648
+ )
649
+ .filter(([local, domain]) => local.length > 0 && domain.length > 0)
650
+ .map(([local, domain]) => `${local}@${domain}`);
651
+
652
+ fc.assert(
653
+ fc.property(noDotDomainArb, (email) => {
654
+ const result = validateEmail(email);
655
+ expect(result.valid).toBe(false);
656
+ expect(result.errors.length).toBeGreaterThan(0);
657
+ }),
658
+ { numRuns: 100 }
659
+ );
660
+ });
661
+
662
+ // **Feature: earnd-bounty-engine, Property 18: Email Syntax Validation**
663
+ test('validation is deterministic - same input produces same output', () => {
664
+ fc.assert(
665
+ fc.property(fc.string(), (email) => {
666
+ const result1 = validateEmail(email);
667
+ const result2 = validateEmail(email);
668
+ expect(result1.valid).toBe(result2.valid);
669
+ expect(result1.errors).toEqual(result2.errors);
670
+ }),
671
+ { numRuns: 100 }
672
+ );
673
+ });
674
+
675
+ // **Feature: earnd-bounty-engine, Property 18: Email Syntax Validation**
676
+ test('validation result structure is correct', () => {
677
+ fc.assert(
678
+ fc.property(fc.string(), (email) => {
679
+ const result = validateEmail(email);
680
+ // Result must have valid boolean and errors array
681
+ expect(typeof result.valid).toBe('boolean');
682
+ expect(Array.isArray(result.errors)).toBe(true);
683
+ // If valid, errors should be empty; if invalid, errors should have content
684
+ if (result.valid) {
685
+ expect(result.errors).toHaveLength(0);
686
+ } else {
687
+ expect(result.errors.length).toBeGreaterThan(0);
688
+ }
689
+ }),
690
+ { numRuns: 100 }
691
+ );
692
+ });
693
+
694
+ // **Feature: earnd-bounty-engine, Property 18: Email Syntax Validation**
695
+ test('empty string is invalid', () => {
696
+ const result = validateEmail('');
697
+ expect(result.valid).toBe(false);
698
+ expect(result.errors.length).toBeGreaterThan(0);
699
+ });
700
+
701
+ // **Feature: earnd-bounty-engine, Property 18: Email Syntax Validation**
702
+ test('common valid email formats pass validation', () => {
703
+ const validEmails = [
704
+ 'user@example.com',
705
+ 'user.name@example.com',
706
+ 'user+tag@example.com',
707
+ 'user_name@example.co.uk',
708
+ 'user123@subdomain.example.org',
709
+ 'first.last@company.io',
710
+ ];
711
+
712
+ fc.assert(
713
+ fc.property(fc.constantFrom(...validEmails), (email) => {
714
+ const result = validateEmail(email);
715
+ expect(result.valid).toBe(true);
716
+ expect(result.errors).toHaveLength(0);
717
+ }),
718
+ { numRuns: 100 }
719
+ );
720
+ });
721
+
722
+ // **Feature: earnd-bounty-engine, Property 18: Email Syntax Validation**
723
+ test('common invalid email formats fail validation', () => {
724
+ const invalidEmails = [
725
+ 'plainaddress',
726
+ '@missinglocal.com',
727
+ 'missing@.com',
728
+ 'missing@domain.',
729
+ 'spaces in@email.com',
730
+ 'email@domain',
731
+ ];
732
+
733
+ fc.assert(
734
+ fc.property(fc.constantFrom(...invalidEmails), (email) => {
735
+ const result = validateEmail(email);
736
+ expect(result.valid).toBe(false);
737
+ expect(result.errors.length).toBeGreaterThan(0);
738
+ }),
739
+ { numRuns: 100 }
740
+ );
741
+ });
742
+
743
+ // **Feature: earnd-bounty-engine, Property 18: Email Syntax Validation**
744
+ test('TLD must be at least 2 characters', () => {
745
+ // Generate emails with single-character TLDs
746
+ const shortTldArb = fc
747
+ .tuple(
748
+ fc.stringMatching(/^[a-zA-Z0-9._%+-]+$/).filter((s) => s.length > 0),
749
+ fc.stringMatching(/^[a-zA-Z0-9-]+$/).filter((s) => s.length > 0),
750
+ fc.stringMatching(/^[a-zA-Z]$/)
751
+ )
752
+ .map(([local, domain, tld]) => `${local}@${domain}.${tld}`);
753
+
754
+ fc.assert(
755
+ fc.property(shortTldArb, (email) => {
756
+ const result = validateEmail(email);
757
+ expect(result.valid).toBe(false);
758
+ expect(result.errors.length).toBeGreaterThan(0);
759
+ }),
760
+ { numRuns: 100 }
761
+ );
762
+ });
763
+ });
764
+
765
+ /**
766
+ * Property-based tests for LinkedIn URL Validation
767
+ *
768
+ * **Feature: launch-readiness-checklist, Property 1: Tournament validator completeness**
769
+ * **Validates: Requirements 1.1**
770
+ *
771
+ * Property 1: Tournament validator completeness
772
+ * *For any* LinkedIn URL string, the LinkedIn validator SHALL return true
773
+ * if and only if the URL starts with "https://www.linkedin.com/in/".
774
+ */
775
+
776
+ import { validateLinkedIn, validateLeadGenPrecision } from './validators.js';
777
+
778
+ /** The required LinkedIn URL prefix per Requirements 2.7, 4.6 */
779
+ const LINKEDIN_PREFIX = 'https://www.linkedin.com/in/';
780
+
781
+ describe('LinkedIn URL Validation - Property Tests', () => {
782
+ // **Feature: launch-readiness-checklist, Property 1: Tournament validator completeness**
783
+ test('URLs with correct prefix are valid', () => {
784
+ // Generate LinkedIn URLs with the correct prefix
785
+ const validLinkedInArb = fc
786
+ .string({ minLength: 1 })
787
+ .map((suffix) => `${LINKEDIN_PREFIX}${suffix}`);
788
+
789
+ fc.assert(
790
+ fc.property(validLinkedInArb, (linkedIn) => {
791
+ const result = validateLinkedIn(linkedIn);
792
+ expect(result.valid).toBe(true);
793
+ expect(result.errors).toHaveLength(0);
794
+ }),
795
+ { numRuns: 100 }
796
+ );
797
+ });
798
+
799
+ // **Feature: launch-readiness-checklist, Property 1: Tournament validator completeness**
800
+ test('URLs without correct prefix are invalid', () => {
801
+ // Generate URLs that don't start with the correct prefix
802
+ const invalidLinkedInArb = fc
803
+ .string()
804
+ .filter((url) => !url.startsWith(LINKEDIN_PREFIX));
805
+
806
+ fc.assert(
807
+ fc.property(invalidLinkedInArb, (linkedIn) => {
808
+ const result = validateLinkedIn(linkedIn);
809
+ expect(result.valid).toBe(false);
810
+ expect(result.errors.length).toBeGreaterThan(0);
811
+ expect(result.errors[0]).toContain('Invalid LinkedIn URL');
812
+ }),
813
+ { numRuns: 100 }
814
+ );
815
+ });
816
+
817
+ // **Feature: launch-readiness-checklist, Property 1: Tournament validator completeness**
818
+ test('validation is deterministic - same input produces same output', () => {
819
+ fc.assert(
820
+ fc.property(fc.string(), (linkedIn) => {
821
+ const result1 = validateLinkedIn(linkedIn);
822
+ const result2 = validateLinkedIn(linkedIn);
823
+ expect(result1.valid).toBe(result2.valid);
824
+ expect(result1.errors).toEqual(result2.errors);
825
+ }),
826
+ { numRuns: 100 }
827
+ );
828
+ });
829
+
830
+ // **Feature: launch-readiness-checklist, Property 1: Tournament validator completeness**
831
+ test('validation result structure is correct', () => {
832
+ fc.assert(
833
+ fc.property(fc.string(), (linkedIn) => {
834
+ const result = validateLinkedIn(linkedIn);
835
+ // Result must have valid boolean and errors array
836
+ expect(typeof result.valid).toBe('boolean');
837
+ expect(Array.isArray(result.errors)).toBe(true);
838
+ // If valid, errors should be empty; if invalid, errors should have content
839
+ if (result.valid) {
840
+ expect(result.errors).toHaveLength(0);
841
+ } else {
842
+ expect(result.errors.length).toBeGreaterThan(0);
843
+ }
844
+ }),
845
+ { numRuns: 100 }
846
+ );
847
+ });
848
+
849
+ // **Feature: launch-readiness-checklist, Property 1: Tournament validator completeness**
850
+ test('empty string is invalid', () => {
851
+ const result = validateLinkedIn('');
852
+ expect(result.valid).toBe(false);
853
+ expect(result.errors.length).toBeGreaterThan(0);
854
+ });
855
+
856
+ // **Feature: launch-readiness-checklist, Property 1: Tournament validator completeness**
857
+ test('common valid LinkedIn URLs pass validation', () => {
858
+ const validLinkedInUrls = [
859
+ 'https://www.linkedin.com/in/johndoe',
860
+ 'https://www.linkedin.com/in/jane-smith-123',
861
+ 'https://www.linkedin.com/in/user_name',
862
+ 'https://www.linkedin.com/in/first-last-456789',
863
+ ];
864
+
865
+ fc.assert(
866
+ fc.property(fc.constantFrom(...validLinkedInUrls), (linkedIn) => {
867
+ const result = validateLinkedIn(linkedIn);
868
+ expect(result.valid).toBe(true);
869
+ expect(result.errors).toHaveLength(0);
870
+ }),
871
+ { numRuns: 100 }
872
+ );
873
+ });
874
+
875
+ // **Feature: launch-readiness-checklist, Property 1: Tournament validator completeness**
876
+ test('common invalid LinkedIn URLs fail validation', () => {
877
+ const invalidLinkedInUrls = [
878
+ 'http://www.linkedin.com/in/johndoe', // http instead of https
879
+ 'https://linkedin.com/in/johndoe', // missing www
880
+ 'https://www.linkedin.com/johndoe', // missing /in/
881
+ 'https://www.facebook.com/johndoe', // wrong domain
882
+ 'www.linkedin.com/in/johndoe', // missing protocol
883
+ ];
884
+
885
+ fc.assert(
886
+ fc.property(fc.constantFrom(...invalidLinkedInUrls), (linkedIn) => {
887
+ const result = validateLinkedIn(linkedIn);
888
+ expect(result.valid).toBe(false);
889
+ expect(result.errors.length).toBeGreaterThan(0);
890
+ }),
891
+ { numRuns: 100 }
892
+ );
893
+ });
894
+
895
+ // **Feature: launch-readiness-checklist, Property 1: Tournament validator completeness**
896
+ test('prefix matching is case-sensitive', () => {
897
+ // Generate case variations of the prefix (should all be invalid except exact match)
898
+ const caseVariations = [
899
+ 'HTTPS://WWW.LINKEDIN.COM/IN/',
900
+ 'https://WWW.linkedin.com/in/',
901
+ 'https://www.LINKEDIN.com/in/',
902
+ 'https://www.linkedin.COM/in/',
903
+ 'https://www.linkedin.com/IN/',
904
+ ];
905
+
906
+ fc.assert(
907
+ fc.property(fc.constantFrom(...caseVariations), fc.string(), (prefix, suffix) => {
908
+ const linkedIn = `${prefix}${suffix}`;
909
+ const result = validateLinkedIn(linkedIn);
910
+ // Only the exact prefix should be valid
911
+ if (prefix === LINKEDIN_PREFIX) {
912
+ expect(result.valid).toBe(true);
913
+ } else {
914
+ expect(result.valid).toBe(false);
915
+ }
916
+ }),
917
+ { numRuns: 100 }
918
+ );
919
+ });
920
+ });
921
+
922
+
923
+ /**
924
+ * Property-based tests for Lead Generation Precision Validation
925
+ *
926
+ * **Feature: launch-readiness-checklist, Property 1: Tournament validator completeness**
927
+ * **Validates: Requirements 1.1**
928
+ *
929
+ * Property 1: Tournament validator completeness (continued)
930
+ * *For any* lead generation artifact, the composite validator SHALL return true
931
+ * if and only if ALL criteria are met: valid email, company size >= 50,
932
+ * non-excluded role, and valid LinkedIn URL.
933
+ */
934
+
935
+ /** Lead generation artifact arbitrary for property testing */
936
+ const leadGenArtifactArb = fc.record({
937
+ email: fc.emailAddress(),
938
+ companySize: fc.integer({ min: 1, max: 10000 }),
939
+ role: fc.string({ minLength: 1, maxLength: 50 }),
940
+ linkedIn: fc.string({ minLength: 1, maxLength: 100 }),
941
+ });
942
+
943
+ /** Valid lead generation artifact arbitrary */
944
+ const validLeadGenArtifactArb = fc.record({
945
+ email: fc.emailAddress(),
946
+ companySize: fc.integer({ min: 50, max: 10000 }),
947
+ role: fc.string({ minLength: 1, maxLength: 50 }).filter((role) => {
948
+ const lowerRole = role.toLowerCase().trim();
949
+ return !['intern', 'student'].includes(lowerRole);
950
+ }),
951
+ linkedIn: fc.string({ minLength: 1 }).map((suffix) => `${LINKEDIN_PREFIX}${suffix}`),
952
+ });
953
+
954
+ describe('Lead Generation Precision Validation - Property Tests', () => {
955
+ // **Feature: launch-readiness-checklist, Property 1: Tournament validator completeness**
956
+ test('artifacts meeting all criteria are valid', () => {
957
+ fc.assert(
958
+ fc.property(validLeadGenArtifactArb, (artifact) => {
959
+ const result = validateLeadGenPrecision(artifact);
960
+ expect(result.valid).toBe(true);
961
+ expect(result.errors).toHaveLength(0);
962
+ }),
963
+ { numRuns: 100 }
964
+ );
965
+ });
966
+
967
+ // **Feature: launch-readiness-checklist, Property 1: Tournament validator completeness**
968
+ test('artifacts with invalid email are invalid', () => {
969
+ const invalidEmailArtifactArb = validLeadGenArtifactArb.map((artifact) => ({
970
+ ...artifact,
971
+ email: 'invalid-email-format', // Invalid email
972
+ }));
973
+
974
+ fc.assert(
975
+ fc.property(invalidEmailArtifactArb, (artifact) => {
976
+ const result = validateLeadGenPrecision(artifact);
977
+ expect(result.valid).toBe(false);
978
+ expect(result.errors.length).toBeGreaterThan(0);
979
+ expect(result.errors.some((error) => error.includes('Invalid email format'))).toBe(true);
980
+ }),
981
+ { numRuns: 100 }
982
+ );
983
+ });
984
+
985
+ // **Feature: launch-readiness-checklist, Property 1: Tournament validator completeness**
986
+ test('artifacts with company size < 50 are invalid', () => {
987
+ const smallCompanyArtifactArb = validLeadGenArtifactArb.map((artifact) => ({
988
+ ...artifact,
989
+ companySize: fc.sample(fc.integer({ min: 1, max: 49 }), 1)[0], // Company size < 50
990
+ }));
991
+
992
+ fc.assert(
993
+ fc.property(smallCompanyArtifactArb, (artifact) => {
994
+ const result = validateLeadGenPrecision(artifact);
995
+ expect(result.valid).toBe(false);
996
+ expect(result.errors.length).toBeGreaterThan(0);
997
+ expect(result.errors.some((error) => error.includes('Company too small'))).toBe(true);
998
+ }),
999
+ { numRuns: 100 }
1000
+ );
1001
+ });
1002
+
1003
+ // **Feature: launch-readiness-checklist, Property 1: Tournament validator completeness**
1004
+ test('artifacts with excluded roles are invalid', () => {
1005
+ const excludedRoleArtifactArb = validLeadGenArtifactArb.map((artifact) => ({
1006
+ ...artifact,
1007
+ role: fc.sample(fc.constantFrom('intern', 'student', 'INTERN', 'Student'), 1)[0], // Excluded role
1008
+ }));
1009
+
1010
+ fc.assert(
1011
+ fc.property(excludedRoleArtifactArb, (artifact) => {
1012
+ const result = validateLeadGenPrecision(artifact);
1013
+ expect(result.valid).toBe(false);
1014
+ expect(result.errors.length).toBeGreaterThan(0);
1015
+ expect(result.errors.some((error) => error.includes('Invalid role'))).toBe(true);
1016
+ }),
1017
+ { numRuns: 100 }
1018
+ );
1019
+ });
1020
+
1021
+ // **Feature: launch-readiness-checklist, Property 1: Tournament validator completeness**
1022
+ test('artifacts with invalid LinkedIn URL are invalid', () => {
1023
+ const invalidLinkedInArtifactArb = validLeadGenArtifactArb.map((artifact) => ({
1024
+ ...artifact,
1025
+ linkedIn: 'https://www.facebook.com/profile', // Invalid LinkedIn URL
1026
+ }));
1027
+
1028
+ fc.assert(
1029
+ fc.property(invalidLinkedInArtifactArb, (artifact) => {
1030
+ const result = validateLeadGenPrecision(artifact);
1031
+ expect(result.valid).toBe(false);
1032
+ expect(result.errors.length).toBeGreaterThan(0);
1033
+ expect(result.errors.some((error) => error.includes('Invalid LinkedIn URL'))).toBe(true);
1034
+ }),
1035
+ { numRuns: 100 }
1036
+ );
1037
+ });
1038
+
1039
+ // **Feature: launch-readiness-checklist, Property 1: Tournament validator completeness**
1040
+ test('validation is deterministic - same input produces same output', () => {
1041
+ fc.assert(
1042
+ fc.property(leadGenArtifactArb, (artifact) => {
1043
+ const result1 = validateLeadGenPrecision(artifact);
1044
+ const result2 = validateLeadGenPrecision(artifact);
1045
+ expect(result1.valid).toBe(result2.valid);
1046
+ expect(result1.errors).toEqual(result2.errors);
1047
+ }),
1048
+ { numRuns: 100 }
1049
+ );
1050
+ });
1051
+
1052
+ // **Feature: launch-readiness-checklist, Property 1: Tournament validator completeness**
1053
+ test('validation result structure is correct', () => {
1054
+ fc.assert(
1055
+ fc.property(leadGenArtifactArb, (artifact) => {
1056
+ const result = validateLeadGenPrecision(artifact);
1057
+ // Result must have valid boolean and errors array
1058
+ expect(typeof result.valid).toBe('boolean');
1059
+ expect(Array.isArray(result.errors)).toBe(true);
1060
+ // If valid, errors should be empty; if invalid, errors should have content
1061
+ if (result.valid) {
1062
+ expect(result.errors).toHaveLength(0);
1063
+ } else {
1064
+ expect(result.errors.length).toBeGreaterThan(0);
1065
+ }
1066
+ }),
1067
+ { numRuns: 100 }
1068
+ );
1069
+ });
1070
+
1071
+ // **Feature: launch-readiness-checklist, Property 1: Tournament validator completeness**
1072
+ test('multiple validation errors are accumulated', () => {
1073
+ // Create an artifact that fails all validations
1074
+ const allInvalidArtifact = {
1075
+ email: 'invalid-email',
1076
+ companySize: 10, // Too small
1077
+ role: 'intern', // Excluded role
1078
+ linkedIn: 'https://www.facebook.com/profile', // Invalid LinkedIn
1079
+ };
1080
+
1081
+ const result = validateLeadGenPrecision(allInvalidArtifact);
1082
+ expect(result.valid).toBe(false);
1083
+ expect(result.errors.length).toBe(4); // Should have 4 errors (one for each validation)
1084
+ expect(result.errors.some((error) => error.includes('Invalid email format'))).toBe(true);
1085
+ expect(result.errors.some((error) => error.includes('Company too small'))).toBe(true);
1086
+ expect(result.errors.some((error) => error.includes('Invalid role'))).toBe(true);
1087
+ expect(result.errors.some((error) => error.includes('Invalid LinkedIn URL'))).toBe(true);
1088
+ });
1089
+
1090
+ // **Feature: launch-readiness-checklist, Property 1: Tournament validator completeness**
1091
+ test('boundary value: company size exactly 50 is valid', () => {
1092
+ const boundaryArtifact = {
1093
+ email: 'user@example.com',
1094
+ companySize: 50, // Exactly at boundary
1095
+ role: 'Manager',
1096
+ linkedIn: 'https://www.linkedin.com/in/user',
1097
+ };
1098
+
1099
+ const result = validateLeadGenPrecision(boundaryArtifact);
1100
+ expect(result.valid).toBe(true);
1101
+ expect(result.errors).toHaveLength(0);
1102
+ });
1103
+
1104
+ // **Feature: launch-readiness-checklist, Property 1: Tournament validator completeness**
1105
+ test('boundary value: company size 49 is invalid', () => {
1106
+ const boundaryArtifact = {
1107
+ email: 'user@example.com',
1108
+ companySize: 49, // Just below boundary
1109
+ role: 'Manager',
1110
+ linkedIn: 'https://www.linkedin.com/in/user',
1111
+ };
1112
+
1113
+ const result = validateLeadGenPrecision(boundaryArtifact);
1114
+ expect(result.valid).toBe(false);
1115
+ expect(result.errors.length).toBeGreaterThan(0);
1116
+ expect(result.errors.some((error) => error.includes('Company too small'))).toBe(true);
1117
+ });
1118
+ });