eljay-ai 1.1.0
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/AGENTS.md +57 -0
- package/README.md +479 -0
- package/agent/__init__.py +10 -0
- package/agent/agent.py +269 -0
- package/agent/agents/__init__.py +34 -0
- package/agent/agents/registry.py +212 -0
- package/agent/builtin_tools.py +591 -0
- package/agent/chat.py +354 -0
- package/agent/config.py +67 -0
- package/agent/context.py +63 -0
- package/agent/edits.py +167 -0
- package/agent/gitaware.py +126 -0
- package/agent/hardware.py +331 -0
- package/agent/knowledge.py +133 -0
- package/agent/memory.py +98 -0
- package/agent/ollama_client.py +133 -0
- package/agent/permissions.py +93 -0
- package/agent/providers/__init__.py +51 -0
- package/agent/providers/base.py +78 -0
- package/agent/providers/image.py +110 -0
- package/agent/providers/ollama.py +111 -0
- package/agent/providers/video.py +96 -0
- package/agent/providers/web.py +155 -0
- package/agent/router.py +200 -0
- package/agent/rules.py +52 -0
- package/agent/runner.py +168 -0
- package/agent/skills.py +161 -0
- package/agent/tools.py +102 -0
- package/agent/verification.py +80 -0
- package/agent/workspace.py +487 -0
- package/bin/eljay +5 -0
- package/bin/eljay.cmd +4 -0
- package/bin/myagent +4 -0
- package/bin/myagent.cmd +4 -0
- package/eljay-ai-1.1.0.tgz +0 -0
- package/eljay.js +56 -0
- package/eljay.py +150 -0
- package/install.ps1 +28 -0
- package/knowledge/reference/diffusers.md +32 -0
- package/knowledge/reference/video-providers.md +21 -0
- package/knowledge/setup/comfyui.md +35 -0
- package/knowledge/setup/image-providers.md +16 -0
- package/knowledge/test-category/test-entry.md +5 -0
- package/myagent.py +30 -0
- package/package.json +40 -0
- package/skills/coding/code-review.md +3 -0
- package/skills/coding/fix-attempt-protocol.md +9 -0
- package/skills/debugging/root-cause-analysis.md +9 -0
- package/skills/general/communication.md +7 -0
- package/skills/laravel/authentication.md +15 -0
- package/skills/mysql/performance.md +9 -0
- package/skills/php/standards.md +8 -0
- package/skills/react/component-best-practices.md +8 -0
- package/skills/research/source-tracking.md +7 -0
- package/skills/security/input-validation.md +9 -0
- package/skills/testing/pytest-best-practices.md +7 -0
- package/tests/run_all.py +34 -0
- package/tests/test_agent_core.py +385 -0
- package/tests/test_capabilities.py +169 -0
- package/tests/test_edits.py +117 -0
- package/tests/test_eljay.py +209 -0
- package/tests/test_runner.py +114 -0
- package/tests/test_universal.py +458 -0
- package/tests/test_workspace.py +221 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
"""Tests for ElJay AI universal-assistant components.
|
|
2
|
+
|
|
3
|
+
Run from the project root:
|
|
4
|
+
python tests/test_universal.py
|
|
5
|
+
|
|
6
|
+
These are deterministic and do NOT require Ollama or network access.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import unittest
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class HardwareTests(unittest.TestCase):
|
|
18
|
+
def test_detect_hardware_returns_hardware_info(self):
|
|
19
|
+
from agent.hardware import detect_hardware
|
|
20
|
+
hw = detect_hardware()
|
|
21
|
+
self.assertIsNotNone(hw)
|
|
22
|
+
self.assertTrue(hasattr(hw, "os"))
|
|
23
|
+
self.assertTrue(hasattr(hw, "cpu_cores"))
|
|
24
|
+
self.assertTrue(hasattr(hw, "ram_gb"))
|
|
25
|
+
self.assertTrue(hasattr(hw, "can_code"))
|
|
26
|
+
self.assertTrue(hasattr(hw, "can_image"))
|
|
27
|
+
self.assertTrue(hasattr(hw, "can_video"))
|
|
28
|
+
|
|
29
|
+
def test_summarize_hardware_returns_string(self):
|
|
30
|
+
from agent.hardware import detect_hardware, summarize_hardware
|
|
31
|
+
hw = detect_hardware()
|
|
32
|
+
s = summarize_hardware(hw)
|
|
33
|
+
self.assertIsInstance(s, str)
|
|
34
|
+
self.assertTrue(len(s) > 0)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class HardwareVramTests(unittest.TestCase):
|
|
38
|
+
def test_hardware_has_vram_available_field(self):
|
|
39
|
+
from agent.hardware import detect_hardware
|
|
40
|
+
hw = detect_hardware()
|
|
41
|
+
self.assertTrue(hasattr(hw, "vram_available_gb"))
|
|
42
|
+
self.assertTrue(hasattr(hw, "vram_total_gb"))
|
|
43
|
+
|
|
44
|
+
def test_summarize_hardware_handles_no_gpu(self):
|
|
45
|
+
from agent.hardware import summarize_hardware, HardwareInfo
|
|
46
|
+
hw = HardwareInfo(gpu="none", vram_total_gb=0.0, vram_available_gb=0.0)
|
|
47
|
+
s = summarize_hardware(hw)
|
|
48
|
+
self.assertIn("none", s.lower())
|
|
49
|
+
|
|
50
|
+
def test_recommended_image_mode_returns_string(self):
|
|
51
|
+
from agent.hardware import HardwareInfo
|
|
52
|
+
hw = HardwareInfo()
|
|
53
|
+
s = hw.recommended_image_mode()
|
|
54
|
+
self.assertIsInstance(s, str)
|
|
55
|
+
|
|
56
|
+
def test_recommended_video_mode_returns_string(self):
|
|
57
|
+
from agent.hardware import HardwareInfo
|
|
58
|
+
hw = HardwareInfo()
|
|
59
|
+
s = hw.recommended_video_mode()
|
|
60
|
+
self.assertIsInstance(s, str)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class RouterTests(unittest.TestCase):
|
|
64
|
+
def test_router_importable(self):
|
|
65
|
+
from agent.router import AgentRouter
|
|
66
|
+
self.assertTrue(callable(AgentRouter))
|
|
67
|
+
|
|
68
|
+
def test_router_can_be_constructed(self):
|
|
69
|
+
from agent.router import AgentRouter
|
|
70
|
+
from agent.skills import Skills
|
|
71
|
+
from agent.knowledge import KnowledgeBase
|
|
72
|
+
root = Path(__file__).resolve().parent.parent
|
|
73
|
+
skills = Skills(str(root))
|
|
74
|
+
knowledge = KnowledgeBase(str(root), str(root))
|
|
75
|
+
router = AgentRouter(skills=skills, knowledge=knowledge)
|
|
76
|
+
self.assertIsNotNone(router)
|
|
77
|
+
|
|
78
|
+
def test_router_len_returns_agent_count(self):
|
|
79
|
+
from agent.router import AgentRouter
|
|
80
|
+
router = AgentRouter()
|
|
81
|
+
self.assertGreater(len(router), 0)
|
|
82
|
+
|
|
83
|
+
def test_router_registry_is_list(self):
|
|
84
|
+
from agent.router import AgentRouter
|
|
85
|
+
router = AgentRouter()
|
|
86
|
+
self.assertIsInstance(router.registry, list)
|
|
87
|
+
|
|
88
|
+
def test_route_returns_agent_definition(self):
|
|
89
|
+
from agent.router import AgentRouter
|
|
90
|
+
from agent.agents import AgentDefinition
|
|
91
|
+
router = AgentRouter()
|
|
92
|
+
# Without provider availability checks, route based on keywords.
|
|
93
|
+
from agent.agents import route_request
|
|
94
|
+
agent = route_request("write some code", None)
|
|
95
|
+
self.assertIsInstance(agent, AgentDefinition)
|
|
96
|
+
|
|
97
|
+
def test_route_image_via_route_request(self):
|
|
98
|
+
from agent.agents import route_request
|
|
99
|
+
agent = route_request("create an image of a cowboy", None)
|
|
100
|
+
self.assertEqual(agent.name, "image")
|
|
101
|
+
|
|
102
|
+
def test_route_video_via_route_request(self):
|
|
103
|
+
from agent.agents import route_request
|
|
104
|
+
agent = route_request("make a cinematic video", None)
|
|
105
|
+
self.assertEqual(agent.name, "video")
|
|
106
|
+
|
|
107
|
+
def test_route_research_via_route_request(self):
|
|
108
|
+
from agent.agents import route_request
|
|
109
|
+
agent = route_request("research the latest Laravel", None)
|
|
110
|
+
self.assertEqual(agent.name, "research")
|
|
111
|
+
|
|
112
|
+
def test_route_coding_via_route_request(self):
|
|
113
|
+
from agent.agents import route_request
|
|
114
|
+
agent = route_request("fix my React error", None)
|
|
115
|
+
self.assertEqual(agent.name, "coding")
|
|
116
|
+
|
|
117
|
+
def test_route_knowledge_via_route_request(self):
|
|
118
|
+
from agent.agents import route_request
|
|
119
|
+
agent = route_request("explain quantum computing", None)
|
|
120
|
+
# Knowledge or main fallback
|
|
121
|
+
self.assertIn(agent.name, ("knowledge", "main"))
|
|
122
|
+
|
|
123
|
+
def test_delegated_agent_for_returns_none_for_main(self):
|
|
124
|
+
from agent.router import AgentRouter
|
|
125
|
+
router = AgentRouter()
|
|
126
|
+
# route_request with provider availability may return main for "hello"
|
|
127
|
+
result = router.delegated_agent_for("hello")
|
|
128
|
+
self.assertTrue(result is None or hasattr(result, "name"))
|
|
129
|
+
|
|
130
|
+
def test_delegated_agent_for_returns_agent_for_specialist(self):
|
|
131
|
+
from agent.router import AgentRouter
|
|
132
|
+
router = AgentRouter()
|
|
133
|
+
# route_request with None providers bypasses availability check
|
|
134
|
+
from agent.agents import route_request
|
|
135
|
+
agent = route_request("create an image", None)
|
|
136
|
+
if agent.name == "image":
|
|
137
|
+
self.assertEqual(agent.name, "image")
|
|
138
|
+
else:
|
|
139
|
+
# If image provider unavailable, router.route may return main.
|
|
140
|
+
# But route_request with None should still match image.
|
|
141
|
+
self.assertEqual(agent.name, "image")
|
|
142
|
+
|
|
143
|
+
def test_agent_by_name(self):
|
|
144
|
+
from agent.router import AgentRouter
|
|
145
|
+
router = AgentRouter()
|
|
146
|
+
agent = router.agent_by_name("image")
|
|
147
|
+
self.assertIsNotNone(agent)
|
|
148
|
+
self.assertEqual(agent.name, "image")
|
|
149
|
+
|
|
150
|
+
def test_all_agents_returns_list(self):
|
|
151
|
+
from agent.router import AgentRouter
|
|
152
|
+
router = AgentRouter()
|
|
153
|
+
agents = router.all_agents()
|
|
154
|
+
self.assertIsInstance(agents, list)
|
|
155
|
+
self.assertGreater(len(agents), 0)
|
|
156
|
+
|
|
157
|
+
def test_provider_status_returns_dict(self):
|
|
158
|
+
from agent.router import AgentRouter
|
|
159
|
+
router = AgentRouter()
|
|
160
|
+
status = router.provider_status()
|
|
161
|
+
self.assertIsInstance(status, dict)
|
|
162
|
+
|
|
163
|
+
def test_inject_context_returns_string(self):
|
|
164
|
+
from agent.router import AgentRouter
|
|
165
|
+
router = AgentRouter()
|
|
166
|
+
from agent.agents import route_request
|
|
167
|
+
agent = route_request("coding help", None)
|
|
168
|
+
ctx = router.inject_context("coding help", agent)
|
|
169
|
+
self.assertIsInstance(ctx, str)
|
|
170
|
+
|
|
171
|
+
def test_route_falls_back_to_main_for_greeting(self):
|
|
172
|
+
from agent.agents import route_request
|
|
173
|
+
agent = route_request("hello world", None)
|
|
174
|
+
self.assertEqual(agent.name, "main")
|
|
175
|
+
|
|
176
|
+
def test_route_image_via_router_even_without_provider(self):
|
|
177
|
+
from agent.router import AgentRouter
|
|
178
|
+
router = AgentRouter()
|
|
179
|
+
agent = router.route("create an image of a cyberpunk city")
|
|
180
|
+
self.assertEqual(agent.name, "image")
|
|
181
|
+
|
|
182
|
+
def test_route_video_via_router_even_without_provider(self):
|
|
183
|
+
from agent.router import AgentRouter
|
|
184
|
+
router = AgentRouter()
|
|
185
|
+
agent = router.route("make a cinematic video")
|
|
186
|
+
self.assertEqual(agent.name, "video")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class SkillsTests(unittest.TestCase):
|
|
190
|
+
def test_skills_importable(self):
|
|
191
|
+
from agent.skills import Skills
|
|
192
|
+
self.assertTrue(callable(Skills))
|
|
193
|
+
|
|
194
|
+
def test_skills_can_be_constructed(self):
|
|
195
|
+
from agent.skills import Skills
|
|
196
|
+
root = Path(__file__).resolve().parent.parent
|
|
197
|
+
skills = Skills(str(root))
|
|
198
|
+
self.assertIsNotNone(skills)
|
|
199
|
+
|
|
200
|
+
def test_skills_all_skills_returns_list(self):
|
|
201
|
+
from agent.skills import Skills
|
|
202
|
+
root = Path(__file__).resolve().parent.parent
|
|
203
|
+
skills = Skills(str(root))
|
|
204
|
+
self.assertIsInstance(skills.all_skills(), list)
|
|
205
|
+
|
|
206
|
+
def test_skills_has_summary(self):
|
|
207
|
+
from agent.skills import Skills
|
|
208
|
+
root = Path(__file__).resolve().parent.parent
|
|
209
|
+
skills = Skills(str(root))
|
|
210
|
+
s = skills.summary()
|
|
211
|
+
self.assertIsInstance(s, str)
|
|
212
|
+
|
|
213
|
+
def test_skills_relevant_to(self):
|
|
214
|
+
from agent.skills import Skills
|
|
215
|
+
root = Path(__file__).resolve().parent.parent
|
|
216
|
+
skills = Skills(str(root))
|
|
217
|
+
results = skills.relevant_to("debug")
|
|
218
|
+
self.assertIsInstance(results, list)
|
|
219
|
+
|
|
220
|
+
def test_skills_search(self):
|
|
221
|
+
from agent.skills import Skills
|
|
222
|
+
root = Path(__file__).resolve().parent.parent
|
|
223
|
+
skills = Skills(str(root))
|
|
224
|
+
results = skills.search("coding")
|
|
225
|
+
self.assertIsInstance(results, list)
|
|
226
|
+
|
|
227
|
+
def test_skills_directory_exists(self):
|
|
228
|
+
root = Path(__file__).resolve().parent.parent / "skills"
|
|
229
|
+
self.assertTrue(root.exists(), "skills/ directory should exist")
|
|
230
|
+
|
|
231
|
+
def test_skills_directory_has_content(self):
|
|
232
|
+
root = Path(__file__).resolve().parent.parent / "skills"
|
|
233
|
+
skills = list(root.rglob("*.md"))
|
|
234
|
+
self.assertGreater(len(skills), 0, "skills/ should contain .md files")
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class KnowledgeTests(unittest.TestCase):
|
|
238
|
+
def test_knowledge_importable(self):
|
|
239
|
+
from agent.knowledge import KnowledgeBase
|
|
240
|
+
self.assertTrue(callable(KnowledgeBase))
|
|
241
|
+
|
|
242
|
+
def test_knowledge_can_be_constructed(self):
|
|
243
|
+
from agent.knowledge import KnowledgeBase
|
|
244
|
+
root = str(Path(__file__).resolve().parent.parent)
|
|
245
|
+
kb = KnowledgeBase(root, root)
|
|
246
|
+
self.assertIsNotNone(kb)
|
|
247
|
+
|
|
248
|
+
def test_knowledge_load_global(self):
|
|
249
|
+
from agent.knowledge import KnowledgeBase
|
|
250
|
+
root = str(Path(__file__).resolve().parent.parent)
|
|
251
|
+
kb = KnowledgeBase(root, root)
|
|
252
|
+
entries = kb.entries()
|
|
253
|
+
self.assertIsInstance(entries, list)
|
|
254
|
+
|
|
255
|
+
def test_knowledge_has_summary(self):
|
|
256
|
+
from agent.knowledge import KnowledgeBase
|
|
257
|
+
root = str(Path(__file__).resolve().parent.parent)
|
|
258
|
+
kb = KnowledgeBase(root, root)
|
|
259
|
+
s = kb.summary()
|
|
260
|
+
self.assertIsInstance(s, str)
|
|
261
|
+
|
|
262
|
+
def test_knowledge_search_returns_list(self):
|
|
263
|
+
from agent.knowledge import KnowledgeBase
|
|
264
|
+
root = str(Path(__file__).resolve().parent.parent)
|
|
265
|
+
kb = KnowledgeBase(root, root)
|
|
266
|
+
results = kb.search("image")
|
|
267
|
+
self.assertIsInstance(results, list)
|
|
268
|
+
|
|
269
|
+
def test_knowledge_directory_exists(self):
|
|
270
|
+
root = Path(__file__).resolve().parent.parent / "knowledge"
|
|
271
|
+
self.assertTrue(root.exists(), "knowledge/ directory should exist")
|
|
272
|
+
|
|
273
|
+
def test_knowledge_add_and_search(self):
|
|
274
|
+
from agent.knowledge import KnowledgeBase
|
|
275
|
+
root = str(Path(__file__).resolve().parent.parent)
|
|
276
|
+
kb = KnowledgeBase(root, root)
|
|
277
|
+
kb.add_entry("test-category", "test-entry", "a test entry about Python", "Python is a programming language", project_local=False)
|
|
278
|
+
results = kb.search("Python")
|
|
279
|
+
self.assertGreater(len(results), 0)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class AgentsRegistryTests(unittest.TestCase):
|
|
283
|
+
def test_list_agents_returns_all(self):
|
|
284
|
+
from agent.agents import list_agents
|
|
285
|
+
agents = list_agents()
|
|
286
|
+
self.assertGreaterEqual(len(agents), 6, "Should have at least 6 agent types")
|
|
287
|
+
|
|
288
|
+
def test_main_agent_present(self):
|
|
289
|
+
from agent.agents import get_agent
|
|
290
|
+
agent = get_agent("main")
|
|
291
|
+
self.assertIsNotNone(agent)
|
|
292
|
+
self.assertEqual(agent.name, "main")
|
|
293
|
+
|
|
294
|
+
def test_coding_agent_present(self):
|
|
295
|
+
from agent.agents import get_agent
|
|
296
|
+
agent = get_agent("coding")
|
|
297
|
+
self.assertIsNotNone(agent)
|
|
298
|
+
self.assertEqual(agent.name, "coding")
|
|
299
|
+
|
|
300
|
+
def test_image_agent_present(self):
|
|
301
|
+
from agent.agents import get_agent
|
|
302
|
+
agent = get_agent("image")
|
|
303
|
+
self.assertIsNotNone(agent)
|
|
304
|
+
self.assertEqual(agent.name, "image")
|
|
305
|
+
|
|
306
|
+
def test_video_agent_present(self):
|
|
307
|
+
from agent.agents import get_agent
|
|
308
|
+
agent = get_agent("video")
|
|
309
|
+
self.assertIsNotNone(agent)
|
|
310
|
+
self.assertEqual(agent.name, "video")
|
|
311
|
+
|
|
312
|
+
def test_research_agent_present(self):
|
|
313
|
+
from agent.agents import get_agent
|
|
314
|
+
agent = get_agent("research")
|
|
315
|
+
self.assertIsNotNone(agent)
|
|
316
|
+
self.assertEqual(agent.name, "research")
|
|
317
|
+
|
|
318
|
+
def test_knowledge_agent_present(self):
|
|
319
|
+
from agent.agents import get_agent
|
|
320
|
+
agent = get_agent("knowledge")
|
|
321
|
+
self.assertIsNotNone(agent)
|
|
322
|
+
self.assertEqual(agent.name, "knowledge")
|
|
323
|
+
|
|
324
|
+
def test_route_request_returns_agent(self):
|
|
325
|
+
from agent.agents import route_request
|
|
326
|
+
agent = route_request("create an image", None)
|
|
327
|
+
self.assertEqual(agent.name, "image")
|
|
328
|
+
|
|
329
|
+
def test_route_request_fallback_to_main(self):
|
|
330
|
+
from agent.agents import route_request
|
|
331
|
+
agent = route_request("hello world", None)
|
|
332
|
+
self.assertEqual(agent.name, "main")
|
|
333
|
+
|
|
334
|
+
def test_route_request_coding_keywords(self):
|
|
335
|
+
from agent.agents import route_request
|
|
336
|
+
agent = route_request("fix my code", None)
|
|
337
|
+
self.assertEqual(agent.name, "coding")
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class ProvidersTests(unittest.TestCase):
|
|
341
|
+
def test_providers_importable(self):
|
|
342
|
+
from agent.providers import PROVIDERS
|
|
343
|
+
self.assertIsInstance(PROVIDERS, list)
|
|
344
|
+
|
|
345
|
+
def test_providers_has_required_types(self):
|
|
346
|
+
from agent.providers import PROVIDERS
|
|
347
|
+
names = [p.name for p in PROVIDERS]
|
|
348
|
+
self.assertIn("ollama", names)
|
|
349
|
+
|
|
350
|
+
def test_get_provider_capable_of_returns_provider(self):
|
|
351
|
+
from agent.providers import get_provider_capable_of
|
|
352
|
+
provider = get_provider_capable_of("text")
|
|
353
|
+
self.assertIsNotNone(provider)
|
|
354
|
+
|
|
355
|
+
def test_get_provider_capable_of_returns_none_for_missing(self):
|
|
356
|
+
from agent.providers import get_provider_capable_of
|
|
357
|
+
provider = get_provider_capable_of("nonexistent-capability")
|
|
358
|
+
self.assertIsNone(provider)
|
|
359
|
+
|
|
360
|
+
def test_image_provider_importable(self):
|
|
361
|
+
from agent.providers.image import ImageProvider
|
|
362
|
+
self.assertTrue(hasattr(ImageProvider, "status_string"))
|
|
363
|
+
|
|
364
|
+
def test_video_provider_importable(self):
|
|
365
|
+
from agent.providers.video import VideoProvider
|
|
366
|
+
self.assertTrue(hasattr(VideoProvider, "status_string"))
|
|
367
|
+
|
|
368
|
+
def test_web_search_provider_importable(self):
|
|
369
|
+
from agent.providers.web import WebSearchProvider
|
|
370
|
+
self.assertTrue(hasattr(WebSearchProvider, "status_string"))
|
|
371
|
+
|
|
372
|
+
def test_search_ddg_function_exists(self):
|
|
373
|
+
from agent.providers.web import search_ddg
|
|
374
|
+
self.assertTrue(callable(search_ddg))
|
|
375
|
+
|
|
376
|
+
def test_fetch_web_page_function_exists(self):
|
|
377
|
+
from agent.providers.web import fetch_web_page
|
|
378
|
+
self.assertTrue(callable(fetch_web_page))
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
class ToolRegistryTests(unittest.TestCase):
|
|
382
|
+
def test_register_builtin_tools_callable(self):
|
|
383
|
+
from agent.builtin_tools import register_builtin_tools
|
|
384
|
+
self.assertTrue(callable(register_builtin_tools))
|
|
385
|
+
|
|
386
|
+
def test_tool_registry_has_tools(self):
|
|
387
|
+
from agent.tools import ToolRegistry
|
|
388
|
+
from agent.builtin_tools import register_builtin_tools
|
|
389
|
+
from agent.workspace import Workspace, find_project_root
|
|
390
|
+
from agent.memory import Memory
|
|
391
|
+
from pathlib import Path
|
|
392
|
+
ws = Workspace(find_project_root(Path.cwd()))
|
|
393
|
+
mem = Memory(ws)
|
|
394
|
+
registry = ToolRegistry()
|
|
395
|
+
register_builtin_tools(registry, ws, mem)
|
|
396
|
+
self.assertGreater(len(registry), 0)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
class CLITests(unittest.TestCase):
|
|
400
|
+
def test_eljay_has_main(self):
|
|
401
|
+
import eljay
|
|
402
|
+
self.assertTrue(callable(eljay.main))
|
|
403
|
+
|
|
404
|
+
def test_myagent_is_alias(self):
|
|
405
|
+
import myagent
|
|
406
|
+
self.assertTrue(callable(myagent.main))
|
|
407
|
+
|
|
408
|
+
def test_build_registry_callable(self):
|
|
409
|
+
import eljay
|
|
410
|
+
self.assertTrue(callable(eljay.build_registry))
|
|
411
|
+
|
|
412
|
+
def test_resolve_model_callable(self):
|
|
413
|
+
import eljay
|
|
414
|
+
self.assertTrue(callable(eljay.resolve_model))
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
class OutputDirectoryTests(unittest.TestCase):
|
|
418
|
+
def test_agent_outputs_dir_exists_or_can_create(self):
|
|
419
|
+
"""Ensure the .agent/outputs directory structure can be created."""
|
|
420
|
+
from pathlib import Path
|
|
421
|
+
root = Path(__file__).resolve().parent.parent
|
|
422
|
+
outputs = root / ".agent" / "outputs"
|
|
423
|
+
images = outputs / "images"
|
|
424
|
+
videos = outputs / "videos"
|
|
425
|
+
images.mkdir(parents=True, exist_ok=True)
|
|
426
|
+
videos.mkdir(parents=True, exist_ok=True)
|
|
427
|
+
self.assertTrue(images.exists())
|
|
428
|
+
self.assertTrue(videos.exists())
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
class ProviderFactoryTests(unittest.TestCase):
|
|
432
|
+
"""Verify all four provider classes can be instantiated."""
|
|
433
|
+
|
|
434
|
+
def test_ollama_provider_instantiable(self):
|
|
435
|
+
from agent.providers.ollama import OllamaProvider
|
|
436
|
+
p = OllamaProvider()
|
|
437
|
+
self.assertEqual(p.name, "ollama")
|
|
438
|
+
self.assertIn("text", p.capabilities)
|
|
439
|
+
|
|
440
|
+
def test_web_search_provider_instantiable(self):
|
|
441
|
+
from agent.providers.web import WebSearchProvider
|
|
442
|
+
p = WebSearchProvider()
|
|
443
|
+
self.assertEqual(p.name, "duckduckgo")
|
|
444
|
+
self.assertIn("web-search", p.capabilities)
|
|
445
|
+
|
|
446
|
+
def test_image_provider_instantiable(self):
|
|
447
|
+
from agent.providers.image import ImageProvider
|
|
448
|
+
p = ImageProvider()
|
|
449
|
+
self.assertEqual(p.name, "image-local")
|
|
450
|
+
|
|
451
|
+
def test_video_provider_instantiable(self):
|
|
452
|
+
from agent.providers.video import VideoProvider
|
|
453
|
+
p = VideoProvider()
|
|
454
|
+
self.assertEqual(p.name, "video-local")
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
if __name__ == "__main__":
|
|
458
|
+
unittest.main(verbosity=2)
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Deterministic tests for the workspace module — no model, no network.
|
|
2
|
+
|
|
3
|
+
Run from the project root:
|
|
4
|
+
python tests/test_workspace.py
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import tempfile
|
|
11
|
+
import unittest
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
15
|
+
|
|
16
|
+
from agent.workspace import ( # noqa: E402
|
|
17
|
+
Workspace,
|
|
18
|
+
find_project_root,
|
|
19
|
+
human_size,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class WorkspaceTests(unittest.TestCase):
|
|
24
|
+
def setUp(self):
|
|
25
|
+
self._tmp = tempfile.TemporaryDirectory()
|
|
26
|
+
self.root = Path(self._tmp.name)
|
|
27
|
+
(self.root / "a.py").write_text("print('hi')\n", encoding="utf-8")
|
|
28
|
+
(self.root / "sub").mkdir()
|
|
29
|
+
(self.root / "sub" / "b.txt").write_text("hello", encoding="utf-8")
|
|
30
|
+
(self.root / "node_modules").mkdir()
|
|
31
|
+
(self.root / "node_modules" / "x.js").write_text("ignore me", encoding="utf-8")
|
|
32
|
+
self.ws = Workspace(self.root)
|
|
33
|
+
|
|
34
|
+
def tearDown(self):
|
|
35
|
+
self._tmp.cleanup()
|
|
36
|
+
|
|
37
|
+
# -- safety ------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
def test_resolve_allows_paths_inside_root(self):
|
|
40
|
+
self.assertEqual(self.ws.resolve("sub"), (self.root / "sub").resolve())
|
|
41
|
+
|
|
42
|
+
def test_resolve_rejects_escape_attempts(self):
|
|
43
|
+
with self.assertRaises(ValueError):
|
|
44
|
+
self.ws.resolve("..")
|
|
45
|
+
with self.assertRaises(ValueError):
|
|
46
|
+
self.ws.resolve("../../etc/passwd")
|
|
47
|
+
|
|
48
|
+
def test_resolve_rejects_absolute_path_outside_root(self):
|
|
49
|
+
with self.assertRaises(ValueError):
|
|
50
|
+
self.ws.resolve(str(self.root.parent))
|
|
51
|
+
|
|
52
|
+
# -- listing -----------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
def test_list_tree_shows_files_and_dirs(self):
|
|
55
|
+
out = self.ws.list_tree(depth=3)
|
|
56
|
+
self.assertIn("a.py", out)
|
|
57
|
+
self.assertIn("sub/", out)
|
|
58
|
+
self.assertIn("b.txt", out)
|
|
59
|
+
|
|
60
|
+
def test_list_tree_skips_ignored_dirs(self):
|
|
61
|
+
out = self.ws.list_tree(depth=3)
|
|
62
|
+
self.assertNotIn("node_modules", out)
|
|
63
|
+
self.assertNotIn("x.js", out)
|
|
64
|
+
|
|
65
|
+
def test_list_tree_respects_depth(self):
|
|
66
|
+
out = self.ws.list_tree(depth=1)
|
|
67
|
+
self.assertIn("sub/", out)
|
|
68
|
+
self.assertNotIn("b.txt", out) # one level down should be hidden
|
|
69
|
+
|
|
70
|
+
def test_list_tree_truncates(self):
|
|
71
|
+
for i in range(20):
|
|
72
|
+
(self.root / f"file_{i}.txt").write_text("x", encoding="utf-8")
|
|
73
|
+
out = self.ws.list_tree(depth=2, max_entries=5)
|
|
74
|
+
self.assertIn("truncated", out)
|
|
75
|
+
|
|
76
|
+
def test_list_tree_missing_path_reports_error(self):
|
|
77
|
+
out = self.ws.list_tree(rel="does_not_exist")
|
|
78
|
+
self.assertIn("does not exist", out)
|
|
79
|
+
|
|
80
|
+
def test_list_tree_outside_root_reports_error(self):
|
|
81
|
+
out = self.ws.list_tree(rel="../..")
|
|
82
|
+
self.assertIn("ERROR", out)
|
|
83
|
+
|
|
84
|
+
# -- detection ---------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
def test_find_project_root_from_nested_dir(self):
|
|
87
|
+
(self.root / ".git").mkdir()
|
|
88
|
+
nested = self.root / "deep" / "deeper"
|
|
89
|
+
nested.mkdir(parents=True)
|
|
90
|
+
self.assertEqual(find_project_root(nested), self.root.resolve())
|
|
91
|
+
|
|
92
|
+
def test_detect_stack_react(self):
|
|
93
|
+
(self.root / "package.json").write_text(
|
|
94
|
+
json.dumps({"dependencies": {"react": "^18.0.0"}}), encoding="utf-8"
|
|
95
|
+
)
|
|
96
|
+
stacks = self.ws.detect_stack()
|
|
97
|
+
self.assertIn("Node.js", stacks)
|
|
98
|
+
self.assertIn("React", stacks)
|
|
99
|
+
|
|
100
|
+
def test_detect_stack_laravel(self):
|
|
101
|
+
(self.root / "artisan").write_text("", encoding="utf-8")
|
|
102
|
+
(self.root / "composer.json").write_text(
|
|
103
|
+
json.dumps({"require": {"laravel/framework": "^11.0"}}), encoding="utf-8"
|
|
104
|
+
)
|
|
105
|
+
stacks = self.ws.detect_stack()
|
|
106
|
+
self.assertIn("PHP", stacks)
|
|
107
|
+
self.assertIn("Laravel", stacks)
|
|
108
|
+
|
|
109
|
+
def test_detect_stack_database_from_env(self):
|
|
110
|
+
(self.root / ".env").write_text("DB_CONNECTION=mysql\n", encoding="utf-8")
|
|
111
|
+
self.assertIn("MySQL", self.ws.detect_stack())
|
|
112
|
+
|
|
113
|
+
# -- reading -----------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
def test_read_file_returns_numbered_lines(self):
|
|
116
|
+
out = self.ws.read_file("sub/b.txt")
|
|
117
|
+
self.assertIn("hello", out)
|
|
118
|
+
self.assertIn("1 |", out)
|
|
119
|
+
|
|
120
|
+
def test_read_file_line_range(self):
|
|
121
|
+
(self.root / "nums.txt").write_text(
|
|
122
|
+
"\n".join(f"line{i}" for i in range(1, 11)) + "\n", encoding="utf-8"
|
|
123
|
+
)
|
|
124
|
+
out = self.ws.read_file("nums.txt", start_line=3, end_line=5)
|
|
125
|
+
self.assertIn("line3", out)
|
|
126
|
+
self.assertIn("line5", out)
|
|
127
|
+
self.assertNotIn("line2", out)
|
|
128
|
+
self.assertNotIn("line6", out)
|
|
129
|
+
|
|
130
|
+
def test_read_file_refuses_directory(self):
|
|
131
|
+
out = self.ws.read_file("sub")
|
|
132
|
+
self.assertIn("is a directory", out)
|
|
133
|
+
|
|
134
|
+
def test_read_file_refuses_binary(self):
|
|
135
|
+
(self.root / "blob.bin").write_bytes(b"PNG\x00\x01\x02binary")
|
|
136
|
+
out = self.ws.read_file("blob.bin")
|
|
137
|
+
self.assertIn("binary", out.lower())
|
|
138
|
+
|
|
139
|
+
def test_read_file_refuses_large_file_without_range(self):
|
|
140
|
+
(self.root / "big.txt").write_text("a" * 1_100_000, encoding="utf-8")
|
|
141
|
+
out = self.ws.read_file("big.txt")
|
|
142
|
+
self.assertIn("larger than", out)
|
|
143
|
+
|
|
144
|
+
def test_read_file_large_file_with_range_is_allowed(self):
|
|
145
|
+
# >1 MB file made of many short lines, so a small range stays small.
|
|
146
|
+
(self.root / "big2.txt").write_text("x\n" * 600_000, encoding="utf-8")
|
|
147
|
+
out = self.ws.read_file("big2.txt", start_line=1, end_line=2)
|
|
148
|
+
self.assertNotIn("larger than", out)
|
|
149
|
+
self.assertIn("1 |", out)
|
|
150
|
+
|
|
151
|
+
def test_read_file_outside_root_is_rejected(self):
|
|
152
|
+
out = self.ws.read_file("../../secret.txt")
|
|
153
|
+
self.assertIn("ERROR", out)
|
|
154
|
+
|
|
155
|
+
def test_read_file_missing_reports_error(self):
|
|
156
|
+
out = self.ws.read_file("nope.txt")
|
|
157
|
+
self.assertIn("does not exist", out)
|
|
158
|
+
|
|
159
|
+
def test_read_file_reversed_range_reports_error(self):
|
|
160
|
+
out = self.ws.read_file("a.py", start_line=5, end_line=2)
|
|
161
|
+
self.assertIn("ERROR", out)
|
|
162
|
+
|
|
163
|
+
# -- searching ---------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
def test_search_finds_literal_with_line_number(self):
|
|
166
|
+
(self.root / "code.py").write_text(
|
|
167
|
+
"x = 1\ndef useAuth():\n pass\n", encoding="utf-8"
|
|
168
|
+
)
|
|
169
|
+
out = self.ws.search("useAuth")
|
|
170
|
+
self.assertIn("code.py:2:", out)
|
|
171
|
+
self.assertIn("def useAuth", out)
|
|
172
|
+
|
|
173
|
+
def test_search_is_case_insensitive_by_default(self):
|
|
174
|
+
(self.root / "code.py").write_text("UseAuth()\n", encoding="utf-8")
|
|
175
|
+
self.assertIn("code.py", self.ws.search("useauth"))
|
|
176
|
+
|
|
177
|
+
def test_search_case_sensitive(self):
|
|
178
|
+
(self.root / "code.py").write_text("UseAuth()\n", encoding="utf-8")
|
|
179
|
+
self.assertIn("No matches", self.ws.search("useauth", case_sensitive=True))
|
|
180
|
+
|
|
181
|
+
def test_search_regex(self):
|
|
182
|
+
(self.root / "code.py").write_text("foo123\nbar\n", encoding="utf-8")
|
|
183
|
+
self.assertIn("foo123", self.ws.search(r"foo\d+", regex=True))
|
|
184
|
+
|
|
185
|
+
def test_search_invalid_regex_reports_error(self):
|
|
186
|
+
self.assertIn("ERROR", self.ws.search("([", regex=True))
|
|
187
|
+
|
|
188
|
+
def test_search_empty_query_reports_error(self):
|
|
189
|
+
self.assertIn("ERROR", self.ws.search(""))
|
|
190
|
+
|
|
191
|
+
def test_search_missing_path_reports_error(self):
|
|
192
|
+
self.assertIn("ERROR", self.ws.search("x", rel="nope"))
|
|
193
|
+
|
|
194
|
+
def test_search_skips_ignored_dirs(self):
|
|
195
|
+
(self.root / "node_modules" / "x.js").write_text("needle\n", encoding="utf-8")
|
|
196
|
+
self.assertIn("No matches", self.ws.search("needle"))
|
|
197
|
+
|
|
198
|
+
def test_search_skips_binary_files(self):
|
|
199
|
+
(self.root / "blob.bin").write_bytes(b"\x00needle")
|
|
200
|
+
self.assertIn("No matches", self.ws.search("needle"))
|
|
201
|
+
|
|
202
|
+
def test_search_truncates_at_max_results(self):
|
|
203
|
+
(self.root / "many.txt").write_text("hit\n" * 20, encoding="utf-8")
|
|
204
|
+
out = self.ws.search("hit", max_results=5)
|
|
205
|
+
self.assertIn("truncated", out)
|
|
206
|
+
|
|
207
|
+
def test_search_scoped_to_subdir_excludes_elsewhere(self):
|
|
208
|
+
(self.root / "a.py").write_text("needle\n", encoding="utf-8")
|
|
209
|
+
(self.root / "sub" / "b.txt").write_text("needle\n", encoding="utf-8")
|
|
210
|
+
out = self.ws.search("needle", rel="sub")
|
|
211
|
+
self.assertIn("b.txt", out)
|
|
212
|
+
self.assertNotIn("a.py", out)
|
|
213
|
+
|
|
214
|
+
def test_human_size(self):
|
|
215
|
+
self.assertEqual(human_size(0), "0 B")
|
|
216
|
+
self.assertEqual(human_size(512), "512 B")
|
|
217
|
+
self.assertEqual(human_size(2048), "2.0 KB")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
if __name__ == "__main__":
|
|
221
|
+
unittest.main(verbosity=2)
|