jarviscore-framework 0.3.0__py3-none-any.whl → 0.3.2__py3-none-any.whl
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.
- examples/cloud_deployment_example.py +3 -3
- examples/{listeneragent_cognitive_discovery_example.py → customagent_cognitive_discovery_example.py} +55 -14
- examples/customagent_distributed_example.py +140 -1
- examples/fastapi_integration_example.py +74 -11
- jarviscore/__init__.py +8 -11
- jarviscore/cli/smoketest.py +1 -1
- jarviscore/core/mesh.py +158 -0
- jarviscore/data/examples/cloud_deployment_example.py +3 -3
- jarviscore/data/examples/custom_profile_decorator.py +134 -0
- jarviscore/data/examples/custom_profile_wrap.py +168 -0
- jarviscore/data/examples/{listeneragent_cognitive_discovery_example.py → customagent_cognitive_discovery_example.py} +55 -14
- jarviscore/data/examples/customagent_distributed_example.py +140 -1
- jarviscore/data/examples/fastapi_integration_example.py +74 -11
- jarviscore/docs/API_REFERENCE.md +576 -47
- jarviscore/docs/CHANGELOG.md +131 -0
- jarviscore/docs/CONFIGURATION.md +1 -1
- jarviscore/docs/CUSTOMAGENT_GUIDE.md +591 -153
- jarviscore/docs/GETTING_STARTED.md +186 -329
- jarviscore/docs/TROUBLESHOOTING.md +1 -1
- jarviscore/docs/USER_GUIDE.md +292 -12
- jarviscore/integrations/fastapi.py +4 -4
- jarviscore/p2p/coordinator.py +36 -7
- jarviscore/p2p/messages.py +13 -0
- jarviscore/p2p/peer_client.py +380 -21
- jarviscore/p2p/peer_tool.py +17 -11
- jarviscore/profiles/__init__.py +2 -4
- jarviscore/profiles/customagent.py +302 -74
- jarviscore/testing/__init__.py +35 -0
- jarviscore/testing/mocks.py +578 -0
- {jarviscore_framework-0.3.0.dist-info → jarviscore_framework-0.3.2.dist-info}/METADATA +61 -46
- {jarviscore_framework-0.3.0.dist-info → jarviscore_framework-0.3.2.dist-info}/RECORD +42 -34
- tests/test_13_dx_improvements.py +37 -37
- tests/test_15_llm_cognitive_discovery.py +18 -18
- tests/test_16_unified_dx_flow.py +3 -3
- tests/test_17_session_context.py +489 -0
- tests/test_18_mesh_diagnostics.py +465 -0
- tests/test_19_async_requests.py +516 -0
- tests/test_20_load_balancing.py +546 -0
- tests/test_21_mock_testing.py +776 -0
- jarviscore/profiles/listeneragent.py +0 -292
- {jarviscore_framework-0.3.0.dist-info → jarviscore_framework-0.3.2.dist-info}/WHEEL +0 -0
- {jarviscore_framework-0.3.0.dist-info → jarviscore_framework-0.3.2.dist-info}/licenses/LICENSE +0 -0
- {jarviscore_framework-0.3.0.dist-info → jarviscore_framework-0.3.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test 20: Load Balancing Strategies (Feature F7)
|
|
3
|
+
|
|
4
|
+
Tests the discovery load balancing strategies:
|
|
5
|
+
- strategy="first" (default behavior)
|
|
6
|
+
- strategy="random" (shuffled order)
|
|
7
|
+
- strategy="round_robin" (rotates each call)
|
|
8
|
+
- strategy="least_recent" (oldest used first)
|
|
9
|
+
- discover_one() convenience method
|
|
10
|
+
- record_peer_usage()
|
|
11
|
+
|
|
12
|
+
Run with: pytest tests/test_20_load_balancing.py -v -s
|
|
13
|
+
"""
|
|
14
|
+
import asyncio
|
|
15
|
+
import sys
|
|
16
|
+
import time
|
|
17
|
+
import pytest
|
|
18
|
+
import logging
|
|
19
|
+
from unittest.mock import MagicMock, patch
|
|
20
|
+
from collections import Counter
|
|
21
|
+
|
|
22
|
+
sys.path.insert(0, '.')
|
|
23
|
+
|
|
24
|
+
# Setup logging
|
|
25
|
+
logging.basicConfig(level=logging.INFO)
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# =============================================================================
|
|
30
|
+
# TEST: STRATEGY FIRST (DEFAULT)
|
|
31
|
+
# =============================================================================
|
|
32
|
+
|
|
33
|
+
class TestStrategyFirst:
|
|
34
|
+
"""Test strategy='first' (default behavior)."""
|
|
35
|
+
|
|
36
|
+
def test_discover_first_returns_consistent_order(self):
|
|
37
|
+
"""Test 'first' strategy returns peers in consistent order."""
|
|
38
|
+
from jarviscore.testing import MockPeerClient
|
|
39
|
+
|
|
40
|
+
client = MockPeerClient(
|
|
41
|
+
agent_id="test-agent",
|
|
42
|
+
agent_role="test",
|
|
43
|
+
mock_peers=[
|
|
44
|
+
{"role": "worker", "agent_id": "worker-a", "capabilities": ["work"]},
|
|
45
|
+
{"role": "worker", "agent_id": "worker-b", "capabilities": ["work"]},
|
|
46
|
+
{"role": "worker", "agent_id": "worker-c", "capabilities": ["work"]}
|
|
47
|
+
]
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Multiple calls should return same order
|
|
51
|
+
result1 = client.discover(role="worker", strategy="first")
|
|
52
|
+
result2 = client.discover(role="worker", strategy="first")
|
|
53
|
+
result3 = client.discover(role="worker", strategy="first")
|
|
54
|
+
|
|
55
|
+
ids1 = [p.agent_id for p in result1]
|
|
56
|
+
ids2 = [p.agent_id for p in result2]
|
|
57
|
+
ids3 = [p.agent_id for p in result3]
|
|
58
|
+
|
|
59
|
+
assert ids1 == ids2 == ids3
|
|
60
|
+
|
|
61
|
+
def test_discover_default_strategy_is_first(self):
|
|
62
|
+
"""Test default strategy is 'first'."""
|
|
63
|
+
from jarviscore.testing import MockPeerClient
|
|
64
|
+
|
|
65
|
+
client = MockPeerClient(
|
|
66
|
+
agent_id="test-agent",
|
|
67
|
+
agent_role="test",
|
|
68
|
+
mock_peers=[
|
|
69
|
+
{"role": "worker", "agent_id": "worker-1", "capabilities": ["work"]},
|
|
70
|
+
{"role": "worker", "agent_id": "worker-2", "capabilities": ["work"]}
|
|
71
|
+
]
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# No strategy specified
|
|
75
|
+
result_default = client.discover(role="worker")
|
|
76
|
+
# Explicit first
|
|
77
|
+
result_first = client.discover(role="worker", strategy="first")
|
|
78
|
+
|
|
79
|
+
ids_default = [p.agent_id for p in result_default]
|
|
80
|
+
ids_first = [p.agent_id for p in result_first]
|
|
81
|
+
|
|
82
|
+
assert ids_default == ids_first
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# =============================================================================
|
|
86
|
+
# TEST: STRATEGY RANDOM
|
|
87
|
+
# =============================================================================
|
|
88
|
+
|
|
89
|
+
class TestStrategyRandom:
|
|
90
|
+
"""Test strategy='random' (shuffled order)."""
|
|
91
|
+
|
|
92
|
+
def test_discover_random_returns_all_peers(self):
|
|
93
|
+
"""Test 'random' strategy returns all peers."""
|
|
94
|
+
from jarviscore.testing import MockPeerClient
|
|
95
|
+
|
|
96
|
+
client = MockPeerClient(
|
|
97
|
+
agent_id="test-agent",
|
|
98
|
+
agent_role="test",
|
|
99
|
+
mock_peers=[
|
|
100
|
+
{"role": "worker", "agent_id": "worker-1", "capabilities": ["work"]},
|
|
101
|
+
{"role": "worker", "agent_id": "worker-2", "capabilities": ["work"]},
|
|
102
|
+
{"role": "worker", "agent_id": "worker-3", "capabilities": ["work"]}
|
|
103
|
+
]
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
result = client.discover(role="worker", strategy="random")
|
|
107
|
+
|
|
108
|
+
assert len(result) == 3
|
|
109
|
+
ids = {p.agent_id for p in result}
|
|
110
|
+
assert ids == {"worker-1", "worker-2", "worker-3"}
|
|
111
|
+
|
|
112
|
+
def test_discover_random_varies_order(self):
|
|
113
|
+
"""Test 'random' strategy produces different orders over many calls."""
|
|
114
|
+
from jarviscore.testing import MockPeerClient
|
|
115
|
+
|
|
116
|
+
client = MockPeerClient(
|
|
117
|
+
agent_id="test-agent",
|
|
118
|
+
agent_role="test",
|
|
119
|
+
mock_peers=[
|
|
120
|
+
{"role": "worker", "agent_id": "w-1", "capabilities": ["work"]},
|
|
121
|
+
{"role": "worker", "agent_id": "w-2", "capabilities": ["work"]},
|
|
122
|
+
{"role": "worker", "agent_id": "w-3", "capabilities": ["work"]},
|
|
123
|
+
{"role": "worker", "agent_id": "w-4", "capabilities": ["work"]}
|
|
124
|
+
]
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Collect first elements over many iterations
|
|
128
|
+
first_peers = []
|
|
129
|
+
for _ in range(50):
|
|
130
|
+
result = client.discover(role="worker", strategy="random")
|
|
131
|
+
first_peers.append(result[0].agent_id)
|
|
132
|
+
|
|
133
|
+
# Should have variation in first position
|
|
134
|
+
unique_first = set(first_peers)
|
|
135
|
+
# With 4 workers and 50 iterations, should see at least 2 different first peers
|
|
136
|
+
assert len(unique_first) >= 2, "Random strategy should vary the order"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# =============================================================================
|
|
140
|
+
# TEST: STRATEGY ROUND ROBIN
|
|
141
|
+
# =============================================================================
|
|
142
|
+
|
|
143
|
+
class TestStrategyRoundRobin:
|
|
144
|
+
"""Test strategy='round_robin' (rotates each call)."""
|
|
145
|
+
|
|
146
|
+
def test_discover_round_robin_rotates(self):
|
|
147
|
+
"""Test 'round_robin' strategy rotates through peers."""
|
|
148
|
+
from jarviscore.testing import MockPeerClient
|
|
149
|
+
|
|
150
|
+
client = MockPeerClient(
|
|
151
|
+
agent_id="test-agent",
|
|
152
|
+
agent_role="test",
|
|
153
|
+
mock_peers=[
|
|
154
|
+
{"role": "worker", "agent_id": "w-0", "capabilities": ["work"]},
|
|
155
|
+
{"role": "worker", "agent_id": "w-1", "capabilities": ["work"]},
|
|
156
|
+
{"role": "worker", "agent_id": "w-2", "capabilities": ["work"]}
|
|
157
|
+
]
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# First call
|
|
161
|
+
result1 = client.discover(role="worker", strategy="round_robin")
|
|
162
|
+
first1 = result1[0].agent_id
|
|
163
|
+
|
|
164
|
+
# Second call - should rotate
|
|
165
|
+
result2 = client.discover(role="worker", strategy="round_robin")
|
|
166
|
+
first2 = result2[0].agent_id
|
|
167
|
+
|
|
168
|
+
# Third call - should rotate again
|
|
169
|
+
result3 = client.discover(role="worker", strategy="round_robin")
|
|
170
|
+
first3 = result3[0].agent_id
|
|
171
|
+
|
|
172
|
+
# Fourth call - should wrap around
|
|
173
|
+
result4 = client.discover(role="worker", strategy="round_robin")
|
|
174
|
+
first4 = result4[0].agent_id
|
|
175
|
+
|
|
176
|
+
# Should have rotated through all three
|
|
177
|
+
firsts = [first1, first2, first3]
|
|
178
|
+
assert len(set(firsts)) == 3, "Round robin should cycle through all peers"
|
|
179
|
+
|
|
180
|
+
# Fourth should match first (wrapped around)
|
|
181
|
+
assert first4 == first1, "Round robin should wrap around"
|
|
182
|
+
|
|
183
|
+
def test_round_robin_independent_keys(self):
|
|
184
|
+
"""Test round robin maintains separate indices per discovery key."""
|
|
185
|
+
from jarviscore.testing import MockPeerClient
|
|
186
|
+
|
|
187
|
+
client = MockPeerClient(
|
|
188
|
+
agent_id="test-agent",
|
|
189
|
+
agent_role="test",
|
|
190
|
+
mock_peers=[
|
|
191
|
+
{"role": "analyst", "agent_id": "a-0", "capabilities": ["analysis"]},
|
|
192
|
+
{"role": "analyst", "agent_id": "a-1", "capabilities": ["analysis"]},
|
|
193
|
+
{"role": "worker", "agent_id": "w-0", "capabilities": ["work"]},
|
|
194
|
+
{"role": "worker", "agent_id": "w-1", "capabilities": ["work"]}
|
|
195
|
+
]
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Query analysts
|
|
199
|
+
analysts1 = client.discover(role="analyst", strategy="round_robin")
|
|
200
|
+
analyst_first1 = analysts1[0].agent_id
|
|
201
|
+
|
|
202
|
+
# Query workers
|
|
203
|
+
workers1 = client.discover(role="worker", strategy="round_robin")
|
|
204
|
+
worker_first1 = workers1[0].agent_id
|
|
205
|
+
|
|
206
|
+
# Query analysts again - should have rotated independently
|
|
207
|
+
analysts2 = client.discover(role="analyst", strategy="round_robin")
|
|
208
|
+
analyst_first2 = analysts2[0].agent_id
|
|
209
|
+
|
|
210
|
+
# Query workers again
|
|
211
|
+
workers2 = client.discover(role="worker", strategy="round_robin")
|
|
212
|
+
worker_first2 = workers2[0].agent_id
|
|
213
|
+
|
|
214
|
+
# Each role should have rotated independently
|
|
215
|
+
assert analyst_first1 != analyst_first2
|
|
216
|
+
assert worker_first1 != worker_first2
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# =============================================================================
|
|
220
|
+
# TEST: STRATEGY LEAST RECENT
|
|
221
|
+
# =============================================================================
|
|
222
|
+
|
|
223
|
+
class TestStrategyLeastRecent:
|
|
224
|
+
"""Test strategy='least_recent' (oldest used first)."""
|
|
225
|
+
|
|
226
|
+
def test_discover_least_recent_prefers_unused(self):
|
|
227
|
+
"""Test 'least_recent' returns unused peers first."""
|
|
228
|
+
from jarviscore.testing import MockPeerClient
|
|
229
|
+
|
|
230
|
+
client = MockPeerClient(
|
|
231
|
+
agent_id="test-agent",
|
|
232
|
+
agent_role="test",
|
|
233
|
+
mock_peers=[
|
|
234
|
+
{"role": "worker", "agent_id": "w-1", "capabilities": ["work"]},
|
|
235
|
+
{"role": "worker", "agent_id": "w-2", "capabilities": ["work"]},
|
|
236
|
+
{"role": "worker", "agent_id": "w-3", "capabilities": ["work"]}
|
|
237
|
+
]
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# Mark w-2 as recently used
|
|
241
|
+
client.record_peer_usage("w-2")
|
|
242
|
+
|
|
243
|
+
result = client.discover(role="worker", strategy="least_recent")
|
|
244
|
+
|
|
245
|
+
# w-2 should be last (most recently used)
|
|
246
|
+
assert result[-1].agent_id == "w-2"
|
|
247
|
+
|
|
248
|
+
def test_discover_least_recent_ordering(self):
|
|
249
|
+
"""Test 'least_recent' orders by usage time."""
|
|
250
|
+
from jarviscore.testing import MockPeerClient
|
|
251
|
+
|
|
252
|
+
client = MockPeerClient(
|
|
253
|
+
agent_id="test-agent",
|
|
254
|
+
agent_role="test",
|
|
255
|
+
mock_peers=[
|
|
256
|
+
{"role": "worker", "agent_id": "w-1", "capabilities": ["work"]},
|
|
257
|
+
{"role": "worker", "agent_id": "w-2", "capabilities": ["work"]},
|
|
258
|
+
{"role": "worker", "agent_id": "w-3", "capabilities": ["work"]}
|
|
259
|
+
]
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
# Mark usage in specific order
|
|
263
|
+
client.record_peer_usage("w-3") # First used
|
|
264
|
+
time.sleep(0.01)
|
|
265
|
+
client.record_peer_usage("w-1") # Second used
|
|
266
|
+
time.sleep(0.01)
|
|
267
|
+
client.record_peer_usage("w-2") # Most recently used
|
|
268
|
+
|
|
269
|
+
result = client.discover(role="worker", strategy="least_recent")
|
|
270
|
+
|
|
271
|
+
ids = [p.agent_id for p in result]
|
|
272
|
+
|
|
273
|
+
# w-3 should be first (least recently used), w-2 last (most recent)
|
|
274
|
+
assert ids[0] == "w-3"
|
|
275
|
+
assert ids[-1] == "w-2"
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
# =============================================================================
|
|
279
|
+
# TEST: RECORD_PEER_USAGE
|
|
280
|
+
# =============================================================================
|
|
281
|
+
|
|
282
|
+
class TestRecordPeerUsage:
|
|
283
|
+
"""Test record_peer_usage() method."""
|
|
284
|
+
|
|
285
|
+
def test_record_peer_usage_updates_timestamp(self):
|
|
286
|
+
"""Test record_peer_usage() updates internal timestamp."""
|
|
287
|
+
from jarviscore.testing import MockPeerClient
|
|
288
|
+
|
|
289
|
+
client = MockPeerClient()
|
|
290
|
+
|
|
291
|
+
# Initially no usage recorded
|
|
292
|
+
assert client._peer_last_used.get("peer-1") is None
|
|
293
|
+
|
|
294
|
+
client.record_peer_usage("peer-1")
|
|
295
|
+
|
|
296
|
+
assert client._peer_last_used.get("peer-1") is not None
|
|
297
|
+
assert isinstance(client._peer_last_used["peer-1"], float)
|
|
298
|
+
|
|
299
|
+
def test_record_peer_usage_updates_on_repeated_calls(self):
|
|
300
|
+
"""Test record_peer_usage() updates timestamp on repeated calls."""
|
|
301
|
+
from jarviscore.testing import MockPeerClient
|
|
302
|
+
|
|
303
|
+
client = MockPeerClient()
|
|
304
|
+
|
|
305
|
+
client.record_peer_usage("peer-1")
|
|
306
|
+
first_time = client._peer_last_used["peer-1"]
|
|
307
|
+
|
|
308
|
+
time.sleep(0.01)
|
|
309
|
+
|
|
310
|
+
client.record_peer_usage("peer-1")
|
|
311
|
+
second_time = client._peer_last_used["peer-1"]
|
|
312
|
+
|
|
313
|
+
assert second_time > first_time
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
# =============================================================================
|
|
317
|
+
# TEST: DISCOVER_ONE
|
|
318
|
+
# =============================================================================
|
|
319
|
+
|
|
320
|
+
class TestDiscoverOne:
|
|
321
|
+
"""Test discover_one() convenience method."""
|
|
322
|
+
|
|
323
|
+
def test_discover_one_returns_single_peer(self):
|
|
324
|
+
"""Test discover_one() returns single PeerInfo."""
|
|
325
|
+
from jarviscore.testing import MockPeerClient
|
|
326
|
+
from jarviscore.p2p.messages import PeerInfo
|
|
327
|
+
|
|
328
|
+
client = MockPeerClient(
|
|
329
|
+
agent_id="test-agent",
|
|
330
|
+
agent_role="test",
|
|
331
|
+
mock_peers=[
|
|
332
|
+
{"role": "analyst", "agent_id": "a-1", "capabilities": ["analysis"]},
|
|
333
|
+
{"role": "analyst", "agent_id": "a-2", "capabilities": ["analysis"]}
|
|
334
|
+
]
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
result = client.discover_one(role="analyst")
|
|
338
|
+
|
|
339
|
+
assert result is not None
|
|
340
|
+
assert isinstance(result, PeerInfo)
|
|
341
|
+
assert result.role == "analyst"
|
|
342
|
+
|
|
343
|
+
def test_discover_one_returns_none_if_no_match(self):
|
|
344
|
+
"""Test discover_one() returns None if no peers match."""
|
|
345
|
+
from jarviscore.testing import MockPeerClient
|
|
346
|
+
|
|
347
|
+
client = MockPeerClient(
|
|
348
|
+
agent_id="test-agent",
|
|
349
|
+
agent_role="test",
|
|
350
|
+
mock_peers=[{"role": "worker", "capabilities": ["work"]}]
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
result = client.discover_one(role="analyst")
|
|
354
|
+
|
|
355
|
+
assert result is None
|
|
356
|
+
|
|
357
|
+
def test_discover_one_with_strategy(self):
|
|
358
|
+
"""Test discover_one() respects strategy parameter."""
|
|
359
|
+
from jarviscore.testing import MockPeerClient
|
|
360
|
+
|
|
361
|
+
client = MockPeerClient(
|
|
362
|
+
agent_id="test-agent",
|
|
363
|
+
agent_role="test",
|
|
364
|
+
mock_peers=[
|
|
365
|
+
{"role": "worker", "agent_id": "w-0", "capabilities": ["work"]},
|
|
366
|
+
{"role": "worker", "agent_id": "w-1", "capabilities": ["work"]},
|
|
367
|
+
{"role": "worker", "agent_id": "w-2", "capabilities": ["work"]}
|
|
368
|
+
]
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
# Round robin should rotate
|
|
372
|
+
first1 = client.discover_one(role="worker", strategy="round_robin")
|
|
373
|
+
first2 = client.discover_one(role="worker", strategy="round_robin")
|
|
374
|
+
first3 = client.discover_one(role="worker", strategy="round_robin")
|
|
375
|
+
|
|
376
|
+
ids = [first1.agent_id, first2.agent_id, first3.agent_id]
|
|
377
|
+
assert len(set(ids)) == 3, "Round robin via discover_one should rotate"
|
|
378
|
+
|
|
379
|
+
def test_discover_one_with_capability(self):
|
|
380
|
+
"""Test discover_one() filters by capability."""
|
|
381
|
+
from jarviscore.testing import MockPeerClient
|
|
382
|
+
|
|
383
|
+
client = MockPeerClient(
|
|
384
|
+
agent_id="test-agent",
|
|
385
|
+
agent_role="test",
|
|
386
|
+
mock_peers=[
|
|
387
|
+
{"role": "agent1", "agent_id": "a-1", "capabilities": ["analysis"]},
|
|
388
|
+
{"role": "agent2", "agent_id": "a-2", "capabilities": ["reporting"]},
|
|
389
|
+
{"role": "agent3", "agent_id": "a-3", "capabilities": ["analysis", "reporting"]}
|
|
390
|
+
]
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
result = client.discover_one(capability="reporting")
|
|
394
|
+
|
|
395
|
+
assert result is not None
|
|
396
|
+
assert "reporting" in result.capabilities
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
# =============================================================================
|
|
400
|
+
# TEST: REAL PEER CLIENT STRATEGIES
|
|
401
|
+
# =============================================================================
|
|
402
|
+
|
|
403
|
+
class TestRealPeerClientStrategies:
|
|
404
|
+
"""Test strategies with real PeerClient."""
|
|
405
|
+
|
|
406
|
+
def test_real_client_round_robin(self):
|
|
407
|
+
"""Test real PeerClient round robin strategy."""
|
|
408
|
+
from jarviscore.p2p.peer_client import PeerClient
|
|
409
|
+
|
|
410
|
+
mock_coordinator = MagicMock()
|
|
411
|
+
mock_coordinator._remote_agent_registry = {}
|
|
412
|
+
|
|
413
|
+
# Create mock agents
|
|
414
|
+
class MockAgent:
|
|
415
|
+
def __init__(self, aid, role):
|
|
416
|
+
self.agent_id = aid
|
|
417
|
+
self.role = role
|
|
418
|
+
self.capabilities = ["work"]
|
|
419
|
+
|
|
420
|
+
agents = [
|
|
421
|
+
MockAgent("w-0", "worker"),
|
|
422
|
+
MockAgent("w-1", "worker"),
|
|
423
|
+
MockAgent("w-2", "worker")
|
|
424
|
+
]
|
|
425
|
+
|
|
426
|
+
agent_registry = {"worker": agents}
|
|
427
|
+
|
|
428
|
+
client = PeerClient(
|
|
429
|
+
coordinator=mock_coordinator,
|
|
430
|
+
agent_id="client-1",
|
|
431
|
+
agent_role="client",
|
|
432
|
+
agent_registry=agent_registry,
|
|
433
|
+
node_id="localhost:7946"
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
# Collect first results
|
|
437
|
+
firsts = []
|
|
438
|
+
for _ in range(6): # 2 full cycles
|
|
439
|
+
result = client.discover(role="worker", strategy="round_robin")
|
|
440
|
+
firsts.append(result[0].agent_id)
|
|
441
|
+
|
|
442
|
+
# Should cycle: 0,1,2,0,1,2
|
|
443
|
+
assert firsts[:3] != firsts[3:] or len(set(firsts[:3])) == 3
|
|
444
|
+
|
|
445
|
+
def test_real_client_least_recent(self):
|
|
446
|
+
"""Test real PeerClient least_recent strategy."""
|
|
447
|
+
from jarviscore.p2p.peer_client import PeerClient
|
|
448
|
+
|
|
449
|
+
mock_coordinator = MagicMock()
|
|
450
|
+
mock_coordinator._remote_agent_registry = {}
|
|
451
|
+
|
|
452
|
+
class MockAgent:
|
|
453
|
+
def __init__(self, aid, role):
|
|
454
|
+
self.agent_id = aid
|
|
455
|
+
self.role = role
|
|
456
|
+
self.capabilities = ["work"]
|
|
457
|
+
|
|
458
|
+
agents = [
|
|
459
|
+
MockAgent("w-1", "worker"),
|
|
460
|
+
MockAgent("w-2", "worker"),
|
|
461
|
+
MockAgent("w-3", "worker")
|
|
462
|
+
]
|
|
463
|
+
|
|
464
|
+
agent_registry = {"worker": agents}
|
|
465
|
+
|
|
466
|
+
client = PeerClient(
|
|
467
|
+
coordinator=mock_coordinator,
|
|
468
|
+
agent_id="client-1",
|
|
469
|
+
agent_role="client",
|
|
470
|
+
agent_registry=agent_registry,
|
|
471
|
+
node_id="localhost:7946"
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
# Mark some as used
|
|
475
|
+
client.record_peer_usage("w-2")
|
|
476
|
+
|
|
477
|
+
result = client.discover(role="worker", strategy="least_recent")
|
|
478
|
+
|
|
479
|
+
# w-2 should be last
|
|
480
|
+
assert result[-1].agent_id == "w-2"
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
# =============================================================================
|
|
484
|
+
# TEST: EDGE CASES
|
|
485
|
+
# =============================================================================
|
|
486
|
+
|
|
487
|
+
class TestLoadBalancingEdgeCases:
|
|
488
|
+
"""Test edge cases for load balancing."""
|
|
489
|
+
|
|
490
|
+
def test_single_peer_all_strategies(self):
|
|
491
|
+
"""Test all strategies work with single peer."""
|
|
492
|
+
from jarviscore.testing import MockPeerClient
|
|
493
|
+
|
|
494
|
+
client = MockPeerClient(
|
|
495
|
+
agent_id="test-agent",
|
|
496
|
+
agent_role="test",
|
|
497
|
+
mock_peers=[{"role": "worker", "agent_id": "w-1", "capabilities": ["work"]}]
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
for strategy in ["first", "random", "round_robin", "least_recent"]:
|
|
501
|
+
result = client.discover(role="worker", strategy=strategy)
|
|
502
|
+
assert len(result) == 1
|
|
503
|
+
assert result[0].agent_id == "w-1"
|
|
504
|
+
|
|
505
|
+
def test_empty_results_all_strategies(self):
|
|
506
|
+
"""Test all strategies handle empty results."""
|
|
507
|
+
from jarviscore.testing import MockPeerClient
|
|
508
|
+
|
|
509
|
+
client = MockPeerClient(
|
|
510
|
+
agent_id="test-agent",
|
|
511
|
+
agent_role="test",
|
|
512
|
+
mock_peers=[{"role": "analyst", "capabilities": ["analysis"]}]
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
for strategy in ["first", "random", "round_robin", "least_recent"]:
|
|
516
|
+
result = client.discover(role="nonexistent", strategy=strategy)
|
|
517
|
+
assert result == []
|
|
518
|
+
|
|
519
|
+
def test_unknown_strategy_falls_back_to_first(self):
|
|
520
|
+
"""Test unknown strategy falls back to 'first' behavior."""
|
|
521
|
+
from jarviscore.testing import MockPeerClient
|
|
522
|
+
|
|
523
|
+
client = MockPeerClient(
|
|
524
|
+
agent_id="test-agent",
|
|
525
|
+
agent_role="test",
|
|
526
|
+
mock_peers=[
|
|
527
|
+
{"role": "worker", "agent_id": "w-1", "capabilities": ["work"]},
|
|
528
|
+
{"role": "worker", "agent_id": "w-2", "capabilities": ["work"]}
|
|
529
|
+
]
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
result1 = client.discover(role="worker", strategy="unknown_strategy")
|
|
533
|
+
result2 = client.discover(role="worker", strategy="first")
|
|
534
|
+
|
|
535
|
+
ids1 = [p.agent_id for p in result1]
|
|
536
|
+
ids2 = [p.agent_id for p in result2]
|
|
537
|
+
|
|
538
|
+
assert ids1 == ids2
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
# =============================================================================
|
|
542
|
+
# RUN TESTS
|
|
543
|
+
# =============================================================================
|
|
544
|
+
|
|
545
|
+
if __name__ == "__main__":
|
|
546
|
+
pytest.main([__file__, "-v", "-s"])
|