jarviscore-framework 0.1.0__py3-none-any.whl → 0.1.1__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.
@@ -0,0 +1,622 @@
1
+ """
2
+ Tests for @jarvis_agent decorator.
3
+
4
+ Tests the decorator that converts any class into a JarvisCore agent,
5
+ enabling Custom Profile usage with existing agents (LangChain, CrewAI, raw Python).
6
+ """
7
+ import pytest
8
+ from jarviscore import jarvis_agent, JarvisContext, Mesh
9
+ from jarviscore.adapter.decorator import detect_execute_method, EXECUTE_METHODS
10
+ from jarviscore.profiles.customagent import CustomAgent
11
+
12
+
13
+ class TestDetectExecuteMethod:
14
+ """Tests for execute method detection."""
15
+
16
+ def test_detects_run_method(self):
17
+ """Test detection of 'run' method."""
18
+ class Agent:
19
+ def run(self, data):
20
+ return data
21
+
22
+ assert detect_execute_method(Agent) == "run"
23
+
24
+ def test_detects_invoke_method(self):
25
+ """Test detection of 'invoke' method (LangChain style)."""
26
+ class Agent:
27
+ def invoke(self, input):
28
+ return input
29
+
30
+ assert detect_execute_method(Agent) == "invoke"
31
+
32
+ def test_detects_execute_method(self):
33
+ """Test detection of 'execute' method."""
34
+ class Agent:
35
+ def execute(self, task):
36
+ return task
37
+
38
+ assert detect_execute_method(Agent) == "execute"
39
+
40
+ def test_detects_call_method(self):
41
+ """Test detection of 'call' method."""
42
+ class Agent:
43
+ def call(self, args):
44
+ return args
45
+
46
+ assert detect_execute_method(Agent) == "call"
47
+
48
+ def test_detects_dunder_call(self):
49
+ """Test detection of '__call__' method."""
50
+ class Agent:
51
+ def __call__(self, x):
52
+ return x
53
+
54
+ assert detect_execute_method(Agent) == "__call__"
55
+
56
+ def test_detects_process_method(self):
57
+ """Test detection of 'process' method."""
58
+ class Agent:
59
+ def process(self, data):
60
+ return data
61
+
62
+ assert detect_execute_method(Agent) == "process"
63
+
64
+ def test_detects_handle_method(self):
65
+ """Test detection of 'handle' method."""
66
+ class Agent:
67
+ def handle(self, request):
68
+ return request
69
+
70
+ assert detect_execute_method(Agent) == "handle"
71
+
72
+ def test_prefers_run_over_others(self):
73
+ """Test that 'run' is preferred when multiple methods exist."""
74
+ class Agent:
75
+ def run(self, data):
76
+ return data
77
+
78
+ def invoke(self, data):
79
+ return data
80
+
81
+ assert detect_execute_method(Agent) == "run"
82
+
83
+ def test_returns_none_for_no_method(self):
84
+ """Test returns None when no execute method found."""
85
+ class Agent:
86
+ def custom_method(self, data):
87
+ return data
88
+
89
+ assert detect_execute_method(Agent) is None
90
+
91
+ def test_execute_methods_list(self):
92
+ """Test EXECUTE_METHODS contains expected methods."""
93
+ assert "run" in EXECUTE_METHODS
94
+ assert "invoke" in EXECUTE_METHODS
95
+ assert "execute" in EXECUTE_METHODS
96
+ assert "__call__" in EXECUTE_METHODS
97
+
98
+
99
+ class TestJarvisAgentDecorator:
100
+ """Tests for @jarvis_agent decorator."""
101
+
102
+ def test_decorator_sets_role(self):
103
+ """Test decorator sets role attribute."""
104
+ @jarvis_agent(role="test_role", capabilities=["cap1"])
105
+ class TestAgent:
106
+ def run(self, data):
107
+ return data
108
+
109
+ assert TestAgent.role == "test_role"
110
+
111
+ def test_decorator_sets_capabilities(self):
112
+ """Test decorator sets capabilities attribute."""
113
+ @jarvis_agent(role="test", capabilities=["cap1", "cap2", "cap3"])
114
+ class TestAgent:
115
+ def run(self, data):
116
+ return data
117
+
118
+ assert TestAgent.capabilities == ["cap1", "cap2", "cap3"]
119
+
120
+ def test_decorator_creates_customagent_subclass(self):
121
+ """Test decorator creates CustomAgent subclass."""
122
+ @jarvis_agent(role="test", capabilities=["cap"])
123
+ class TestAgent:
124
+ def run(self, data):
125
+ return data
126
+
127
+ assert issubclass(TestAgent, CustomAgent)
128
+
129
+ def test_decorator_preserves_class_name(self):
130
+ """Test decorator preserves original class name."""
131
+ @jarvis_agent(role="test", capabilities=["cap"])
132
+ class MySpecialAgent:
133
+ def run(self, data):
134
+ return data
135
+
136
+ assert MySpecialAgent.__name__ == "MySpecialAgent"
137
+
138
+ def test_decorator_preserves_docstring(self):
139
+ """Test decorator preserves class docstring."""
140
+ @jarvis_agent(role="test", capabilities=["cap"])
141
+ class DocAgent:
142
+ """This is a documented agent."""
143
+ def run(self, data):
144
+ return data
145
+
146
+ assert "documented agent" in DocAgent.__doc__
147
+
148
+ def test_decorator_stores_wrapped_class(self):
149
+ """Test decorator stores reference to wrapped class."""
150
+ class OriginalClass:
151
+ def run(self, data):
152
+ return data
153
+
154
+ Wrapped = jarvis_agent(role="test", capabilities=["cap"])(OriginalClass)
155
+
156
+ assert Wrapped._wrapped_class is OriginalClass
157
+
158
+ def test_decorator_stores_execute_method_name(self):
159
+ """Test decorator stores execute method name."""
160
+ @jarvis_agent(role="test", capabilities=["cap"])
161
+ class TestAgent:
162
+ def run(self, data):
163
+ return data
164
+
165
+ assert TestAgent._execute_method == "run"
166
+
167
+ def test_decorator_with_custom_execute_method(self):
168
+ """Test decorator with explicit execute_method."""
169
+ @jarvis_agent(role="test", capabilities=["cap"], execute_method="custom_run")
170
+ class TestAgent:
171
+ def custom_run(self, data):
172
+ return {"result": data}
173
+
174
+ assert TestAgent._execute_method == "custom_run"
175
+
176
+ def test_decorator_detects_context_parameter(self):
177
+ """Test decorator detects ctx parameter in method."""
178
+ @jarvis_agent(role="test", capabilities=["cap"])
179
+ class TestAgent:
180
+ def run(self, task, ctx: JarvisContext):
181
+ return task
182
+
183
+ assert TestAgent._expects_context is True
184
+
185
+ def test_decorator_detects_no_context_parameter(self):
186
+ """Test decorator detects absence of ctx parameter."""
187
+ @jarvis_agent(role="test", capabilities=["cap"])
188
+ class TestAgent:
189
+ def run(self, data):
190
+ return data
191
+
192
+ assert TestAgent._expects_context is False
193
+
194
+ def test_decorator_detects_context_named_context(self):
195
+ """Test decorator detects 'context' parameter name."""
196
+ @jarvis_agent(role="test", capabilities=["cap"])
197
+ class TestAgent:
198
+ def run(self, task, context):
199
+ return task
200
+
201
+ assert TestAgent._expects_context is True
202
+
203
+ def test_decorator_raises_for_missing_method(self):
204
+ """Test decorator raises error if method not found."""
205
+ with pytest.raises(ValueError) as exc_info:
206
+ @jarvis_agent(role="test", capabilities=["cap"], execute_method="missing")
207
+ class TestAgent:
208
+ def run(self, data):
209
+ return data
210
+
211
+ assert "has no method 'missing'" in str(exc_info.value)
212
+
213
+ def test_decorator_raises_for_no_detectable_method(self):
214
+ """Test decorator raises error if no method detected."""
215
+ with pytest.raises(ValueError) as exc_info:
216
+ @jarvis_agent(role="test", capabilities=["cap"])
217
+ class TestAgent:
218
+ def custom_only(self, data):
219
+ return data
220
+
221
+ assert "Could not detect execute method" in str(exc_info.value)
222
+
223
+
224
+ class TestDecoratedAgentInstantiation:
225
+ """Tests for instantiating decorated agents."""
226
+
227
+ def test_instantiation_creates_agent(self):
228
+ """Test instantiation creates agent instance."""
229
+ @jarvis_agent(role="test", capabilities=["cap"])
230
+ class TestAgent:
231
+ def run(self, data):
232
+ return data
233
+
234
+ agent = TestAgent()
235
+
236
+ assert agent is not None
237
+ assert agent.role == "test"
238
+
239
+ def test_instantiation_generates_agent_id(self):
240
+ """Test instantiation generates agent_id."""
241
+ @jarvis_agent(role="calculator", capabilities=["math"])
242
+ class Calculator:
243
+ def run(self, data):
244
+ return data
245
+
246
+ agent = Calculator()
247
+
248
+ assert agent.agent_id is not None
249
+ assert agent.agent_id.startswith("calculator-")
250
+
251
+ def test_instantiation_with_custom_agent_id(self):
252
+ """Test instantiation with custom agent_id."""
253
+ @jarvis_agent(role="test", capabilities=["cap"])
254
+ class TestAgent:
255
+ def run(self, data):
256
+ return data
257
+
258
+ agent = TestAgent(agent_id="custom-id-123")
259
+
260
+ assert agent.agent_id == "custom-id-123"
261
+
262
+ def test_instantiation_creates_wrapped_instance(self):
263
+ """Test instantiation creates instance of wrapped class."""
264
+ @jarvis_agent(role="test", capabilities=["cap"])
265
+ class TestAgent:
266
+ def __init__(self):
267
+ self.initialized = True
268
+
269
+ def run(self, data):
270
+ return data
271
+
272
+ agent = TestAgent()
273
+
274
+ assert hasattr(agent, "_instance")
275
+ assert agent._instance.initialized is True
276
+
277
+ def test_instantiation_with_kwargs(self):
278
+ """Test instantiation passes kwargs to wrapped class."""
279
+ @jarvis_agent(role="test", capabilities=["cap"])
280
+ class ConfigurableAgent:
281
+ def __init__(self, config_value=None):
282
+ self.config = config_value
283
+
284
+ def run(self, data):
285
+ return data
286
+
287
+ agent = ConfigurableAgent(config_value="custom_config")
288
+
289
+ assert agent._instance.config == "custom_config"
290
+
291
+
292
+ class TestDecoratedAgentSetup:
293
+ """Tests for decorated agent setup."""
294
+
295
+ @pytest.mark.asyncio
296
+ async def test_setup_calls_wrapped_setup(self):
297
+ """Test setup calls wrapped class setup if exists."""
298
+ setup_called = []
299
+
300
+ @jarvis_agent(role="test", capabilities=["cap"])
301
+ class TestAgent:
302
+ def setup(self):
303
+ setup_called.append("wrapped_setup")
304
+
305
+ def run(self, data):
306
+ return data
307
+
308
+ agent = TestAgent()
309
+ await agent.setup()
310
+
311
+ assert "wrapped_setup" in setup_called
312
+
313
+ @pytest.mark.asyncio
314
+ async def test_setup_handles_async_wrapped_setup(self):
315
+ """Test setup handles async wrapped setup."""
316
+ setup_called = []
317
+
318
+ @jarvis_agent(role="test", capabilities=["cap"])
319
+ class TestAgent:
320
+ async def setup(self):
321
+ setup_called.append("async_setup")
322
+
323
+ def run(self, data):
324
+ return data
325
+
326
+ agent = TestAgent()
327
+ await agent.setup()
328
+
329
+ assert "async_setup" in setup_called
330
+
331
+ @pytest.mark.asyncio
332
+ async def test_setup_works_without_wrapped_setup(self):
333
+ """Test setup works when wrapped class has no setup."""
334
+ @jarvis_agent(role="test", capabilities=["cap"])
335
+ class TestAgent:
336
+ def run(self, data):
337
+ return data
338
+
339
+ agent = TestAgent()
340
+ await agent.setup() # Should not raise
341
+
342
+
343
+ class TestDecoratedAgentExecution:
344
+ """Tests for decorated agent task execution."""
345
+
346
+ @pytest.mark.asyncio
347
+ async def test_execute_task_calls_wrapped_method(self):
348
+ """Test execute_task calls wrapped class method."""
349
+ @jarvis_agent(role="test", capabilities=["cap"])
350
+ class TestAgent:
351
+ def run(self, data):
352
+ return {"doubled": data * 2}
353
+
354
+ agent = TestAgent()
355
+ result = await agent.execute_task({"params": 5})
356
+
357
+ assert result["status"] == "success"
358
+ assert result["output"]["doubled"] == 10
359
+
360
+ @pytest.mark.asyncio
361
+ async def test_execute_task_handles_async_method(self):
362
+ """Test execute_task handles async wrapped method."""
363
+ @jarvis_agent(role="test", capabilities=["cap"])
364
+ class TestAgent:
365
+ async def run(self, data):
366
+ return {"async_result": data}
367
+
368
+ agent = TestAgent()
369
+ result = await agent.execute_task({"params": "test"})
370
+
371
+ assert result["status"] == "success"
372
+ assert result["output"]["async_result"] == "test"
373
+
374
+ @pytest.mark.asyncio
375
+ async def test_execute_task_normalizes_dict_result(self):
376
+ """Test execute_task normalizes dict result."""
377
+ @jarvis_agent(role="test", capabilities=["cap"])
378
+ class TestAgent:
379
+ def run(self, data):
380
+ return {"key": "value"} # No status field
381
+
382
+ agent = TestAgent()
383
+ result = await agent.execute_task({"params": {}})
384
+
385
+ assert result["status"] == "success"
386
+ assert result["output"] == {"key": "value"}
387
+ assert "agent" in result
388
+
389
+ @pytest.mark.asyncio
390
+ async def test_execute_task_normalizes_non_dict_result(self):
391
+ """Test execute_task normalizes non-dict result."""
392
+ @jarvis_agent(role="test", capabilities=["cap"])
393
+ class TestAgent:
394
+ def run(self, data):
395
+ return [1, 2, 3, 4, 5]
396
+
397
+ agent = TestAgent()
398
+ result = await agent.execute_task({"params": {}})
399
+
400
+ assert result["status"] == "success"
401
+ assert result["output"] == [1, 2, 3, 4, 5]
402
+
403
+ @pytest.mark.asyncio
404
+ async def test_execute_task_preserves_existing_status(self):
405
+ """Test execute_task preserves status if already present."""
406
+ @jarvis_agent(role="test", capabilities=["cap"])
407
+ class TestAgent:
408
+ def run(self, data):
409
+ return {"status": "custom_status", "data": "value"}
410
+
411
+ agent = TestAgent()
412
+ result = await agent.execute_task({"params": {}})
413
+
414
+ assert result["status"] == "custom_status"
415
+
416
+ @pytest.mark.asyncio
417
+ async def test_execute_task_handles_exception(self):
418
+ """Test execute_task handles exceptions gracefully."""
419
+ @jarvis_agent(role="test", capabilities=["cap"])
420
+ class TestAgent:
421
+ def run(self, data):
422
+ raise ValueError("Test error")
423
+
424
+ agent = TestAgent()
425
+ result = await agent.execute_task({"params": {}})
426
+
427
+ assert result["status"] == "failure"
428
+ assert "Test error" in result["error"]
429
+
430
+ @pytest.mark.asyncio
431
+ async def test_execute_task_with_task_string(self):
432
+ """Test execute_task passes task string correctly."""
433
+ received_task = []
434
+
435
+ @jarvis_agent(role="test", capabilities=["cap"])
436
+ class TestAgent:
437
+ def run(self, task):
438
+ received_task.append(task)
439
+ return {"received": task}
440
+
441
+ agent = TestAgent()
442
+ await agent.execute_task({"task": "Do something"})
443
+
444
+ assert "Do something" in str(received_task)
445
+
446
+ @pytest.mark.asyncio
447
+ async def test_execute_task_with_params_dict(self):
448
+ """Test execute_task passes params dict correctly."""
449
+ @jarvis_agent(role="test", capabilities=["cap"])
450
+ class TestAgent:
451
+ def run(self, params):
452
+ return {"a": params.get("a"), "b": params.get("b")}
453
+
454
+ agent = TestAgent()
455
+ result = await agent.execute_task({
456
+ "task": "Calculate",
457
+ "params": {"a": 5, "b": 3}
458
+ })
459
+
460
+ assert result["output"]["a"] == 5
461
+ assert result["output"]["b"] == 3
462
+
463
+
464
+ class TestDecoratedAgentTeardown:
465
+ """Tests for decorated agent teardown."""
466
+
467
+ @pytest.mark.asyncio
468
+ async def test_teardown_calls_wrapped_teardown(self):
469
+ """Test teardown calls wrapped class teardown if exists."""
470
+ teardown_called = []
471
+
472
+ @jarvis_agent(role="test", capabilities=["cap"])
473
+ class TestAgent:
474
+ def teardown(self):
475
+ teardown_called.append("wrapped_teardown")
476
+
477
+ def run(self, data):
478
+ return data
479
+
480
+ agent = TestAgent()
481
+ await agent.teardown()
482
+
483
+ assert "wrapped_teardown" in teardown_called
484
+
485
+ @pytest.mark.asyncio
486
+ async def test_teardown_handles_async_wrapped_teardown(self):
487
+ """Test teardown handles async wrapped teardown."""
488
+ teardown_called = []
489
+
490
+ @jarvis_agent(role="test", capabilities=["cap"])
491
+ class TestAgent:
492
+ async def teardown(self):
493
+ teardown_called.append("async_teardown")
494
+
495
+ def run(self, data):
496
+ return data
497
+
498
+ agent = TestAgent()
499
+ await agent.teardown()
500
+
501
+ assert "async_teardown" in teardown_called
502
+
503
+
504
+ class TestDecoratedAgentWithContext:
505
+ """Tests for decorated agents using JarvisContext."""
506
+
507
+ @pytest.mark.asyncio
508
+ async def test_context_not_passed_when_not_expected(self):
509
+ """Test context is not passed when method doesn't expect it."""
510
+ received_args = []
511
+
512
+ @jarvis_agent(role="test", capabilities=["cap"])
513
+ class TestAgent:
514
+ def run(self, data):
515
+ received_args.append(data)
516
+ return {"received": data}
517
+
518
+ agent = TestAgent()
519
+ await agent.execute_task({"params": {"key": "value"}})
520
+
521
+ # Should receive params, not context
522
+ assert len(received_args) == 1
523
+ assert not isinstance(received_args[0], JarvisContext)
524
+
525
+ @pytest.mark.asyncio
526
+ async def test_context_passed_when_expected(self):
527
+ """Test context is passed when method expects it."""
528
+ received_ctx = []
529
+
530
+ @jarvis_agent(role="test", capabilities=["cap"])
531
+ class TestAgent:
532
+ def run(self, task, ctx: JarvisContext):
533
+ received_ctx.append(ctx)
534
+ return {"has_ctx": ctx is not None}
535
+
536
+ agent = TestAgent()
537
+ agent._mesh = None # Simulate no mesh
538
+
539
+ result = await agent.execute_task({
540
+ "task": "test",
541
+ "context": {"workflow_id": "w1", "step_id": "s1"}
542
+ })
543
+
544
+ assert len(received_ctx) == 1
545
+ assert isinstance(received_ctx[0], JarvisContext)
546
+
547
+
548
+ class TestDecoratedAgentInheritance:
549
+ """Tests for decorated agent inheritance."""
550
+
551
+ def test_inherits_can_handle(self):
552
+ """Test decorated agent inherits can_handle method."""
553
+ @jarvis_agent(role="processor", capabilities=["processing", "transform"])
554
+ class TestAgent:
555
+ def run(self, data):
556
+ return data
557
+
558
+ agent = TestAgent()
559
+
560
+ assert agent.can_handle({"role": "processor"}) is True
561
+ assert agent.can_handle({"capability": "processing"}) is True
562
+ assert agent.can_handle({"capability": "transform"}) is True
563
+ assert agent.can_handle({"role": "other"}) is False
564
+
565
+ def test_inherits_agent_methods(self):
566
+ """Test decorated agent has all Agent methods."""
567
+ @jarvis_agent(role="test", capabilities=["cap"])
568
+ class TestAgent:
569
+ def run(self, data):
570
+ return data
571
+
572
+ agent = TestAgent()
573
+
574
+ assert hasattr(agent, "setup")
575
+ assert hasattr(agent, "teardown")
576
+ assert hasattr(agent, "execute_task")
577
+ assert hasattr(agent, "can_handle")
578
+
579
+
580
+ class TestMultipleDecoratedAgents:
581
+ """Tests for multiple decorated agents."""
582
+
583
+ def test_multiple_agents_independent(self):
584
+ """Test multiple decorated agents are independent."""
585
+ @jarvis_agent(role="agent1", capabilities=["cap1"])
586
+ class Agent1:
587
+ def run(self, data):
588
+ return {"from": "agent1"}
589
+
590
+ @jarvis_agent(role="agent2", capabilities=["cap2"])
591
+ class Agent2:
592
+ def run(self, data):
593
+ return {"from": "agent2"}
594
+
595
+ assert Agent1.role == "agent1"
596
+ assert Agent2.role == "agent2"
597
+ assert Agent1.capabilities == ["cap1"]
598
+ assert Agent2.capabilities == ["cap2"]
599
+
600
+ @pytest.mark.asyncio
601
+ async def test_multiple_instances_independent(self):
602
+ """Test multiple instances of same agent are independent."""
603
+ @jarvis_agent(role="counter", capabilities=["count"])
604
+ class Counter:
605
+ def __init__(self):
606
+ self.count = 0
607
+
608
+ def run(self, data):
609
+ self.count += 1
610
+ return {"count": self.count}
611
+
612
+ agent1 = Counter()
613
+ agent2 = Counter()
614
+
615
+ await agent1.execute_task({"params": {}})
616
+ await agent1.execute_task({"params": {}})
617
+
618
+ result1 = await agent1.execute_task({"params": {}})
619
+ result2 = await agent2.execute_task({"params": {}})
620
+
621
+ assert result1["output"]["count"] == 3
622
+ assert result2["output"]["count"] == 1