@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,349 @@
1
+ /**
2
+ * Tests for FW-1.0 Self-Improvement Module
3
+ *
4
+ * FrameworkAbsorber, TestGenerator, PromptOptimizer
5
+ */
6
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
7
+
8
+ // Mock fetch globally
9
+ const mockFetch = vi.fn();
10
+ vi.stubGlobal('fetch', mockFetch);
11
+
12
+ // Mock callLLM
13
+ vi.mock('../llm/llm-adapter', () => ({
14
+ callLLM: vi.fn(),
15
+ }));
16
+
17
+ // Mock fs for TestGenerator
18
+ vi.mock('fs', () => ({
19
+ readFileSync: vi.fn(() => 'export function add(a: number, b: number): number { return a + b; }'),
20
+ existsSync: vi.fn(() => true),
21
+ }));
22
+
23
+ import { FrameworkAbsorber } from '../self-improve/framework-absorber';
24
+ import { TestGenerator } from '../self-improve/test-generator';
25
+ import { PromptOptimizer } from '../self-improve/prompt-optimizer';
26
+ import { callLLM } from '../llm/llm-adapter';
27
+ import { existsSync } from 'fs';
28
+
29
+ const mockedCallLLM = vi.mocked(callLLM);
30
+ const mockedExistsSync = vi.mocked(existsSync);
31
+
32
+ // ── FrameworkAbsorber ──
33
+
34
+ describe('FrameworkAbsorber', () => {
35
+ beforeEach(() => {
36
+ vi.clearAllMocks();
37
+ });
38
+
39
+ it('constructs with default config', () => {
40
+ const absorber = new FrameworkAbsorber();
41
+ expect(absorber).toBeInstanceOf(FrameworkAbsorber);
42
+ });
43
+
44
+ it('constructs with custom config', () => {
45
+ const absorber = new FrameworkAbsorber({
46
+ absorbUrl: 'https://custom.absorb.net',
47
+ absorbApiKey: 'test-key',
48
+ codebasePath: '/tmp/code',
49
+ });
50
+ expect(absorber).toBeInstanceOf(FrameworkAbsorber);
51
+ });
52
+
53
+ it('scanSelf falls back to knowledge store when absorb is unreachable', async () => {
54
+ mockFetch.mockRejectedValueOnce(new Error('ECONNREFUSED'));
55
+ // Second call for knowledge store query
56
+ mockFetch.mockResolvedValueOnce({
57
+ ok: true,
58
+ json: async () => ({
59
+ results: [
60
+ { type: 'gotcha', content: 'Test gotcha', metadata: { domain: 'framework' } },
61
+ ],
62
+ }),
63
+ });
64
+
65
+ const absorber = new FrameworkAbsorber({ absorbApiKey: 'test-key', mcpApiKey: 'test-key' });
66
+ const graph = await absorber.scanSelf();
67
+
68
+ expect(graph).toBeDefined();
69
+ expect(typeof graph.fileCount).toBe('number');
70
+ expect(typeof graph.edgeCount).toBe('number');
71
+ expect(Array.isArray(graph.modules)).toBe(true);
72
+ });
73
+
74
+ it('scanSelf returns graph from absorb MCP when reachable', async () => {
75
+ // Mock the MCP JSON-RPC response from absorb_run_absorb
76
+ mockFetch.mockResolvedValueOnce({
77
+ ok: true,
78
+ json: async () => ({
79
+ result: {
80
+ content: [{ type: 'text', text: JSON.stringify({ file_count: 42, edge_count: 100, modules: ['core', 'self-improve'] }) }],
81
+ },
82
+ }),
83
+ });
84
+
85
+ const absorber = new FrameworkAbsorber({ absorbApiKey: 'test-key' });
86
+ const graph = await absorber.scanSelf();
87
+
88
+ expect(graph.fileCount).toBe(42);
89
+ expect(graph.edgeCount).toBe(100);
90
+ expect(graph.modules).toEqual(['core', 'self-improve']);
91
+ });
92
+
93
+ it('scanSelf returns empty graph when no API key', async () => {
94
+ // No key, no absorb call
95
+ mockFetch.mockResolvedValueOnce({
96
+ ok: false,
97
+ json: async () => ({}),
98
+ });
99
+
100
+ const absorber = new FrameworkAbsorber({ absorbApiKey: '', mcpApiKey: '' });
101
+ const graph = await absorber.scanSelf();
102
+
103
+ expect(graph.fileCount).toBe(0);
104
+ expect(graph.edgeCount).toBe(0);
105
+ });
106
+
107
+ it('findImprovements returns improvement list from knowledge store', async () => {
108
+ // findImprovements calls runFullScan which calls scanFramework (knowledge store query)
109
+ mockFetch.mockResolvedValueOnce({
110
+ ok: true,
111
+ json: async () => ({
112
+ results: [
113
+ { type: 'gotcha', content: 'Missing test coverage for X', metadata: { domain: 'framework', confidence: 0.8 } },
114
+ { type: 'pattern', content: 'Use dependency injection', metadata: { domain: 'architecture' } },
115
+ ],
116
+ }),
117
+ });
118
+
119
+ const absorber = new FrameworkAbsorber({ absorbApiKey: '', mcpApiKey: 'test-key' });
120
+ const improvements = await absorber.findImprovements();
121
+
122
+ expect(Array.isArray(improvements)).toBe(true);
123
+ // Only gotchas become improvements
124
+ expect(improvements.length).toBe(1);
125
+ expect(improvements[0].title).toContain('Missing test coverage');
126
+ expect(improvements[0].confidence).toBeGreaterThan(0);
127
+ });
128
+
129
+ it('getKnowledge returns empty array before scan', () => {
130
+ const absorber = new FrameworkAbsorber();
131
+ expect(absorber.getKnowledge()).toEqual([]);
132
+ });
133
+ });
134
+
135
+ // ── TestGenerator ──
136
+
137
+ describe('TestGenerator', () => {
138
+ const modelConfig = { provider: 'anthropic' as const, model: 'claude-sonnet-4-20250514' };
139
+
140
+ beforeEach(() => {
141
+ vi.clearAllMocks();
142
+ mockedExistsSync.mockReturnValue(true);
143
+ });
144
+
145
+ it('constructs with config', () => {
146
+ const gen = new TestGenerator({ model: modelConfig });
147
+ expect(gen).toBeInstanceOf(TestGenerator);
148
+ });
149
+
150
+ it('generateTests calls LLM and returns test content', async () => {
151
+ const testOutput = `import { describe, it, expect } from 'vitest';
152
+ import { add } from '../add';
153
+
154
+ describe('add', () => {
155
+ it('adds two numbers', () => {
156
+ expect(add(1, 2)).toBe(3);
157
+ });
158
+
159
+ it('handles negative numbers', () => {
160
+ expect(add(-1, 1)).toBe(0);
161
+ });
162
+ });`;
163
+
164
+ mockedCallLLM.mockResolvedValueOnce({
165
+ content: testOutput,
166
+ model: 'claude-sonnet-4-20250514',
167
+ provider: 'anthropic',
168
+ tokensUsed: 500,
169
+ });
170
+
171
+ const gen = new TestGenerator({ model: modelConfig });
172
+ const result = await gen.generateTests('src/add.ts');
173
+
174
+ expect(result.testContent).toContain("describe('add'");
175
+ expect(result.testCount).toBe(2);
176
+ expect(result.tokensUsed).toBe(500);
177
+ expect(mockedCallLLM).toHaveBeenCalledOnce();
178
+ });
179
+
180
+ it('strips markdown fences from LLM output', async () => {
181
+ mockedCallLLM.mockResolvedValueOnce({
182
+ content: '```typescript\nit("works", () => {});\n```',
183
+ model: 'test',
184
+ provider: 'anthropic',
185
+ });
186
+
187
+ const gen = new TestGenerator({ model: modelConfig });
188
+ const result = await gen.generateTests('src/foo.ts');
189
+
190
+ expect(result.testContent).not.toContain('```');
191
+ expect(result.testContent).toContain('it("works"');
192
+ });
193
+
194
+ it('throws when source file does not exist', async () => {
195
+ mockedExistsSync.mockReturnValue(false);
196
+
197
+ const gen = new TestGenerator({ model: modelConfig });
198
+ await expect(gen.generateTests('nonexistent.ts')).rejects.toThrow('Source file not found');
199
+ });
200
+
201
+ it('generateBatch handles multiple files', async () => {
202
+ mockedCallLLM.mockResolvedValue({
203
+ content: 'it("test", () => {});',
204
+ model: 'test',
205
+ provider: 'anthropic',
206
+ tokensUsed: 100,
207
+ });
208
+
209
+ const gen = new TestGenerator({ model: modelConfig });
210
+ const results = await gen.generateBatch(['src/a.ts', 'src/b.ts']);
211
+
212
+ expect(results.length).toBe(2);
213
+ expect(mockedCallLLM).toHaveBeenCalledTimes(2);
214
+ });
215
+
216
+ it('generateBatch skips files that fail', async () => {
217
+ mockedExistsSync.mockReturnValueOnce(false).mockReturnValueOnce(true);
218
+ mockedCallLLM.mockResolvedValueOnce({
219
+ content: 'it("works", () => {});',
220
+ model: 'test',
221
+ provider: 'anthropic',
222
+ tokensUsed: 50,
223
+ });
224
+
225
+ const gen = new TestGenerator({ model: modelConfig });
226
+ const results = await gen.generateBatch(['nonexistent.ts', 'src/ok.ts']);
227
+
228
+ expect(results.length).toBe(1);
229
+ });
230
+ });
231
+
232
+ // ── PromptOptimizer ──
233
+
234
+ describe('PromptOptimizer', () => {
235
+ const modelConfig = { provider: 'anthropic' as const, model: 'claude-sonnet-4-20250514' };
236
+
237
+ beforeEach(() => {
238
+ vi.clearAllMocks();
239
+ });
240
+
241
+ it('constructs with config', () => {
242
+ const opt = new PromptOptimizer({ model: modelConfig });
243
+ expect(opt).toBeInstanceOf(PromptOptimizer);
244
+ });
245
+
246
+ it('abTest runs both variants and returns a winner', async () => {
247
+ // Mock callLLM: 3 runs per variant (6 calls) + 6 judge calls = 12 total
248
+ let callCount = 0;
249
+ mockedCallLLM.mockImplementation(async () => {
250
+ callCount++;
251
+ // Judge calls return JSON scores
252
+ if (callCount % 2 === 0) {
253
+ return {
254
+ content: JSON.stringify({ relevance: 8, quality: 7, completeness: 9 }),
255
+ model: 'test',
256
+ provider: 'anthropic',
257
+ tokensUsed: 50,
258
+ };
259
+ }
260
+ return {
261
+ content: 'Test response content',
262
+ model: 'test',
263
+ provider: 'anthropic',
264
+ tokensUsed: 200,
265
+ };
266
+ });
267
+
268
+ const opt = new PromptOptimizer({ model: modelConfig, runs: 2 });
269
+ const result = await opt.abTest(
270
+ 'Explain this briefly.',
271
+ 'You are an expert. Explain this in detail.',
272
+ 'What is TypeScript?'
273
+ );
274
+
275
+ expect(result.task).toBe('What is TypeScript?');
276
+ expect(result.variantA).toBeDefined();
277
+ expect(result.variantB).toBeDefined();
278
+ expect(['A', 'B', 'tie']).toContain(result.winner);
279
+ expect(result.confidence).toBeGreaterThanOrEqual(0);
280
+ expect(result.confidence).toBeLessThanOrEqual(1);
281
+ expect(result.totalTokens).toBeGreaterThanOrEqual(0);
282
+ expect(result.variantA.responses.length).toBe(2);
283
+ expect(result.variantB.responses.length).toBe(2);
284
+ });
285
+
286
+ it('handles judge failures gracefully (neutral score)', async () => {
287
+ mockedCallLLM.mockImplementation(async (_config, messages) => {
288
+ // Judge prompt contains "Score the following"
289
+ const isJudge = messages.some(m => m.content.includes('Score the following') || m.content.includes('prompt quality evaluator'));
290
+ if (isJudge) {
291
+ throw new Error('LLM quota exceeded');
292
+ }
293
+ return {
294
+ content: 'Some response',
295
+ model: 'test',
296
+ provider: 'anthropic',
297
+ tokensUsed: 100,
298
+ };
299
+ });
300
+
301
+ const opt = new PromptOptimizer({ model: modelConfig, runs: 1 });
302
+ const result = await opt.abTest('Prompt A', 'Prompt B', 'Test task');
303
+
304
+ // Both variants should get neutral scores (5) when judge fails
305
+ expect(result.variantA.avgScore).toBe(5);
306
+ expect(result.variantB.avgScore).toBe(5);
307
+ expect(result.winner).toBe('tie');
308
+ });
309
+
310
+ it('reports correct variant scores', async () => {
311
+ // Make variant A consistently better than variant B
312
+ mockedCallLLM.mockImplementation(async (_config, messages) => {
313
+ const systemMsg = messages.find(m => m.role === 'system');
314
+ const isJudge = systemMsg?.content.includes('prompt quality evaluator');
315
+
316
+ if (isJudge) {
317
+ // Check if the response being judged is from variant A or B
318
+ const userMsg = messages.find(m => m.role === 'user');
319
+ const isVariantA = userMsg?.content.includes('Great response from A');
320
+ return {
321
+ content: JSON.stringify({
322
+ relevance: isVariantA ? 9 : 4,
323
+ quality: isVariantA ? 9 : 4,
324
+ completeness: isVariantA ? 9 : 4,
325
+ }),
326
+ model: 'test',
327
+ provider: 'anthropic',
328
+ tokensUsed: 50,
329
+ };
330
+ }
331
+
332
+ // Variant A uses system prompt "Prompt A"
333
+ const isA = systemMsg?.content === 'Prompt A';
334
+ return {
335
+ content: isA ? 'Great response from A' : 'Mediocre response from B',
336
+ model: 'test',
337
+ provider: 'anthropic',
338
+ tokensUsed: 100,
339
+ };
340
+ });
341
+
342
+ const opt = new PromptOptimizer({ model: modelConfig, runs: 2 });
343
+ const result = await opt.abTest('Prompt A', 'Prompt B', 'Test task');
344
+
345
+ expect(result.variantA.avgScore).toBeGreaterThan(result.variantB.avgScore);
346
+ expect(result.winner).toBe('A');
347
+ expect(result.confidence).toBeGreaterThan(0);
348
+ });
349
+ });
@@ -0,0 +1,237 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import {
3
+ BaseService,
4
+ ServiceLifecycle,
5
+ ServiceErrorCode,
6
+ ServiceError,
7
+ ServiceManager,
8
+ } from '../protocol/implementations';
9
+ import type { ServiceMetadata, ServiceConfig } from '../protocol/implementations';
10
+
11
+ // ── Concrete test service ──
12
+
13
+ class TestService extends BaseService {
14
+ public initCalled = false;
15
+ public readyCalled = false;
16
+ public stopCalled = false;
17
+ public failOnInit = false;
18
+
19
+ constructor(name: string, config?: Partial<ServiceConfig>) {
20
+ super({ name, version: '1.0.0', description: `Test service: ${name}` }, config);
21
+ }
22
+
23
+ protected override async onInit(): Promise<void> {
24
+ if (this.failOnInit) throw new Error('init failed');
25
+ this.initCalled = true;
26
+ }
27
+
28
+ protected override async onReady(): Promise<void> {
29
+ this.readyCalled = true;
30
+ }
31
+
32
+ protected override async onStop(): Promise<void> {
33
+ this.stopCalled = true;
34
+ }
35
+
36
+ /** Expose executeWithMetrics for testing */
37
+ async doWork(shouldFail = false): Promise<string> {
38
+ return this.executeWithMetrics(async () => {
39
+ if (shouldFail) throw new Error('work failed');
40
+ return 'done';
41
+ });
42
+ }
43
+ }
44
+
45
+ // =============================================================================
46
+ // BaseService
47
+ // =============================================================================
48
+
49
+ describe('BaseService', () => {
50
+ it('initializes through lifecycle states', async () => {
51
+ const svc = new TestService('alpha');
52
+ expect(svc.getMetadata().lifecycle).toBe(ServiceLifecycle.INITIALIZING);
53
+ expect(svc.isReady()).toBe(false);
54
+
55
+ await svc.initialize();
56
+
57
+ expect(svc.initCalled).toBe(true);
58
+ expect(svc.readyCalled).toBe(true);
59
+ expect(svc.isReady()).toBe(true);
60
+ expect(svc.getMetadata().lifecycle).toBe(ServiceLifecycle.READY);
61
+ expect(svc.getMetadata().initializedAt).toBeInstanceOf(Date);
62
+ expect(svc.getMetadata().readyAt).toBeInstanceOf(Date);
63
+ });
64
+
65
+ it('stops cleanly', async () => {
66
+ const svc = new TestService('beta');
67
+ await svc.initialize();
68
+ await svc.stop();
69
+
70
+ expect(svc.stopCalled).toBe(true);
71
+ expect(svc.isReady()).toBe(false);
72
+ expect(svc.getMetadata().lifecycle).toBe(ServiceLifecycle.STOPPED);
73
+ });
74
+
75
+ it('stop is idempotent', async () => {
76
+ const svc = new TestService('gamma');
77
+ await svc.initialize();
78
+ await svc.stop();
79
+ await svc.stop(); // second call should be a no-op
80
+ expect(svc.getMetadata().lifecycle).toBe(ServiceLifecycle.STOPPED);
81
+ });
82
+
83
+ it('records request metrics via executeWithMetrics', async () => {
84
+ const svc = new TestService('delta');
85
+ await svc.initialize();
86
+ const result = await svc.doWork();
87
+
88
+ expect(result).toBe('done');
89
+ expect(svc.getMetrics().requestCount).toBe(1);
90
+ expect(svc.getMetrics().errorCount).toBe(0);
91
+ expect(svc.getMetrics().lastRequestAt).toBeInstanceOf(Date);
92
+ });
93
+
94
+ it('records error metrics on failure', async () => {
95
+ const svc = new TestService('epsilon');
96
+ await svc.initialize();
97
+
98
+ await expect(svc.doWork(true)).rejects.toThrow('work failed');
99
+ expect(svc.getMetrics().errorCount).toBe(1);
100
+ expect(svc.getMetrics().lastErrorAt).toBeInstanceOf(Date);
101
+ });
102
+
103
+ it('uses default config values', () => {
104
+ const svc = new TestService('zeta');
105
+ const meta = svc.getMetadata();
106
+ expect(meta.name).toBe('zeta');
107
+ expect(meta.version).toBe('1.0.0');
108
+ });
109
+
110
+ it('merges custom config', () => {
111
+ const svc = new TestService('eta', { timeout: 5000, retries: 1 });
112
+ // Config is internal but we verify the service constructs without error
113
+ expect(svc.getMetadata().name).toBe('eta');
114
+ });
115
+ });
116
+
117
+ // =============================================================================
118
+ // ServiceLifecycle enum
119
+ // =============================================================================
120
+
121
+ describe('ServiceLifecycle', () => {
122
+ it('has all expected states', () => {
123
+ expect(ServiceLifecycle.INITIALIZING).toBe('initializing');
124
+ expect(ServiceLifecycle.READY).toBe('ready');
125
+ expect(ServiceLifecycle.DEGRADED).toBe('degraded');
126
+ expect(ServiceLifecycle.STOPPING).toBe('stopping');
127
+ expect(ServiceLifecycle.STOPPED).toBe('stopped');
128
+ expect(ServiceLifecycle.ERROR).toBe('error');
129
+ });
130
+ });
131
+
132
+ // =============================================================================
133
+ // ServiceError
134
+ // =============================================================================
135
+
136
+ describe('ServiceError', () => {
137
+ it('creates with code and message', () => {
138
+ const err = new ServiceError(ServiceErrorCode.NOT_FOUND, 'Resource missing', 404);
139
+ expect(err.code).toBe(ServiceErrorCode.NOT_FOUND);
140
+ expect(err.message).toBe('Resource missing');
141
+ expect(err.statusCode).toBe(404);
142
+ expect(err.name).toBe('ServiceError');
143
+ });
144
+
145
+ it('defaults to status 500', () => {
146
+ const err = new ServiceError(ServiceErrorCode.INTERNAL_ERROR, 'boom');
147
+ expect(err.statusCode).toBe(500);
148
+ });
149
+
150
+ it('carries optional details', () => {
151
+ const err = new ServiceError(ServiceErrorCode.VALIDATION_ERROR, 'bad input', 400, { field: 'name' });
152
+ expect(err.details).toEqual({ field: 'name' });
153
+ });
154
+ });
155
+
156
+ // =============================================================================
157
+ // ServiceManager
158
+ // =============================================================================
159
+
160
+ describe('ServiceManager', () => {
161
+ it('starts and stops services in correct order', async () => {
162
+ const order: string[] = [];
163
+ class OrderedService extends BaseService {
164
+ constructor(name: string) {
165
+ super({ name, version: '1.0.0', description: name });
166
+ }
167
+ protected override async onInit() { order.push(`init:${this.getMetadata().name}`); }
168
+ protected override async onStop() { order.push(`stop:${this.getMetadata().name}`); }
169
+ }
170
+
171
+ const mgr = new ServiceManager();
172
+ mgr.register(new OrderedService('db'));
173
+ mgr.register(new OrderedService('cache'));
174
+ mgr.register(new OrderedService('api'));
175
+
176
+ await mgr.startAll();
177
+ expect(order).toEqual(['init:db', 'init:cache', 'init:api']);
178
+
179
+ order.length = 0;
180
+ await mgr.stopAll();
181
+ // Services stop in reverse order
182
+ expect(order).toEqual(['stop:api', 'stop:cache', 'stop:db']);
183
+ });
184
+
185
+ it('reports aggregate health', async () => {
186
+ const mgr = new ServiceManager();
187
+ const svcA = new TestService('a');
188
+ const svcB = new TestService('b');
189
+ mgr.register(svcA);
190
+ mgr.register(svcB);
191
+
192
+ // Before starting
193
+ let h = mgr.health();
194
+ expect(h.totalServices).toBe(2);
195
+ expect(h.readyCount).toBe(0);
196
+ expect(h.allReady).toBe(false);
197
+
198
+ await mgr.startAll();
199
+ h = mgr.health();
200
+ expect(h.readyCount).toBe(2);
201
+ expect(h.allReady).toBe(true);
202
+ expect(h.services).toHaveLength(2);
203
+ expect(h.services[0].name).toBe('a');
204
+ expect(h.services[0].ready).toBe(true);
205
+ });
206
+
207
+ it('tracks size', () => {
208
+ const mgr = new ServiceManager();
209
+ expect(mgr.size).toBe(0);
210
+ mgr.register(new TestService('x'));
211
+ expect(mgr.size).toBe(1);
212
+ });
213
+
214
+ it('prevents registration after startAll', async () => {
215
+ const mgr = new ServiceManager();
216
+ mgr.register(new TestService('first'));
217
+ await mgr.startAll();
218
+ expect(() => mgr.register(new TestService('late'))).toThrow('Cannot register');
219
+ });
220
+
221
+ it('handles empty manager', async () => {
222
+ const mgr = new ServiceManager();
223
+ await mgr.startAll();
224
+ await mgr.stopAll();
225
+ const h = mgr.health();
226
+ expect(h.totalServices).toBe(0);
227
+ expect(h.allReady).toBe(false);
228
+ });
229
+
230
+ it('propagates init errors', async () => {
231
+ const mgr = new ServiceManager();
232
+ const failing = new TestService('broken');
233
+ failing.failOnInit = true;
234
+ mgr.register(failing);
235
+ await expect(mgr.startAll()).rejects.toThrow('init failed');
236
+ });
237
+ });
@@ -0,0 +1,121 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { SkillRouter } from '../skill-router';
3
+ import type { AgentConfig, TaskDef } from '../types';
4
+
5
+ function makeAgent(overrides: Partial<AgentConfig> & { name: string }): AgentConfig {
6
+ return {
7
+ role: 'coder',
8
+ model: { provider: 'anthropic', model: 'claude-sonnet-4' },
9
+ capabilities: ['code-generation'],
10
+ claimFilter: { roles: ['coder'], maxPriority: 8 },
11
+ ...overrides,
12
+ };
13
+ }
14
+
15
+ function makeTask(overrides: Partial<TaskDef> = {}): TaskDef {
16
+ return {
17
+ id: 'task-1',
18
+ title: 'Fix the parser bug',
19
+ description: 'The parser fails on nested objects',
20
+ status: 'open',
21
+ priority: 3,
22
+ createdAt: new Date().toISOString(),
23
+ ...overrides,
24
+ };
25
+ }
26
+
27
+ describe('SkillRouter', () => {
28
+ const router = new SkillRouter();
29
+
30
+ const coder = makeAgent({
31
+ name: 'Coder',
32
+ capabilities: ['code-generation', 'parser', 'typescript'],
33
+ claimFilter: { roles: ['coder'], maxPriority: 8 },
34
+ });
35
+
36
+ const reviewer = makeAgent({
37
+ name: 'Reviewer',
38
+ role: 'reviewer',
39
+ capabilities: ['code-review', 'security-audit'],
40
+ claimFilter: { roles: ['reviewer', 'tester'], maxPriority: 5 },
41
+ });
42
+
43
+ const researcher = makeAgent({
44
+ name: 'Researcher',
45
+ role: 'researcher',
46
+ capabilities: ['research', 'documentation'],
47
+ claimFilter: { roles: ['researcher'], maxPriority: 10 },
48
+ });
49
+
50
+ const agents = [coder, reviewer, researcher];
51
+
52
+ it('routes to best-fit agent by capability match', () => {
53
+ const task = makeTask({ title: 'Fix the parser bug in typescript' });
54
+ const result = router.route(task, agents);
55
+ expect(result.agent?.name).toBe('Coder');
56
+ expect(result.score).toBeGreaterThan(0);
57
+ });
58
+
59
+ it('returns null when no agents available', () => {
60
+ const task = makeTask();
61
+ const result = router.route(task, []);
62
+ expect(result.agent).toBeNull();
63
+ expect(result.reason).toContain('No agents');
64
+ });
65
+
66
+ it('filters by priority alignment', () => {
67
+ const task = makeTask({ priority: 7 });
68
+ // Reviewer has maxPriority 5, so should be filtered out
69
+ const result = router.route(task, agents);
70
+ expect(result.candidates.find(c => c.agent.name === 'Reviewer')).toBeUndefined();
71
+ });
72
+
73
+ it('scores role match bonus', () => {
74
+ const task = makeTask({
75
+ title: 'Review the authentication module',
76
+ role: 'reviewer',
77
+ });
78
+ const result = router.route(task, agents);
79
+ const reviewerCandidate = result.candidates.find(c => c.agent.name === 'Reviewer');
80
+ expect(reviewerCandidate?.roleMatch).toBe(true);
81
+ });
82
+
83
+ it('respects required capabilities filter', () => {
84
+ const task = makeTask({ title: 'Security audit of payment flow' });
85
+ const result = router.route(task, agents, {
86
+ requiredCapabilities: ['security-audit'],
87
+ });
88
+ // Only reviewer has security-audit
89
+ expect(result.candidates).toHaveLength(1);
90
+ expect(result.candidates[0].agent.name).toBe('Reviewer');
91
+ });
92
+
93
+ it('applies minimum score threshold', () => {
94
+ const task = makeTask({ title: 'Something completely unrelated' });
95
+ const result = router.route(task, agents, { minScore: 100 });
96
+ expect(result.agent).toBeNull();
97
+ expect(result.reason).toContain('below minimum threshold');
98
+ });
99
+
100
+ it('routeMultiple returns top N candidates', () => {
101
+ const task = makeTask({ title: 'Fix parser and review typescript code' });
102
+ const result = router.routeMultiple(task, agents, 2);
103
+ expect(result.candidates.length).toBeLessThanOrEqual(2);
104
+ });
105
+
106
+ it('sorts candidates by score descending', () => {
107
+ const task = makeTask({ title: 'Fix the parser bug in typescript' });
108
+ const result = router.route(task, agents);
109
+ for (let i = 1; i < result.candidates.length; i++) {
110
+ expect(result.candidates[i - 1].score).toBeGreaterThanOrEqual(result.candidates[i].score);
111
+ }
112
+ });
113
+
114
+ it('tracks matched capabilities in result', () => {
115
+ const task = makeTask({ title: 'Fix the parser bug in typescript' });
116
+ const result = router.route(task, agents);
117
+ const coderCandidate = result.candidates.find(c => c.agent.name === 'Coder');
118
+ expect(coderCandidate?.matchedCapabilities).toContain('parser');
119
+ expect(coderCandidate?.matchedCapabilities).toContain('typescript');
120
+ });
121
+ });