@holoscript/framework 6.0.3

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 (329) hide show
  1. package/ALL-test-results.json +1 -0
  2. package/CHANGELOG.md +8 -0
  3. package/LICENSE +21 -0
  4. package/ROADMAP.md +175 -0
  5. package/dist/AgentManifest-CB4xM-Ma.d.cts +704 -0
  6. package/dist/AgentManifest-CB4xM-Ma.d.ts +704 -0
  7. package/dist/BehaviorTree-BrBFECv5.d.cts +103 -0
  8. package/dist/BehaviorTree-BrBFECv5.d.ts +103 -0
  9. package/dist/InvisibleWallet-BB6tFvRA.d.cts +1732 -0
  10. package/dist/InvisibleWallet-rtRrBOA8.d.ts +1732 -0
  11. package/dist/OrchestratorAgent-BvWgf9uw.d.cts +798 -0
  12. package/dist/OrchestratorAgent-Q_CbVTmO.d.ts +798 -0
  13. package/dist/agents/index.cjs +4790 -0
  14. package/dist/agents/index.d.cts +1788 -0
  15. package/dist/agents/index.d.ts +1788 -0
  16. package/dist/agents/index.js +4695 -0
  17. package/dist/ai/index.cjs +5347 -0
  18. package/dist/ai/index.d.cts +1753 -0
  19. package/dist/ai/index.d.ts +1753 -0
  20. package/dist/ai/index.js +5244 -0
  21. package/dist/behavior.cjs +449 -0
  22. package/dist/behavior.d.cts +130 -0
  23. package/dist/behavior.d.ts +130 -0
  24. package/dist/behavior.js +407 -0
  25. package/dist/economy/index.cjs +3659 -0
  26. package/dist/economy/index.d.cts +747 -0
  27. package/dist/economy/index.d.ts +747 -0
  28. package/dist/economy/index.js +3617 -0
  29. package/dist/implementations-D9T3un9D.d.cts +236 -0
  30. package/dist/implementations-D9T3un9D.d.ts +236 -0
  31. package/dist/index.cjs +24550 -0
  32. package/dist/index.d.cts +1729 -0
  33. package/dist/index.d.ts +1729 -0
  34. package/dist/index.js +24277 -0
  35. package/dist/learning/index.cjs +219 -0
  36. package/dist/learning/index.d.cts +104 -0
  37. package/dist/learning/index.d.ts +104 -0
  38. package/dist/learning/index.js +189 -0
  39. package/dist/negotiation/index.cjs +970 -0
  40. package/dist/negotiation/index.d.cts +610 -0
  41. package/dist/negotiation/index.d.ts +610 -0
  42. package/dist/negotiation/index.js +931 -0
  43. package/dist/skills/index.cjs +1118 -0
  44. package/dist/skills/index.d.cts +289 -0
  45. package/dist/skills/index.d.ts +289 -0
  46. package/dist/skills/index.js +1079 -0
  47. package/dist/swarm/index.cjs +5268 -0
  48. package/dist/swarm/index.d.cts +2433 -0
  49. package/dist/swarm/index.d.ts +2433 -0
  50. package/dist/swarm/index.js +5221 -0
  51. package/dist/training/index.cjs +2745 -0
  52. package/dist/training/index.d.cts +1734 -0
  53. package/dist/training/index.d.ts +1734 -0
  54. package/dist/training/index.js +2687 -0
  55. package/extract-failures.js +10 -0
  56. package/package.json +82 -0
  57. package/src/__tests__/bounty-marketplace.test.ts +374 -0
  58. package/src/__tests__/delegation.test.ts +144 -0
  59. package/src/__tests__/distributed-claimer.test.ts +147 -0
  60. package/src/__tests__/done-log-audit.test.ts +342 -0
  61. package/src/__tests__/framework.test.ts +865 -0
  62. package/src/__tests__/goal-synthesizer.test.ts +236 -0
  63. package/src/__tests__/presence.test.ts +223 -0
  64. package/src/__tests__/protocol-agent.test.ts +254 -0
  65. package/src/__tests__/revenue-splitter.test.ts +114 -0
  66. package/src/__tests__/scenario-driven-todo.test.ts +197 -0
  67. package/src/__tests__/self-improve.test.ts +349 -0
  68. package/src/__tests__/service-lifecycle.test.ts +237 -0
  69. package/src/__tests__/skill-router.test.ts +121 -0
  70. package/src/agents/AgentManifest.ts +493 -0
  71. package/src/agents/AgentRegistry.ts +475 -0
  72. package/src/agents/AgentTypes.ts +585 -0
  73. package/src/agents/AgentWalletRegistry.ts +83 -0
  74. package/src/agents/AuthenticatedCRDT.ts +388 -0
  75. package/src/agents/CapabilityMatcher.ts +453 -0
  76. package/src/agents/CrossRealityHandoff.ts +305 -0
  77. package/src/agents/CulturalMemory.ts +454 -0
  78. package/src/agents/FederatedRegistryAdapter.ts +429 -0
  79. package/src/agents/NormEngine.ts +450 -0
  80. package/src/agents/OrchestratorAgent.ts +414 -0
  81. package/src/agents/SkillWorkflowEngine.ts +472 -0
  82. package/src/agents/TaskDelegationService.ts +551 -0
  83. package/src/agents/__tests__/AgentManifest.prod.test.ts +134 -0
  84. package/src/agents/__tests__/AgentManifest.test.ts +182 -0
  85. package/src/agents/__tests__/AgentModule.test.ts +864 -0
  86. package/src/agents/__tests__/AgentRegistry.prod.test.ts +125 -0
  87. package/src/agents/__tests__/AgentRegistry.test.ts +148 -0
  88. package/src/agents/__tests__/AgentTypes.test.ts +534 -0
  89. package/src/agents/__tests__/AgentWalletRegistry.test.ts +152 -0
  90. package/src/agents/__tests__/AuthenticatedCRDT.test.ts +558 -0
  91. package/src/agents/__tests__/CapabilityMatcher.prod.test.ts +117 -0
  92. package/src/agents/__tests__/CapabilityMatcher.test.ts +178 -0
  93. package/src/agents/__tests__/CrossRealityHandoff.test.ts +402 -0
  94. package/src/agents/__tests__/CulturalMemory.test.ts +200 -0
  95. package/src/agents/__tests__/FederatedRegistryAdapter.test.ts +409 -0
  96. package/src/agents/__tests__/NormEngine.test.ts +276 -0
  97. package/src/agents/__tests__/OrchestratorAgent.test.ts +182 -0
  98. package/src/agents/__tests__/SkillWorkflowEngine.test.ts +357 -0
  99. package/src/agents/__tests__/TaskDelegationService.test.ts +446 -0
  100. package/src/agents/index.ts +107 -0
  101. package/src/agents/spatial-comms/Layer1RealTime.ts +621 -0
  102. package/src/agents/spatial-comms/Layer2A2A.ts +661 -0
  103. package/src/agents/spatial-comms/Layer3MCP.ts +651 -0
  104. package/src/agents/spatial-comms/ProtocolTypes.ts +543 -0
  105. package/src/agents/spatial-comms/SpatialCommClient.ts +483 -0
  106. package/src/agents/spatial-comms/__tests__/performance-benchmark.test.ts +465 -0
  107. package/src/agents/spatial-comms/examples/multi-agent-world-creation.ts +409 -0
  108. package/src/agents/spatial-comms/index.ts +66 -0
  109. package/src/ai/AIAdapter.ts +313 -0
  110. package/src/ai/AICopilot.ts +331 -0
  111. package/src/ai/AIOutputValidator.ts +203 -0
  112. package/src/ai/BTNodes.ts +239 -0
  113. package/src/ai/BehaviorSelector.ts +135 -0
  114. package/src/ai/BehaviorTree.ts +153 -0
  115. package/src/ai/Blackboard.ts +165 -0
  116. package/src/ai/GenerationAnalytics.ts +461 -0
  117. package/src/ai/GenerationCache.ts +265 -0
  118. package/src/ai/GoalPlanner.ts +165 -0
  119. package/src/ai/HoloScriptGenerator.ts +580 -0
  120. package/src/ai/InfluenceMap.ts +180 -0
  121. package/src/ai/NavMesh.ts +168 -0
  122. package/src/ai/PerceptionSystem.ts +178 -0
  123. package/src/ai/PromptTemplates.ts +453 -0
  124. package/src/ai/SemanticSearchService.ts +80 -0
  125. package/src/ai/StateMachine.ts +196 -0
  126. package/src/ai/SteeringBehavior.ts +150 -0
  127. package/src/ai/SteeringBehaviors.ts +244 -0
  128. package/src/ai/TrainingDataGenerator.ts +1082 -0
  129. package/src/ai/UtilityAI.ts +145 -0
  130. package/src/ai/__tests__/AIAdapter.prod.test.ts +259 -0
  131. package/src/ai/__tests__/AIAdapter.test.ts +109 -0
  132. package/src/ai/__tests__/AICopilot.prod.test.ts +341 -0
  133. package/src/ai/__tests__/AICopilot.test.ts +178 -0
  134. package/src/ai/__tests__/AIOutputValidator.prod.test.ts +226 -0
  135. package/src/ai/__tests__/AIOutputValidator.test.ts +138 -0
  136. package/src/ai/__tests__/BTNodes.prod.test.ts +391 -0
  137. package/src/ai/__tests__/BTNodes.test.ts +263 -0
  138. package/src/ai/__tests__/BehaviorSelector.prod.test.ts +129 -0
  139. package/src/ai/__tests__/BehaviorSelector.test.ts +132 -0
  140. package/src/ai/__tests__/BehaviorTree.prod.test.ts +266 -0
  141. package/src/ai/__tests__/BehaviorTree.test.ts +216 -0
  142. package/src/ai/__tests__/Blackboard.prod.test.ts +339 -0
  143. package/src/ai/__tests__/Blackboard.test.ts +183 -0
  144. package/src/ai/__tests__/GenerationAnalytics.prod.test.ts +141 -0
  145. package/src/ai/__tests__/GenerationAnalytics.test.ts +165 -0
  146. package/src/ai/__tests__/GenerationCache.prod.test.ts +144 -0
  147. package/src/ai/__tests__/GenerationCache.test.ts +171 -0
  148. package/src/ai/__tests__/GoalPlanner.prod.test.ts +189 -0
  149. package/src/ai/__tests__/GoalPlanner.test.ts +137 -0
  150. package/src/ai/__tests__/GoalPlannerDepth.prod.test.ts +217 -0
  151. package/src/ai/__tests__/HoloScriptGenerator.test.ts +125 -0
  152. package/src/ai/__tests__/InfluenceMap.prod.test.ts +146 -0
  153. package/src/ai/__tests__/InfluenceMap.test.ts +149 -0
  154. package/src/ai/__tests__/NavMesh.prod.test.ts +141 -0
  155. package/src/ai/__tests__/NavMesh.test.ts +159 -0
  156. package/src/ai/__tests__/PerceptionSystem.prod.test.ts +135 -0
  157. package/src/ai/__tests__/PerceptionSystem.test.ts +250 -0
  158. package/src/ai/__tests__/PromptTemplates.prod.test.ts +313 -0
  159. package/src/ai/__tests__/PromptTemplates.test.ts +146 -0
  160. package/src/ai/__tests__/SemanticSearch.test.ts +37 -0
  161. package/src/ai/__tests__/StateMachine.prod.test.ts +162 -0
  162. package/src/ai/__tests__/StateMachine.test.ts +163 -0
  163. package/src/ai/__tests__/SteeringBehavior.prod.test.ts +251 -0
  164. package/src/ai/__tests__/SteeringBehavior.test.ts +135 -0
  165. package/src/ai/__tests__/SteeringBehaviors.prod.test.ts +133 -0
  166. package/src/ai/__tests__/SteeringBehaviors.test.ts +151 -0
  167. package/src/ai/__tests__/TrainingDataGenerator.prod.test.ts +286 -0
  168. package/src/ai/__tests__/TrainingDataGenerator.test.ts +286 -0
  169. package/src/ai/__tests__/UtilityAI.prod.test.ts +207 -0
  170. package/src/ai/__tests__/UtilityAI.test.ts +155 -0
  171. package/src/ai/__tests__/adapters.prod.test.ts +263 -0
  172. package/src/ai/__tests__/adapters.test.ts +320 -0
  173. package/src/ai/adapters.ts +1585 -0
  174. package/src/ai/index.ts +130 -0
  175. package/src/behavior/BehaviorPresets.ts +140 -0
  176. package/src/behavior/BehaviorTree.ts +236 -0
  177. package/src/behavior/StateMachine.ts +176 -0
  178. package/src/behavior/StateTrait.ts +67 -0
  179. package/src/behavior/index.ts +8 -0
  180. package/src/behavior.ts +8 -0
  181. package/src/board/audit.ts +284 -0
  182. package/src/board/board-ops.ts +336 -0
  183. package/src/board/board-types.ts +302 -0
  184. package/src/board/index.ts +69 -0
  185. package/src/define-agent.ts +46 -0
  186. package/src/define-team.ts +33 -0
  187. package/src/delegation.ts +265 -0
  188. package/src/distributed-claimer.ts +228 -0
  189. package/src/economy/AgentBudgetEnforcer.ts +464 -0
  190. package/src/economy/BountyManager.ts +185 -0
  191. package/src/economy/CreatorRevenueAggregator.ts +460 -0
  192. package/src/economy/InvisibleWallet.ts +82 -0
  193. package/src/economy/KnowledgeMarketplace.ts +193 -0
  194. package/src/economy/PaymentWebhookService.ts +512 -0
  195. package/src/economy/RevenueSplitter.ts +156 -0
  196. package/src/economy/SubscriptionManager.ts +546 -0
  197. package/src/economy/UnifiedBudgetOptimizer.ts +635 -0
  198. package/src/economy/UsageMeter.ts +440 -0
  199. package/src/economy/_core-stubs.ts +219 -0
  200. package/src/economy/index.ts +100 -0
  201. package/src/economy/x402-facilitator.ts +1978 -0
  202. package/src/index.ts +348 -0
  203. package/src/knowledge/__tests__/knowledge-consolidator.test.ts +444 -0
  204. package/src/knowledge/__tests__/knowledge-store-vector.test.ts +291 -0
  205. package/src/knowledge/brain.ts +167 -0
  206. package/src/knowledge/consolidation.ts +581 -0
  207. package/src/knowledge/knowledge-consolidator.ts +510 -0
  208. package/src/knowledge/knowledge-store.ts +616 -0
  209. package/src/learning/MemoryConsolidator.ts +102 -0
  210. package/src/learning/MemoryScorer.ts +69 -0
  211. package/src/learning/ProceduralCompiler.ts +45 -0
  212. package/src/learning/SemanticClusterer.ts +66 -0
  213. package/src/learning/index.ts +8 -0
  214. package/src/llm/llm-adapter.ts +159 -0
  215. package/src/mesh/index.ts +309 -0
  216. package/src/negotiation/NegotiationProtocol.ts +694 -0
  217. package/src/negotiation/NegotiationTypes.ts +473 -0
  218. package/src/negotiation/VotingMechanisms.ts +691 -0
  219. package/src/negotiation/index.ts +49 -0
  220. package/src/protocol/goal-synthesizer.ts +317 -0
  221. package/src/protocol/implementations.ts +474 -0
  222. package/src/protocol/micro-phase-decomposer.ts +299 -0
  223. package/src/protocol/micro-step-decomposer.test.ts +306 -0
  224. package/src/protocol-agent.test.ts +353 -0
  225. package/src/protocol-agent.ts +670 -0
  226. package/src/self-improve/absorb-scanner.ts +252 -0
  227. package/src/self-improve/evolution-engine.ts +149 -0
  228. package/src/self-improve/framework-absorber.ts +214 -0
  229. package/src/self-improve/index.ts +50 -0
  230. package/src/self-improve/prompt-optimizer.ts +212 -0
  231. package/src/self-improve/test-generator.ts +175 -0
  232. package/src/skill-router.ts +186 -0
  233. package/src/skills/index.ts +5 -0
  234. package/src/skills/skill-md-bridge.ts +1699 -0
  235. package/src/swarm/ACOEngine.ts +261 -0
  236. package/src/swarm/CollectiveIntelligence.ts +383 -0
  237. package/src/swarm/ContributionSynthesizer.ts +481 -0
  238. package/src/swarm/LeaderElection.ts +393 -0
  239. package/src/swarm/PSOEngine.ts +206 -0
  240. package/src/swarm/QuorumPolicy.ts +173 -0
  241. package/src/swarm/SwarmCoordinator.ts +335 -0
  242. package/src/swarm/SwarmManager.ts +442 -0
  243. package/src/swarm/SwarmMembership.ts +456 -0
  244. package/src/swarm/VotingRound.ts +255 -0
  245. package/src/swarm/__tests__/ACOEngine.prod.test.ts +164 -0
  246. package/src/swarm/__tests__/ACOEngine.test.ts +117 -0
  247. package/src/swarm/__tests__/CollectiveIntelligence.prod.test.ts +296 -0
  248. package/src/swarm/__tests__/CollectiveIntelligence.test.ts +457 -0
  249. package/src/swarm/__tests__/ContributionSynthesizer.prod.test.ts +269 -0
  250. package/src/swarm/__tests__/ContributionSynthesizer.test.ts +254 -0
  251. package/src/swarm/__tests__/LeaderElection.prod.test.ts +196 -0
  252. package/src/swarm/__tests__/LeaderElection.test.ts +151 -0
  253. package/src/swarm/__tests__/PSOEngine.prod.test.ts +162 -0
  254. package/src/swarm/__tests__/PSOEngine.test.ts +106 -0
  255. package/src/swarm/__tests__/QuorumPolicy.prod.test.ts +216 -0
  256. package/src/swarm/__tests__/QuorumPolicy.test.ts +177 -0
  257. package/src/swarm/__tests__/SwarmCoordinator.prod.test.ts +186 -0
  258. package/src/swarm/__tests__/SwarmCoordinator.test.ts +167 -0
  259. package/src/swarm/__tests__/SwarmManager.prod.test.ts +308 -0
  260. package/src/swarm/__tests__/SwarmManager.test.ts +373 -0
  261. package/src/swarm/__tests__/SwarmMembership.prod.test.ts +273 -0
  262. package/src/swarm/__tests__/SwarmMembership.test.ts +264 -0
  263. package/src/swarm/__tests__/VotingRound.prod.test.ts +233 -0
  264. package/src/swarm/__tests__/VotingRound.test.ts +174 -0
  265. package/src/swarm/analytics/SwarmInspector.ts +476 -0
  266. package/src/swarm/analytics/SwarmMetrics.ts +449 -0
  267. package/src/swarm/analytics/__tests__/SwarmInspector.prod.test.ts +366 -0
  268. package/src/swarm/analytics/__tests__/SwarmInspector.test.ts +454 -0
  269. package/src/swarm/analytics/__tests__/SwarmMetrics.prod.test.ts +254 -0
  270. package/src/swarm/analytics/__tests__/SwarmMetrics.test.ts +370 -0
  271. package/src/swarm/analytics/index.ts +7 -0
  272. package/src/swarm/index.ts +69 -0
  273. package/src/swarm/messaging/BroadcastChannel.ts +509 -0
  274. package/src/swarm/messaging/GossipProtocol.ts +565 -0
  275. package/src/swarm/messaging/SwarmEventBus.ts +443 -0
  276. package/src/swarm/messaging/__tests__/BroadcastChannel.prod.test.ts +331 -0
  277. package/src/swarm/messaging/__tests__/BroadcastChannel.test.ts +333 -0
  278. package/src/swarm/messaging/__tests__/GossipProtocol.prod.test.ts +356 -0
  279. package/src/swarm/messaging/__tests__/GossipProtocol.test.ts +437 -0
  280. package/src/swarm/messaging/__tests__/SwarmEventBus.prod.test.ts +191 -0
  281. package/src/swarm/messaging/__tests__/SwarmEventBus.test.ts +247 -0
  282. package/src/swarm/messaging/index.ts +8 -0
  283. package/src/swarm/spatial/FlockingBehavior.ts +462 -0
  284. package/src/swarm/spatial/FormationController.ts +500 -0
  285. package/src/swarm/spatial/Vector3.ts +170 -0
  286. package/src/swarm/spatial/ZoneClaiming.ts +509 -0
  287. package/src/swarm/spatial/__tests__/FlockingBehavior.prod.test.ts +239 -0
  288. package/src/swarm/spatial/__tests__/FlockingBehavior.test.ts +298 -0
  289. package/src/swarm/spatial/__tests__/FormationController.prod.test.ts +240 -0
  290. package/src/swarm/spatial/__tests__/FormationController.test.ts +297 -0
  291. package/src/swarm/spatial/__tests__/Vector3.prod.test.ts +283 -0
  292. package/src/swarm/spatial/__tests__/Vector3.test.ts +224 -0
  293. package/src/swarm/spatial/__tests__/ZoneClaiming.prod.test.ts +246 -0
  294. package/src/swarm/spatial/__tests__/ZoneClaiming.test.ts +374 -0
  295. package/src/swarm/spatial/index.ts +28 -0
  296. package/src/team.ts +1245 -0
  297. package/src/training/LRScheduler.ts +377 -0
  298. package/src/training/QualityScoringPipeline.ts +139 -0
  299. package/src/training/SoftDedup.ts +461 -0
  300. package/src/training/SparsityMonitor.ts +685 -0
  301. package/src/training/SparsityMonitorTypes.ts +209 -0
  302. package/src/training/SpatialTrainingDataGenerator.ts +1526 -0
  303. package/src/training/SpatialTrainingDataTypes.ts +216 -0
  304. package/src/training/TrainingPipelineConfig.ts +215 -0
  305. package/src/training/constants.ts +94 -0
  306. package/src/training/index.ts +138 -0
  307. package/src/training/schema.ts +147 -0
  308. package/src/training/scripts/generate-novel-use-cases-dataset.ts +272 -0
  309. package/src/training/scripts/generate-spatial-dataset.ts +521 -0
  310. package/src/training/training/data/novel-use-cases.jsonl +153 -0
  311. package/src/training/training/data/spatial-reasoning-10k.jsonl +9354 -0
  312. package/src/training/trainingmonkey/TrainingMonkeyIntegration.ts +477 -0
  313. package/src/training/trainingmonkey/TrainingMonkeyTypes.ts +230 -0
  314. package/src/training/trainingmonkey/index.ts +26 -0
  315. package/src/training/trait-mappings.ts +157 -0
  316. package/src/types/core-stubs.d.ts +113 -0
  317. package/src/types.ts +304 -0
  318. package/test-output.txt +0 -0
  319. package/test-result.json +1 -0
  320. package/tsc-errors.txt +4 -0
  321. package/tsc_output.txt +0 -0
  322. package/tsconfig.json +14 -0
  323. package/tsup-learning-esm.config.ts +12 -0
  324. package/tsup.config.ts +21 -0
  325. package/typescript-errors-2.txt +0 -0
  326. package/typescript-errors.txt +22 -0
  327. package/vitest-log-utf8.txt +268 -0
  328. package/vitest-log.txt +0 -0
  329. package/vitest.config.ts +8 -0
@@ -0,0 +1,196 @@
1
+ /**
2
+ * LeaderElection.prod.test.ts
3
+ *
4
+ * Production tests for Raft-inspired LeaderElection:
5
+ * immediate single-node election, receiveVote quorum mechanics,
6
+ * onLeaderChange callbacks, handleMessage routing, and stop() cleanup.
7
+ */
8
+
9
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
10
+ import { LeaderElection } from '../LeaderElection';
11
+
12
+ describe('LeaderElection', () => {
13
+ let le: LeaderElection;
14
+
15
+ afterEach(() => {
16
+ le?.stop();
17
+ });
18
+
19
+ // -------------------------------------------------------------------------
20
+ // Single-node election
21
+ // -------------------------------------------------------------------------
22
+ describe('single-node cluster', () => {
23
+ it('node elects itself immediately (no other members)', async () => {
24
+ le = new LeaderElection('node-1', []);
25
+ const leader = await le.startElection();
26
+ expect(leader).toBe('node-1');
27
+ le.stop();
28
+ });
29
+
30
+ it('becomes leader after startElection', async () => {
31
+ le = new LeaderElection('node-1', []);
32
+ await le.startElection();
33
+ expect(le.getRole()).toBe('leader');
34
+ le.stop();
35
+ });
36
+
37
+ it('getLeader returns self after winning', async () => {
38
+ le = new LeaderElection('node-1', []);
39
+ await le.startElection();
40
+ expect(le.getLeader()).toBe('node-1');
41
+ le.stop();
42
+ });
43
+ });
44
+
45
+ // -------------------------------------------------------------------------
46
+ // receiveVote — quorum mechanics
47
+ // -------------------------------------------------------------------------
48
+ describe('receiveVote() — quorum', () => {
49
+ it('3-node cluster: receiving both other votes makes this node leader', async () => {
50
+ le = new LeaderElection('node-1', ['node-2', 'node-3'], {
51
+ electionTimeoutMin: 9000,
52
+ electionTimeoutMax: 10000, // prevent auto-trigger
53
+ });
54
+ // Manually trigger candidate state
55
+ const electionPromise = le.startElection(); // triggers becomeCandidate
56
+ // Give it a tick to set candidate state
57
+ await new Promise((r) => setTimeout(r, 20));
58
+ le.receiveVote('node-2');
59
+ le.receiveVote('node-3');
60
+ const leader = await electionPromise;
61
+ expect(leader).toBe('node-1');
62
+ le.stop();
63
+ });
64
+
65
+ it('insufficient votes keeps node as candidate', async () => {
66
+ le = new LeaderElection('node-1', ['node-2', 'node-3', 'node-4'], {
67
+ electionTimeoutMin: 9000,
68
+ electionTimeoutMax: 10000,
69
+ quorumSize: 4, // need 4 votes to win
70
+ });
71
+ // Block local simulation by providing a no-op messageHandler
72
+ le.setMessageHandler(() => {}); // messages go nowhere → no auto-votes
73
+ le.startElection(); // triggers becomeCandidate (starts election timer, sends vote requests to handler)
74
+ await new Promise((r) => setTimeout(r, 20));
75
+ // Self vote is counted, only 1 more vote → still needs 2 more for quorum=4
76
+ le.receiveVote('node-2'); // only 2 votes total (self + node-2) < quorum of 4
77
+ expect(le.getRole()).toBe('candidate');
78
+ le.stop();
79
+ });
80
+ });
81
+
82
+ // -------------------------------------------------------------------------
83
+ // onLeaderChange callback
84
+ // -------------------------------------------------------------------------
85
+ describe('onLeaderChange()', () => {
86
+ it('fires callback when leader is elected', async () => {
87
+ le = new LeaderElection('node-1', []);
88
+ const callback = vi.fn();
89
+ le.onLeaderChange(callback);
90
+ await le.startElection();
91
+ expect(callback).toHaveBeenCalledWith('node-1');
92
+ le.stop();
93
+ });
94
+
95
+ it('unsubscribe removes callback', async () => {
96
+ le = new LeaderElection('node-1', []);
97
+ const callback = vi.fn();
98
+ const unsub = le.onLeaderChange(callback);
99
+ unsub();
100
+ await le.startElection();
101
+ expect(callback).not.toHaveBeenCalled();
102
+ le.stop();
103
+ });
104
+
105
+ it('multiple callbacks all fire', async () => {
106
+ le = new LeaderElection('node-1', []);
107
+ const cb1 = vi.fn();
108
+ const cb2 = vi.fn();
109
+ le.onLeaderChange(cb1);
110
+ le.onLeaderChange(cb2);
111
+ await le.startElection();
112
+ expect(cb1).toHaveBeenCalled();
113
+ expect(cb2).toHaveBeenCalled();
114
+ le.stop();
115
+ });
116
+ });
117
+
118
+ // -------------------------------------------------------------------------
119
+ // handleMessage — heartbeat
120
+ // -------------------------------------------------------------------------
121
+ describe('handleMessage() — heartbeat', () => {
122
+ it('receiving heartbeat with higher term makes node a follower', () => {
123
+ le = new LeaderElection('node-2', ['node-1'], {
124
+ electionTimeoutMin: 9000,
125
+ electionTimeoutMax: 10000,
126
+ });
127
+ le.handleMessage('node-1', { type: 'heartbeat', term: 5, leaderId: 'node-1' });
128
+ expect(le.getRole()).toBe('follower');
129
+ expect(le.getLeader()).toBe('node-1');
130
+ le.stop();
131
+ });
132
+
133
+ it('receiving heartbeat updates leader id', () => {
134
+ le = new LeaderElection('node-2', ['node-1']);
135
+ le.handleMessage('node-1', { type: 'heartbeat', term: 3, leaderId: 'node-1' });
136
+ expect(le.getLeader()).toBe('node-1');
137
+ le.stop();
138
+ });
139
+ });
140
+
141
+ // -------------------------------------------------------------------------
142
+ // handleMessage — request-vote
143
+ // -------------------------------------------------------------------------
144
+ describe('handleMessage() — request-vote', () => {
145
+ it('responds with grant when not yet voted in term', () => {
146
+ const sent: any[] = [];
147
+ le = new LeaderElection('node-2', ['node-1'], {
148
+ electionTimeoutMin: 9000,
149
+ electionTimeoutMax: 10000,
150
+ });
151
+ le.setMessageHandler((to, msg) => sent.push({ to, msg }));
152
+ // Term 1 vote request from node-1
153
+ le.handleMessage('node-1', { type: 'request-vote', term: 1, candidateId: 'node-1' });
154
+ const grant = sent.find((s) => s.msg.type === 'vote-response');
155
+ expect(grant).toBeDefined();
156
+ expect(grant.msg.voteGranted).toBe(true);
157
+ le.stop();
158
+ });
159
+ });
160
+
161
+ // -------------------------------------------------------------------------
162
+ // Initial state
163
+ // -------------------------------------------------------------------------
164
+ describe('initial state', () => {
165
+ it('starts as follower', () => {
166
+ le = new LeaderElection('n', ['m']);
167
+ expect(le.getRole()).toBe('follower');
168
+ le.stop();
169
+ });
170
+
171
+ it('starts with no leader', () => {
172
+ le = new LeaderElection('n', ['m']);
173
+ expect(le.getLeader()).toBeNull();
174
+ le.stop();
175
+ });
176
+ });
177
+
178
+ // -------------------------------------------------------------------------
179
+ // stop()
180
+ // -------------------------------------------------------------------------
181
+ describe('stop()', () => {
182
+ it('does not throw when called before startElection', () => {
183
+ le = new LeaderElection('n', ['m']);
184
+ expect(() => le.stop()).not.toThrow();
185
+ });
186
+
187
+ it('can be called multiple times without error', async () => {
188
+ le = new LeaderElection('n', []);
189
+ await le.startElection();
190
+ expect(() => {
191
+ le.stop();
192
+ le.stop();
193
+ }).not.toThrow();
194
+ });
195
+ });
196
+ });
@@ -0,0 +1,151 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { LeaderElection } from '../LeaderElection';
3
+ import type { ElectionMessage } from '../LeaderElection';
4
+
5
+ describe('LeaderElection', () => {
6
+ afterEach(() => {
7
+ vi.useRealTimers();
8
+ });
9
+
10
+ function makeElection(nodeId: string, members: string[], config = {}) {
11
+ return new LeaderElection(nodeId, members, {
12
+ electionTimeoutMin: 10000,
13
+ electionTimeoutMax: 20000,
14
+ heartbeatInterval: 5000,
15
+ ...config,
16
+ });
17
+ }
18
+
19
+ it('initializes as follower with no leader', () => {
20
+ const e = makeElection('n1', ['n2', 'n3']);
21
+ expect(e.getRole()).toBe('follower');
22
+ expect(e.getLeader()).toBeNull();
23
+ e.stop();
24
+ });
25
+
26
+ it('filters self from cluster members', () => {
27
+ // If self is in member list, should not cause issues
28
+ const e = makeElection('n1', ['n1', 'n2', 'n3']);
29
+ e.stop();
30
+ expect(e.getRole()).toBe('follower');
31
+ });
32
+
33
+ it('wins election in local cluster (no message handler)', async () => {
34
+ vi.useFakeTimers();
35
+ const e = makeElection('n1', ['n2', 'n3']);
36
+ const promise = e.startElection();
37
+ vi.advanceTimersByTime(100);
38
+ const leader = await promise;
39
+ expect(leader).toBe('n1');
40
+ expect(e.getRole()).toBe('leader');
41
+ expect(e.getLeader()).toBe('n1');
42
+ e.stop();
43
+ });
44
+
45
+ it('accepts votes from peers', () => {
46
+ vi.useFakeTimers();
47
+ const e = makeElection('n1', ['n2', 'n3', 'n4', 'n5'], { quorumSize: 3 });
48
+ // Manually become a candidate
49
+ e.startElection();
50
+ // Already received own vote + all members' votes in local mode
51
+ expect(e.getRole()).toBe('leader');
52
+ e.stop();
53
+ });
54
+
55
+ it('receiveVote only counts when candidate', () => {
56
+ const e = makeElection('n1', ['n2']);
57
+ e.receiveVote('n2'); // Should be ignored, not a candidate
58
+ expect(e.getRole()).toBe('follower');
59
+ e.stop();
60
+ });
61
+
62
+ it('onLeaderChange fires on leader win', async () => {
63
+ vi.useFakeTimers();
64
+ const e = makeElection('n1', ['n2']);
65
+ const cb = vi.fn();
66
+ e.onLeaderChange(cb);
67
+ const promise = e.startElection();
68
+ vi.advanceTimersByTime(100);
69
+ await promise;
70
+ expect(cb).toHaveBeenCalledWith('n1');
71
+ e.stop();
72
+ });
73
+
74
+ it('onLeaderChange returns unsubscribe fn', () => {
75
+ vi.useFakeTimers();
76
+ const e = makeElection('n1', ['n2']);
77
+ const cb = vi.fn();
78
+ const unsub = e.onLeaderChange(cb);
79
+ unsub();
80
+ e.startElection();
81
+ vi.advanceTimersByTime(100);
82
+ expect(cb).not.toHaveBeenCalled();
83
+ e.stop();
84
+ });
85
+
86
+ it('handleMessage handles vote request (grants vote)', () => {
87
+ vi.useFakeTimers();
88
+ const e = makeElection('n2', ['n1']);
89
+ const sent: { to: string; msg: ElectionMessage }[] = [];
90
+ e.setMessageHandler((to, msg) => sent.push({ to, msg }));
91
+ // n1 asks n2 for a vote
92
+ e.handleMessage('n1', { type: 'request-vote', term: 1, candidateId: 'n1' });
93
+ expect(sent.length).toBeGreaterThan(0);
94
+ const resp = sent.find((s) => s.msg.type === 'vote-response');
95
+ expect(resp).toBeDefined();
96
+ expect((resp!.msg as any).voteGranted).toBe(true);
97
+ e.stop();
98
+ });
99
+
100
+ it('handleMessage rejects old-term vote request', () => {
101
+ vi.useFakeTimers();
102
+ const e = makeElection('n2', ['n1']);
103
+ // Force term to 5
104
+ e.handleMessage('n1', { type: 'heartbeat', term: 5, leaderId: 'n1' });
105
+ const sent: { to: string; msg: ElectionMessage }[] = [];
106
+ e.setMessageHandler((to, msg) => sent.push({ to, msg }));
107
+ // Request with old term
108
+ e.handleMessage('n1', { type: 'request-vote', term: 2, candidateId: 'n1' });
109
+ expect(sent.filter((s) => s.msg.type === 'vote-response')).toHaveLength(0);
110
+ e.stop();
111
+ });
112
+
113
+ it('handleMessage handles heartbeat (become follower)', () => {
114
+ vi.useFakeTimers();
115
+ const e = makeElection('n2', ['n1']);
116
+ e.handleMessage('n1', { type: 'heartbeat', term: 1, leaderId: 'n1' });
117
+ expect(e.getRole()).toBe('follower');
118
+ expect(e.getLeader()).toBe('n1');
119
+ e.stop();
120
+ });
121
+
122
+ it('handleMessage handles vote response with higher term (step down)', () => {
123
+ vi.useFakeTimers();
124
+ const e = makeElection('n1', ['n2', 'n3', 'n4'], { quorumSize: 100 }); // huge quorum so we stay candidate
125
+ e.setMessageHandler(() => {}); // prevent auto-vote
126
+ e.startElection();
127
+ // Now n1 is candidate. Receive vote response with higher term
128
+ e.handleMessage('n2', { type: 'vote-response', term: 999, voteGranted: false });
129
+ expect(e.getRole()).toBe('follower');
130
+ e.stop();
131
+ });
132
+
133
+ it('single-node cluster wins immediately', async () => {
134
+ vi.useFakeTimers();
135
+ const e = makeElection('solo', []);
136
+ const promise = e.startElection();
137
+ vi.advanceTimersByTime(100);
138
+ const leader = await promise;
139
+ expect(leader).toBe('solo');
140
+ e.stop();
141
+ });
142
+
143
+ it('stop clears all timers', () => {
144
+ vi.useFakeTimers();
145
+ const e = makeElection('n1', ['n2']);
146
+ e.startElection();
147
+ e.stop();
148
+ // Should not throw or cause issues after stop
149
+ expect(e.getLeader()).toBeTruthy(); // Was already elected
150
+ });
151
+ });
@@ -0,0 +1,162 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { PSOEngine, type PSOConfig } from '../PSOEngine';
3
+
4
+ // ─── helpers ────────────────────────────────────────────────────────────────
5
+
6
+ function mkPSO(cfg?: Partial<PSOConfig>) {
7
+ return new PSOEngine(cfg);
8
+ }
9
+
10
+ /** Perfect fitness fn — rewards assignment[i] === targetAgentIdx */
11
+ function targetFitness(target: number[], agentCount: number) {
12
+ return (assignment: number[]) => {
13
+ let score = 0;
14
+ for (let i = 0; i < assignment.length; i++) {
15
+ if (assignment[i] === target[i]) score += 10;
16
+ }
17
+ return score;
18
+ };
19
+ }
20
+
21
+ /** Constant fitness fn — always returns same value */
22
+ const constantFitness = (v: number) => (_: number[]) => v;
23
+
24
+ // ─── tests ───────────────────────────────────────────────────────────────────
25
+
26
+ describe('PSOEngine — construction / defaultConfig', () => {
27
+ it('creates with empty config', () => expect(() => mkPSO()).not.toThrow());
28
+ it('default inertiaWeight = 0.729', () => {
29
+ // Run a tiny optimization and check it doesn't blow up
30
+ expect(mkPSO()).toBeDefined();
31
+ });
32
+ it('custom config is respected', () => {
33
+ const pso = mkPSO({ populationSize: 5, maxIterations: 2 });
34
+ expect(pso).toBeDefined();
35
+ });
36
+ });
37
+
38
+ describe('PSOEngine — getRecommendedPopulation', () => {
39
+ const pso = mkPSO();
40
+ it('returns min 10 for tiny problems', () => {
41
+ expect(pso.getRecommendedPopulation(1)).toBeGreaterThanOrEqual(10);
42
+ });
43
+ it('returns max 50 for large problems', () => {
44
+ expect(pso.getRecommendedPopulation(1000)).toBeLessThanOrEqual(50);
45
+ });
46
+ it('scales with problem size (5 > 1)', () => {
47
+ expect(pso.getRecommendedPopulation(5)).toBeGreaterThanOrEqual(pso.getRecommendedPopulation(1));
48
+ });
49
+ it('returns integer', () => {
50
+ const r = pso.getRecommendedPopulation(6);
51
+ expect(r).toBe(Math.round(r));
52
+ });
53
+ });
54
+
55
+ describe('PSOEngine — optimize result shape', () => {
56
+ it('returns all required fields', async () => {
57
+ const pso = mkPSO({ populationSize: 5, maxIterations: 3 });
58
+ const result = await pso.optimize(2, 3, constantFitness(1));
59
+ expect(result).toHaveProperty('bestSolution');
60
+ expect(result).toHaveProperty('bestFitness');
61
+ expect(result).toHaveProperty('converged');
62
+ expect(result).toHaveProperty('iterations');
63
+ expect(result).toHaveProperty('fitnessHistory');
64
+ });
65
+ it('bestSolution has length = taskCount', async () => {
66
+ const pso = mkPSO({ populationSize: 5, maxIterations: 3 });
67
+ const result = await pso.optimize(3, 5, constantFitness(0));
68
+ expect(result.bestSolution).toHaveLength(5);
69
+ });
70
+ it('each solution element is a valid agent index', async () => {
71
+ const agentCount = 4;
72
+ const pso = mkPSO({ populationSize: 5, maxIterations: 3 });
73
+ const result = await pso.optimize(agentCount, 6, constantFitness(0));
74
+ result.bestSolution.forEach((idx) => {
75
+ expect(idx).toBeGreaterThanOrEqual(0);
76
+ expect(idx).toBeLessThan(agentCount);
77
+ });
78
+ });
79
+ it('iterations > 0', async () => {
80
+ const pso = mkPSO({ populationSize: 5, maxIterations: 5 });
81
+ const result = await pso.optimize(2, 2, constantFitness(1));
82
+ expect(result.iterations).toBeGreaterThan(0);
83
+ });
84
+ it('fitnessHistory is non-empty array', async () => {
85
+ const pso = mkPSO({ populationSize: 5, maxIterations: 3 });
86
+ const result = await pso.optimize(2, 2, constantFitness(5));
87
+ expect(Array.isArray(result.fitnessHistory)).toBe(true);
88
+ expect(result.fitnessHistory.length).toBeGreaterThan(0);
89
+ });
90
+ it('bestFitness matches last fitnessHistory value (non-decreasing)', async () => {
91
+ const pso = mkPSO({ populationSize: 10, maxIterations: 5 });
92
+ const result = await pso.optimize(2, 3, constantFitness(7));
93
+ const maxHistory = Math.max(...result.fitnessHistory);
94
+ expect(result.bestFitness).toBeCloseTo(maxHistory, 9);
95
+ });
96
+ it('converged = boolean', async () => {
97
+ const pso = mkPSO({ populationSize: 5, maxIterations: 3 });
98
+ const result = await pso.optimize(2, 2, constantFitness(0));
99
+ expect(typeof result.converged).toBe('boolean');
100
+ });
101
+ });
102
+
103
+ describe('PSOEngine — convergence behavior', () => {
104
+ it('converges when fitness is constant (no improvement)', async () => {
105
+ // constant fitness with zero improvement should trigger convergence after >10 iterations
106
+ const pso = mkPSO({ populationSize: 5, maxIterations: 50, convergenceThreshold: 0.001 });
107
+ const result = await pso.optimize(2, 2, constantFitness(42));
108
+ expect(result.converged).toBe(true);
109
+ });
110
+ it('does NOT converge when maxIterations = 5 (fewer than 10 required to check)', async () => {
111
+ const pso = mkPSO({ populationSize: 5, maxIterations: 5 });
112
+ const result = await pso.optimize(2, 2, constantFitness(1));
113
+ // With only 5 iterations, convergence cannot be detected (needs >10)
114
+ expect(result.converged).toBe(false);
115
+ expect(result.iterations).toBeLessThanOrEqual(6); // 5 + buffer
116
+ });
117
+ });
118
+
119
+ describe('PSOEngine — fitness optimization', () => {
120
+ it('finds high-fitness solution with obvious landscape', async () => {
121
+ const agentCount = 2;
122
+ const taskCount = 2;
123
+ // Fitness = sum of (agentIndex) — higher agent index is better
124
+ // PSO should prefer index 1 over 0
125
+ const pso = mkPSO({ populationSize: 20, maxIterations: 50 });
126
+ const result = await pso.optimize(agentCount, taskCount, (assignment) =>
127
+ assignment.reduce((acc, a) => acc + a, 0)
128
+ );
129
+ // All tasks should be assigned to agent 1
130
+ expect(result.bestFitness).toBeGreaterThan(0);
131
+ });
132
+ it('single agent — all tasks must go to agent 0', async () => {
133
+ const pso = mkPSO({ populationSize: 5, maxIterations: 5 });
134
+ const result = await pso.optimize(1, 3, constantFitness(1));
135
+ result.bestSolution.forEach((idx) => expect(idx).toBe(0));
136
+ });
137
+ it('fitness of bestSolution equals bestFitness', async () => {
138
+ const fitnessMap: Record<string, number> = {};
139
+ const fn = (a: number[]) => {
140
+ const k = a.join('-');
141
+ if (!fitnessMap[k]) fitnessMap[k] = Math.random() * 10;
142
+ return fitnessMap[k];
143
+ };
144
+ const pso = mkPSO({ populationSize: 10, maxIterations: 10 });
145
+ const result = await pso.optimize(3, 3, fn);
146
+ // The reported bestFitness must be ≥ any fitness seen in history
147
+ result.fitnessHistory.forEach((f) => {
148
+ expect(result.bestFitness).toBeGreaterThanOrEqual(f - 1e-9);
149
+ });
150
+ });
151
+ });
152
+
153
+ describe('PSOEngine — velocity clamp', () => {
154
+ it('does not throw with velocityClamp = 0.1', async () => {
155
+ const pso = mkPSO({ velocityClamp: 0.1, populationSize: 5, maxIterations: 3 });
156
+ await expect(pso.optimize(2, 2, constantFitness(1))).resolves.toBeDefined();
157
+ });
158
+ it('does not throw with very large velocityClamp', async () => {
159
+ const pso = mkPSO({ velocityClamp: 100, populationSize: 5, maxIterations: 3 });
160
+ await expect(pso.optimize(2, 2, constantFitness(1))).resolves.toBeDefined();
161
+ });
162
+ });
@@ -0,0 +1,106 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { PSOEngine, type PSOConfig } from '../PSOEngine';
3
+
4
+ describe('PSOEngine', () => {
5
+ let engine: PSOEngine;
6
+
7
+ beforeEach(() => {
8
+ engine = new PSOEngine();
9
+ });
10
+
11
+ describe('constructor', () => {
12
+ it('should create with default config', () => {
13
+ expect(engine).toBeDefined();
14
+ });
15
+
16
+ it('should accept custom config', () => {
17
+ const customConfig: Partial<PSOConfig> = {
18
+ populationSize: 50,
19
+ maxIterations: 200,
20
+ };
21
+ const customEngine = new PSOEngine(customConfig);
22
+ expect(customEngine).toBeDefined();
23
+ });
24
+ });
25
+
26
+ describe('optimize', () => {
27
+ it('should find optimal assignment for simple problem', async () => {
28
+ // Simple fitness: prefer lower agent indices
29
+ const fitnessFunction = (assignment: number[]) => {
30
+ return assignment.reduce((sum, agent) => sum - agent, 0);
31
+ };
32
+
33
+ const result = await engine.optimize(3, 5, fitnessFunction);
34
+
35
+ expect(result.bestSolution).toHaveLength(5);
36
+ expect(result.bestFitness).toBeDefined();
37
+ expect(result.iterations).toBeGreaterThan(0);
38
+ expect(result.fitnessHistory).toBeDefined();
39
+ expect(result.fitnessHistory.length).toBeGreaterThan(0);
40
+ });
41
+
42
+ it('should converge on known optimization problem', async () => {
43
+ // Fitness: all tasks to agent 0 is optimal
44
+ const fitnessFunction = (assignment: number[]) => {
45
+ return assignment.filter((a) => a === 0).length * 10;
46
+ };
47
+
48
+ const result = await engine.optimize(3, 10, fitnessFunction);
49
+
50
+ // Should find many assignments to agent 0
51
+ const agent0Count = result.bestSolution.filter((a) => a === 0).length;
52
+ expect(agent0Count).toBeGreaterThan(5);
53
+ });
54
+
55
+ it('should respect agent count bounds', async () => {
56
+ const agentCount = 5;
57
+ const result = await engine.optimize(agentCount, 20, () => 1);
58
+
59
+ expect(result.bestSolution.every((a) => a >= 0 && a < agentCount)).toBe(true);
60
+ });
61
+
62
+ it('should return discrete integer assignments', async () => {
63
+ const result = await engine.optimize(4, 10, () => Math.random());
64
+
65
+ expect(result.bestSolution.every((a) => Number.isInteger(a))).toBe(true);
66
+ });
67
+
68
+ it('should track fitness history', async () => {
69
+ const result = await engine.optimize(3, 5, (a) => a.length);
70
+
71
+ expect(result.fitnessHistory.length).toBeGreaterThan(1);
72
+ // First entry should exist
73
+ expect(result.fitnessHistory[0]).toBeDefined();
74
+ });
75
+
76
+ it('should detect convergence early', async () => {
77
+ // Constant fitness - should converge quickly
78
+ const engine = new PSOEngine({
79
+ maxIterations: 100,
80
+ convergenceThreshold: 0.01,
81
+ });
82
+
83
+ const result = await engine.optimize(3, 5, () => 100);
84
+
85
+ // Should converge before max iterations
86
+ expect(result.iterations).toBeLessThan(100);
87
+ expect(result.converged).toBe(true);
88
+ });
89
+ });
90
+
91
+ describe('getRecommendedPopulation', () => {
92
+ it('should return at least 10 for small problems', () => {
93
+ expect(engine.getRecommendedPopulation(1)).toBeGreaterThanOrEqual(10);
94
+ });
95
+
96
+ it('should cap at 50 for large problems', () => {
97
+ expect(engine.getRecommendedPopulation(10000)).toBeLessThanOrEqual(50);
98
+ });
99
+
100
+ it('should scale with problem size', () => {
101
+ const small = engine.getRecommendedPopulation(5);
102
+ const large = engine.getRecommendedPopulation(100);
103
+ expect(large).toBeGreaterThanOrEqual(small);
104
+ });
105
+ });
106
+ });