@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.
- package/ALL-test-results.json +1 -0
- package/CHANGELOG.md +8 -0
- package/LICENSE +21 -0
- package/ROADMAP.md +175 -0
- package/dist/AgentManifest-CB4xM-Ma.d.cts +704 -0
- package/dist/AgentManifest-CB4xM-Ma.d.ts +704 -0
- package/dist/BehaviorTree-BrBFECv5.d.cts +103 -0
- package/dist/BehaviorTree-BrBFECv5.d.ts +103 -0
- package/dist/InvisibleWallet-BB6tFvRA.d.cts +1732 -0
- package/dist/InvisibleWallet-rtRrBOA8.d.ts +1732 -0
- package/dist/OrchestratorAgent-BvWgf9uw.d.cts +798 -0
- package/dist/OrchestratorAgent-Q_CbVTmO.d.ts +798 -0
- package/dist/agents/index.cjs +4790 -0
- package/dist/agents/index.d.cts +1788 -0
- package/dist/agents/index.d.ts +1788 -0
- package/dist/agents/index.js +4695 -0
- package/dist/ai/index.cjs +5347 -0
- package/dist/ai/index.d.cts +1753 -0
- package/dist/ai/index.d.ts +1753 -0
- package/dist/ai/index.js +5244 -0
- package/dist/behavior.cjs +449 -0
- package/dist/behavior.d.cts +130 -0
- package/dist/behavior.d.ts +130 -0
- package/dist/behavior.js +407 -0
- package/dist/economy/index.cjs +3659 -0
- package/dist/economy/index.d.cts +747 -0
- package/dist/economy/index.d.ts +747 -0
- package/dist/economy/index.js +3617 -0
- package/dist/implementations-D9T3un9D.d.cts +236 -0
- package/dist/implementations-D9T3un9D.d.ts +236 -0
- package/dist/index.cjs +24550 -0
- package/dist/index.d.cts +1729 -0
- package/dist/index.d.ts +1729 -0
- package/dist/index.js +24277 -0
- package/dist/learning/index.cjs +219 -0
- package/dist/learning/index.d.cts +104 -0
- package/dist/learning/index.d.ts +104 -0
- package/dist/learning/index.js +189 -0
- package/dist/negotiation/index.cjs +970 -0
- package/dist/negotiation/index.d.cts +610 -0
- package/dist/negotiation/index.d.ts +610 -0
- package/dist/negotiation/index.js +931 -0
- package/dist/skills/index.cjs +1118 -0
- package/dist/skills/index.d.cts +289 -0
- package/dist/skills/index.d.ts +289 -0
- package/dist/skills/index.js +1079 -0
- package/dist/swarm/index.cjs +5268 -0
- package/dist/swarm/index.d.cts +2433 -0
- package/dist/swarm/index.d.ts +2433 -0
- package/dist/swarm/index.js +5221 -0
- package/dist/training/index.cjs +2745 -0
- package/dist/training/index.d.cts +1734 -0
- package/dist/training/index.d.ts +1734 -0
- package/dist/training/index.js +2687 -0
- package/extract-failures.js +10 -0
- package/package.json +82 -0
- package/src/__tests__/bounty-marketplace.test.ts +374 -0
- package/src/__tests__/delegation.test.ts +144 -0
- package/src/__tests__/distributed-claimer.test.ts +147 -0
- package/src/__tests__/done-log-audit.test.ts +342 -0
- package/src/__tests__/framework.test.ts +865 -0
- package/src/__tests__/goal-synthesizer.test.ts +236 -0
- package/src/__tests__/presence.test.ts +223 -0
- package/src/__tests__/protocol-agent.test.ts +254 -0
- package/src/__tests__/revenue-splitter.test.ts +114 -0
- package/src/__tests__/scenario-driven-todo.test.ts +197 -0
- package/src/__tests__/self-improve.test.ts +349 -0
- package/src/__tests__/service-lifecycle.test.ts +237 -0
- package/src/__tests__/skill-router.test.ts +121 -0
- package/src/agents/AgentManifest.ts +493 -0
- package/src/agents/AgentRegistry.ts +475 -0
- package/src/agents/AgentTypes.ts +585 -0
- package/src/agents/AgentWalletRegistry.ts +83 -0
- package/src/agents/AuthenticatedCRDT.ts +388 -0
- package/src/agents/CapabilityMatcher.ts +453 -0
- package/src/agents/CrossRealityHandoff.ts +305 -0
- package/src/agents/CulturalMemory.ts +454 -0
- package/src/agents/FederatedRegistryAdapter.ts +429 -0
- package/src/agents/NormEngine.ts +450 -0
- package/src/agents/OrchestratorAgent.ts +414 -0
- package/src/agents/SkillWorkflowEngine.ts +472 -0
- package/src/agents/TaskDelegationService.ts +551 -0
- package/src/agents/__tests__/AgentManifest.prod.test.ts +134 -0
- package/src/agents/__tests__/AgentManifest.test.ts +182 -0
- package/src/agents/__tests__/AgentModule.test.ts +864 -0
- package/src/agents/__tests__/AgentRegistry.prod.test.ts +125 -0
- package/src/agents/__tests__/AgentRegistry.test.ts +148 -0
- package/src/agents/__tests__/AgentTypes.test.ts +534 -0
- package/src/agents/__tests__/AgentWalletRegistry.test.ts +152 -0
- package/src/agents/__tests__/AuthenticatedCRDT.test.ts +558 -0
- package/src/agents/__tests__/CapabilityMatcher.prod.test.ts +117 -0
- package/src/agents/__tests__/CapabilityMatcher.test.ts +178 -0
- package/src/agents/__tests__/CrossRealityHandoff.test.ts +402 -0
- package/src/agents/__tests__/CulturalMemory.test.ts +200 -0
- package/src/agents/__tests__/FederatedRegistryAdapter.test.ts +409 -0
- package/src/agents/__tests__/NormEngine.test.ts +276 -0
- package/src/agents/__tests__/OrchestratorAgent.test.ts +182 -0
- package/src/agents/__tests__/SkillWorkflowEngine.test.ts +357 -0
- package/src/agents/__tests__/TaskDelegationService.test.ts +446 -0
- package/src/agents/index.ts +107 -0
- package/src/agents/spatial-comms/Layer1RealTime.ts +621 -0
- package/src/agents/spatial-comms/Layer2A2A.ts +661 -0
- package/src/agents/spatial-comms/Layer3MCP.ts +651 -0
- package/src/agents/spatial-comms/ProtocolTypes.ts +543 -0
- package/src/agents/spatial-comms/SpatialCommClient.ts +483 -0
- package/src/agents/spatial-comms/__tests__/performance-benchmark.test.ts +465 -0
- package/src/agents/spatial-comms/examples/multi-agent-world-creation.ts +409 -0
- package/src/agents/spatial-comms/index.ts +66 -0
- package/src/ai/AIAdapter.ts +313 -0
- package/src/ai/AICopilot.ts +331 -0
- package/src/ai/AIOutputValidator.ts +203 -0
- package/src/ai/BTNodes.ts +239 -0
- package/src/ai/BehaviorSelector.ts +135 -0
- package/src/ai/BehaviorTree.ts +153 -0
- package/src/ai/Blackboard.ts +165 -0
- package/src/ai/GenerationAnalytics.ts +461 -0
- package/src/ai/GenerationCache.ts +265 -0
- package/src/ai/GoalPlanner.ts +165 -0
- package/src/ai/HoloScriptGenerator.ts +580 -0
- package/src/ai/InfluenceMap.ts +180 -0
- package/src/ai/NavMesh.ts +168 -0
- package/src/ai/PerceptionSystem.ts +178 -0
- package/src/ai/PromptTemplates.ts +453 -0
- package/src/ai/SemanticSearchService.ts +80 -0
- package/src/ai/StateMachine.ts +196 -0
- package/src/ai/SteeringBehavior.ts +150 -0
- package/src/ai/SteeringBehaviors.ts +244 -0
- package/src/ai/TrainingDataGenerator.ts +1082 -0
- package/src/ai/UtilityAI.ts +145 -0
- package/src/ai/__tests__/AIAdapter.prod.test.ts +259 -0
- package/src/ai/__tests__/AIAdapter.test.ts +109 -0
- package/src/ai/__tests__/AICopilot.prod.test.ts +341 -0
- package/src/ai/__tests__/AICopilot.test.ts +178 -0
- package/src/ai/__tests__/AIOutputValidator.prod.test.ts +226 -0
- package/src/ai/__tests__/AIOutputValidator.test.ts +138 -0
- package/src/ai/__tests__/BTNodes.prod.test.ts +391 -0
- package/src/ai/__tests__/BTNodes.test.ts +263 -0
- package/src/ai/__tests__/BehaviorSelector.prod.test.ts +129 -0
- package/src/ai/__tests__/BehaviorSelector.test.ts +132 -0
- package/src/ai/__tests__/BehaviorTree.prod.test.ts +266 -0
- package/src/ai/__tests__/BehaviorTree.test.ts +216 -0
- package/src/ai/__tests__/Blackboard.prod.test.ts +339 -0
- package/src/ai/__tests__/Blackboard.test.ts +183 -0
- package/src/ai/__tests__/GenerationAnalytics.prod.test.ts +141 -0
- package/src/ai/__tests__/GenerationAnalytics.test.ts +165 -0
- package/src/ai/__tests__/GenerationCache.prod.test.ts +144 -0
- package/src/ai/__tests__/GenerationCache.test.ts +171 -0
- package/src/ai/__tests__/GoalPlanner.prod.test.ts +189 -0
- package/src/ai/__tests__/GoalPlanner.test.ts +137 -0
- package/src/ai/__tests__/GoalPlannerDepth.prod.test.ts +217 -0
- package/src/ai/__tests__/HoloScriptGenerator.test.ts +125 -0
- package/src/ai/__tests__/InfluenceMap.prod.test.ts +146 -0
- package/src/ai/__tests__/InfluenceMap.test.ts +149 -0
- package/src/ai/__tests__/NavMesh.prod.test.ts +141 -0
- package/src/ai/__tests__/NavMesh.test.ts +159 -0
- package/src/ai/__tests__/PerceptionSystem.prod.test.ts +135 -0
- package/src/ai/__tests__/PerceptionSystem.test.ts +250 -0
- package/src/ai/__tests__/PromptTemplates.prod.test.ts +313 -0
- package/src/ai/__tests__/PromptTemplates.test.ts +146 -0
- package/src/ai/__tests__/SemanticSearch.test.ts +37 -0
- package/src/ai/__tests__/StateMachine.prod.test.ts +162 -0
- package/src/ai/__tests__/StateMachine.test.ts +163 -0
- package/src/ai/__tests__/SteeringBehavior.prod.test.ts +251 -0
- package/src/ai/__tests__/SteeringBehavior.test.ts +135 -0
- package/src/ai/__tests__/SteeringBehaviors.prod.test.ts +133 -0
- package/src/ai/__tests__/SteeringBehaviors.test.ts +151 -0
- package/src/ai/__tests__/TrainingDataGenerator.prod.test.ts +286 -0
- package/src/ai/__tests__/TrainingDataGenerator.test.ts +286 -0
- package/src/ai/__tests__/UtilityAI.prod.test.ts +207 -0
- package/src/ai/__tests__/UtilityAI.test.ts +155 -0
- package/src/ai/__tests__/adapters.prod.test.ts +263 -0
- package/src/ai/__tests__/adapters.test.ts +320 -0
- package/src/ai/adapters.ts +1585 -0
- package/src/ai/index.ts +130 -0
- package/src/behavior/BehaviorPresets.ts +140 -0
- package/src/behavior/BehaviorTree.ts +236 -0
- package/src/behavior/StateMachine.ts +176 -0
- package/src/behavior/StateTrait.ts +67 -0
- package/src/behavior/index.ts +8 -0
- package/src/behavior.ts +8 -0
- package/src/board/audit.ts +284 -0
- package/src/board/board-ops.ts +336 -0
- package/src/board/board-types.ts +302 -0
- package/src/board/index.ts +69 -0
- package/src/define-agent.ts +46 -0
- package/src/define-team.ts +33 -0
- package/src/delegation.ts +265 -0
- package/src/distributed-claimer.ts +228 -0
- package/src/economy/AgentBudgetEnforcer.ts +464 -0
- package/src/economy/BountyManager.ts +185 -0
- package/src/economy/CreatorRevenueAggregator.ts +460 -0
- package/src/economy/InvisibleWallet.ts +82 -0
- package/src/economy/KnowledgeMarketplace.ts +193 -0
- package/src/economy/PaymentWebhookService.ts +512 -0
- package/src/economy/RevenueSplitter.ts +156 -0
- package/src/economy/SubscriptionManager.ts +546 -0
- package/src/economy/UnifiedBudgetOptimizer.ts +635 -0
- package/src/economy/UsageMeter.ts +440 -0
- package/src/economy/_core-stubs.ts +219 -0
- package/src/economy/index.ts +100 -0
- package/src/economy/x402-facilitator.ts +1978 -0
- package/src/index.ts +348 -0
- package/src/knowledge/__tests__/knowledge-consolidator.test.ts +444 -0
- package/src/knowledge/__tests__/knowledge-store-vector.test.ts +291 -0
- package/src/knowledge/brain.ts +167 -0
- package/src/knowledge/consolidation.ts +581 -0
- package/src/knowledge/knowledge-consolidator.ts +510 -0
- package/src/knowledge/knowledge-store.ts +616 -0
- package/src/learning/MemoryConsolidator.ts +102 -0
- package/src/learning/MemoryScorer.ts +69 -0
- package/src/learning/ProceduralCompiler.ts +45 -0
- package/src/learning/SemanticClusterer.ts +66 -0
- package/src/learning/index.ts +8 -0
- package/src/llm/llm-adapter.ts +159 -0
- package/src/mesh/index.ts +309 -0
- package/src/negotiation/NegotiationProtocol.ts +694 -0
- package/src/negotiation/NegotiationTypes.ts +473 -0
- package/src/negotiation/VotingMechanisms.ts +691 -0
- package/src/negotiation/index.ts +49 -0
- package/src/protocol/goal-synthesizer.ts +317 -0
- package/src/protocol/implementations.ts +474 -0
- package/src/protocol/micro-phase-decomposer.ts +299 -0
- package/src/protocol/micro-step-decomposer.test.ts +306 -0
- package/src/protocol-agent.test.ts +353 -0
- package/src/protocol-agent.ts +670 -0
- package/src/self-improve/absorb-scanner.ts +252 -0
- package/src/self-improve/evolution-engine.ts +149 -0
- package/src/self-improve/framework-absorber.ts +214 -0
- package/src/self-improve/index.ts +50 -0
- package/src/self-improve/prompt-optimizer.ts +212 -0
- package/src/self-improve/test-generator.ts +175 -0
- package/src/skill-router.ts +186 -0
- package/src/skills/index.ts +5 -0
- package/src/skills/skill-md-bridge.ts +1699 -0
- package/src/swarm/ACOEngine.ts +261 -0
- package/src/swarm/CollectiveIntelligence.ts +383 -0
- package/src/swarm/ContributionSynthesizer.ts +481 -0
- package/src/swarm/LeaderElection.ts +393 -0
- package/src/swarm/PSOEngine.ts +206 -0
- package/src/swarm/QuorumPolicy.ts +173 -0
- package/src/swarm/SwarmCoordinator.ts +335 -0
- package/src/swarm/SwarmManager.ts +442 -0
- package/src/swarm/SwarmMembership.ts +456 -0
- package/src/swarm/VotingRound.ts +255 -0
- package/src/swarm/__tests__/ACOEngine.prod.test.ts +164 -0
- package/src/swarm/__tests__/ACOEngine.test.ts +117 -0
- package/src/swarm/__tests__/CollectiveIntelligence.prod.test.ts +296 -0
- package/src/swarm/__tests__/CollectiveIntelligence.test.ts +457 -0
- package/src/swarm/__tests__/ContributionSynthesizer.prod.test.ts +269 -0
- package/src/swarm/__tests__/ContributionSynthesizer.test.ts +254 -0
- package/src/swarm/__tests__/LeaderElection.prod.test.ts +196 -0
- package/src/swarm/__tests__/LeaderElection.test.ts +151 -0
- package/src/swarm/__tests__/PSOEngine.prod.test.ts +162 -0
- package/src/swarm/__tests__/PSOEngine.test.ts +106 -0
- package/src/swarm/__tests__/QuorumPolicy.prod.test.ts +216 -0
- package/src/swarm/__tests__/QuorumPolicy.test.ts +177 -0
- package/src/swarm/__tests__/SwarmCoordinator.prod.test.ts +186 -0
- package/src/swarm/__tests__/SwarmCoordinator.test.ts +167 -0
- package/src/swarm/__tests__/SwarmManager.prod.test.ts +308 -0
- package/src/swarm/__tests__/SwarmManager.test.ts +373 -0
- package/src/swarm/__tests__/SwarmMembership.prod.test.ts +273 -0
- package/src/swarm/__tests__/SwarmMembership.test.ts +264 -0
- package/src/swarm/__tests__/VotingRound.prod.test.ts +233 -0
- package/src/swarm/__tests__/VotingRound.test.ts +174 -0
- package/src/swarm/analytics/SwarmInspector.ts +476 -0
- package/src/swarm/analytics/SwarmMetrics.ts +449 -0
- package/src/swarm/analytics/__tests__/SwarmInspector.prod.test.ts +366 -0
- package/src/swarm/analytics/__tests__/SwarmInspector.test.ts +454 -0
- package/src/swarm/analytics/__tests__/SwarmMetrics.prod.test.ts +254 -0
- package/src/swarm/analytics/__tests__/SwarmMetrics.test.ts +370 -0
- package/src/swarm/analytics/index.ts +7 -0
- package/src/swarm/index.ts +69 -0
- package/src/swarm/messaging/BroadcastChannel.ts +509 -0
- package/src/swarm/messaging/GossipProtocol.ts +565 -0
- package/src/swarm/messaging/SwarmEventBus.ts +443 -0
- package/src/swarm/messaging/__tests__/BroadcastChannel.prod.test.ts +331 -0
- package/src/swarm/messaging/__tests__/BroadcastChannel.test.ts +333 -0
- package/src/swarm/messaging/__tests__/GossipProtocol.prod.test.ts +356 -0
- package/src/swarm/messaging/__tests__/GossipProtocol.test.ts +437 -0
- package/src/swarm/messaging/__tests__/SwarmEventBus.prod.test.ts +191 -0
- package/src/swarm/messaging/__tests__/SwarmEventBus.test.ts +247 -0
- package/src/swarm/messaging/index.ts +8 -0
- package/src/swarm/spatial/FlockingBehavior.ts +462 -0
- package/src/swarm/spatial/FormationController.ts +500 -0
- package/src/swarm/spatial/Vector3.ts +170 -0
- package/src/swarm/spatial/ZoneClaiming.ts +509 -0
- package/src/swarm/spatial/__tests__/FlockingBehavior.prod.test.ts +239 -0
- package/src/swarm/spatial/__tests__/FlockingBehavior.test.ts +298 -0
- package/src/swarm/spatial/__tests__/FormationController.prod.test.ts +240 -0
- package/src/swarm/spatial/__tests__/FormationController.test.ts +297 -0
- package/src/swarm/spatial/__tests__/Vector3.prod.test.ts +283 -0
- package/src/swarm/spatial/__tests__/Vector3.test.ts +224 -0
- package/src/swarm/spatial/__tests__/ZoneClaiming.prod.test.ts +246 -0
- package/src/swarm/spatial/__tests__/ZoneClaiming.test.ts +374 -0
- package/src/swarm/spatial/index.ts +28 -0
- package/src/team.ts +1245 -0
- package/src/training/LRScheduler.ts +377 -0
- package/src/training/QualityScoringPipeline.ts +139 -0
- package/src/training/SoftDedup.ts +461 -0
- package/src/training/SparsityMonitor.ts +685 -0
- package/src/training/SparsityMonitorTypes.ts +209 -0
- package/src/training/SpatialTrainingDataGenerator.ts +1526 -0
- package/src/training/SpatialTrainingDataTypes.ts +216 -0
- package/src/training/TrainingPipelineConfig.ts +215 -0
- package/src/training/constants.ts +94 -0
- package/src/training/index.ts +138 -0
- package/src/training/schema.ts +147 -0
- package/src/training/scripts/generate-novel-use-cases-dataset.ts +272 -0
- package/src/training/scripts/generate-spatial-dataset.ts +521 -0
- package/src/training/training/data/novel-use-cases.jsonl +153 -0
- package/src/training/training/data/spatial-reasoning-10k.jsonl +9354 -0
- package/src/training/trainingmonkey/TrainingMonkeyIntegration.ts +477 -0
- package/src/training/trainingmonkey/TrainingMonkeyTypes.ts +230 -0
- package/src/training/trainingmonkey/index.ts +26 -0
- package/src/training/trait-mappings.ts +157 -0
- package/src/types/core-stubs.d.ts +113 -0
- package/src/types.ts +304 -0
- package/test-output.txt +0 -0
- package/test-result.json +1 -0
- package/tsc-errors.txt +4 -0
- package/tsc_output.txt +0 -0
- package/tsconfig.json +14 -0
- package/tsup-learning-esm.config.ts +12 -0
- package/tsup.config.ts +21 -0
- package/typescript-errors-2.txt +0 -0
- package/typescript-errors.txt +22 -0
- package/vitest-log-utf8.txt +268 -0
- package/vitest-log.txt +0 -0
- 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
|
+
});
|