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,209 @@
|
|
|
1
|
+
"""Tests for the ElJay AI rename, version, compatibility alias, and npm packaging.
|
|
2
|
+
|
|
3
|
+
Run from the project root:
|
|
4
|
+
python tests/test_eljay.py
|
|
5
|
+
|
|
6
|
+
These are deterministic and do NOT require Ollama or network access.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
import unittest
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
16
|
+
|
|
17
|
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class VersionTests(unittest.TestCase):
|
|
21
|
+
def test_version_string(self):
|
|
22
|
+
from agent import __version__
|
|
23
|
+
self.assertEqual(__version__, "1.1.0")
|
|
24
|
+
|
|
25
|
+
def test_version_is_semver(self):
|
|
26
|
+
from agent import __version__
|
|
27
|
+
parts = __version__.split(".")
|
|
28
|
+
self.assertEqual(len(parts), 3)
|
|
29
|
+
for p in parts:
|
|
30
|
+
self.assertTrue(p.isdigit())
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class BannerTests(unittest.TestCase):
|
|
34
|
+
def test_banner_contains_eljay_ai(self):
|
|
35
|
+
from agent.chat import BANNER
|
|
36
|
+
self.assertIn("ElJay AI", BANNER)
|
|
37
|
+
|
|
38
|
+
def test_banner_does_not_say_myagent(self):
|
|
39
|
+
from agent.chat import BANNER
|
|
40
|
+
self.assertNotIn("LOCAL AI CODING AGENT", BANNER)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class EljayEntryTests(unittest.TestCase):
|
|
44
|
+
def test_eljay_module_importable(self):
|
|
45
|
+
import eljay # noqa: F401
|
|
46
|
+
self.assertTrue(hasattr(eljay, "main"))
|
|
47
|
+
|
|
48
|
+
def test_eljay_has_resolve_model(self):
|
|
49
|
+
import eljay
|
|
50
|
+
self.assertTrue(callable(eljay.resolve_model))
|
|
51
|
+
|
|
52
|
+
def test_eljay_has_build_registry(self):
|
|
53
|
+
import eljay
|
|
54
|
+
self.assertTrue(callable(eljay.build_registry))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class MyagentAliasTests(unittest.TestCase):
|
|
58
|
+
def test_myagent_module_importable(self):
|
|
59
|
+
import myagent # noqa: F401
|
|
60
|
+
self.assertTrue(hasattr(myagent, "main"))
|
|
61
|
+
|
|
62
|
+
def test_myagent_main_delegates_to_eljay(self):
|
|
63
|
+
import myagent
|
|
64
|
+
import eljay
|
|
65
|
+
# myagent.main should be eljay.main (same function object)
|
|
66
|
+
self.assertIs(myagent.main, eljay.main)
|
|
67
|
+
|
|
68
|
+
def test_myagent_resolve_model_delegates(self):
|
|
69
|
+
import myagent
|
|
70
|
+
import eljay
|
|
71
|
+
self.assertIs(myagent.resolve_model, eljay.resolve_model)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class HelpTextTests(unittest.TestCase):
|
|
75
|
+
def test_help_mentions_eljay(self):
|
|
76
|
+
# Read eljay.py source and check the --help text
|
|
77
|
+
src = (PROJECT_ROOT / "eljay.py").read_text(encoding="utf-8")
|
|
78
|
+
self.assertIn("eljay", src)
|
|
79
|
+
# The help line should reference eljay
|
|
80
|
+
self.assertIn("usage: eljay", src)
|
|
81
|
+
|
|
82
|
+
def test_version_flag_mentions_eljay_ai(self):
|
|
83
|
+
src = (PROJECT_ROOT / "eljay.py").read_text(encoding="utf-8")
|
|
84
|
+
self.assertIn("ElJay AI {__version__}", src)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class PackageJsonTests(unittest.TestCase):
|
|
88
|
+
def test_package_json_exists(self):
|
|
89
|
+
self.assertTrue((PROJECT_ROOT / "package.json").exists())
|
|
90
|
+
|
|
91
|
+
def test_package_json_name(self):
|
|
92
|
+
data = json.loads((PROJECT_ROOT / "package.json").read_text())
|
|
93
|
+
self.assertEqual(data["name"], "eljay-ai")
|
|
94
|
+
|
|
95
|
+
def test_package_json_version(self):
|
|
96
|
+
data = json.loads((PROJECT_ROOT / "package.json").read_text())
|
|
97
|
+
self.assertEqual(data["version"], "1.1.0")
|
|
98
|
+
|
|
99
|
+
def test_package_json_bin_field(self):
|
|
100
|
+
data = json.loads((PROJECT_ROOT / "package.json").read_text())
|
|
101
|
+
bins = data["bin"]
|
|
102
|
+
self.assertIn("eljay", bins)
|
|
103
|
+
self.assertEqual(bins["eljay"], "eljay.js")
|
|
104
|
+
|
|
105
|
+
def test_package_json_prefer_global(self):
|
|
106
|
+
data = json.loads((PROJECT_ROOT / "package.json").read_text())
|
|
107
|
+
self.assertTrue(data.get("preferGlobal", False))
|
|
108
|
+
|
|
109
|
+
def test_package_json_has_engines(self):
|
|
110
|
+
data = json.loads((PROJECT_ROOT / "package.json").read_text())
|
|
111
|
+
self.assertIn("node", data["engines"])
|
|
112
|
+
self.assertIn("python", data["engines"])
|
|
113
|
+
|
|
114
|
+
def test_package_json_keywords(self):
|
|
115
|
+
data = json.loads((PROJECT_ROOT / "package.json").read_text())
|
|
116
|
+
self.assertIsInstance(data["keywords"], list)
|
|
117
|
+
self.assertTrue(len(data["keywords"]) > 0)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class EljayJsTests(unittest.TestCase):
|
|
121
|
+
def test_eljay_js_exists(self):
|
|
122
|
+
self.assertTrue((PROJECT_ROOT / "eljay.js").exists())
|
|
123
|
+
|
|
124
|
+
def test_eljay_js_is_node_script(self):
|
|
125
|
+
first_line = (PROJECT_ROOT / "eljay.js").read_text(encoding="utf-8").splitlines()[0]
|
|
126
|
+
self.assertTrue("node" in first_line)
|
|
127
|
+
|
|
128
|
+
def test_eljay_js_references_eljay_py(self):
|
|
129
|
+
content = (PROJECT_ROOT / "eljay.js").read_text(encoding="utf-8")
|
|
130
|
+
self.assertIn("eljay.py", content)
|
|
131
|
+
|
|
132
|
+
def test_eljay_js_inherits_stdio(self):
|
|
133
|
+
content = (PROJECT_ROOT / "eljay.js").read_text(encoding="utf-8")
|
|
134
|
+
self.assertIn("stdio", content)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class LauncherTests(unittest.TestCase):
|
|
138
|
+
def test_bin_eljay_exists(self):
|
|
139
|
+
path = PROJECT_ROOT / "bin" / "eljay"
|
|
140
|
+
self.assertTrue(path.exists())
|
|
141
|
+
self.assertTrue(os.access(path, os.X_OK), "bin/eljay should be executable")
|
|
142
|
+
|
|
143
|
+
def test_bin_eljay_cmd_exists(self):
|
|
144
|
+
self.assertTrue((PROJECT_ROOT / "bin" / "eljay.cmd").exists())
|
|
145
|
+
|
|
146
|
+
def test_bin_myagent_still_exists(self):
|
|
147
|
+
self.assertTrue((PROJECT_ROOT / "bin" / "myagent").exists())
|
|
148
|
+
self.assertTrue((PROJECT_ROOT / "bin" / "myagent.cmd").exists())
|
|
149
|
+
|
|
150
|
+
def test_bin_eljay_references_eljay_py(self):
|
|
151
|
+
content = (PROJECT_ROOT / "bin" / "eljay").read_text(encoding="utf-8")
|
|
152
|
+
self.assertIn("eljay.py", content)
|
|
153
|
+
|
|
154
|
+
def test_bin_eljay_cmd_references_eljay_py(self):
|
|
155
|
+
content = (PROJECT_ROOT / "bin" / "eljay.cmd").read_text(encoding="utf-8")
|
|
156
|
+
self.assertIn("eljay.py", content)
|
|
157
|
+
|
|
158
|
+
def test_bin_myagent_references_eljay_py(self):
|
|
159
|
+
"""myagent launcher should delegate to eljay.py (compatibility)."""
|
|
160
|
+
content = (PROJECT_ROOT / "bin" / "myagent").read_text(encoding="utf-8")
|
|
161
|
+
self.assertIn("eljay.py", content)
|
|
162
|
+
|
|
163
|
+
def test_bin_myagent_cmd_references_eljay_py(self):
|
|
164
|
+
content = (PROJECT_ROOT / "bin" / "myagent.cmd").read_text(encoding="utf-8")
|
|
165
|
+
self.assertIn("eljay.py", content)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class InstallPs1Tests(unittest.TestCase):
|
|
169
|
+
def test_install_ps1_references_eljay(self):
|
|
170
|
+
content = (PROJECT_ROOT / "install.ps1").read_text(encoding="utf-8")
|
|
171
|
+
self.assertIn("eljay", content)
|
|
172
|
+
# Should not say 'run myagent' as the primary instruction
|
|
173
|
+
self.assertNotIn(" myagent", content)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class GitignoreTests(unittest.TestCase):
|
|
177
|
+
def test_gitignore_has_agent_dir(self):
|
|
178
|
+
content = (PROJECT_ROOT / ".gitignore").read_text(encoding="utf-8")
|
|
179
|
+
self.assertIn(".agent/", content)
|
|
180
|
+
|
|
181
|
+
def test_gitignore_has_pycache(self):
|
|
182
|
+
content = (PROJECT_ROOT / ".gitignore").read_text(encoding="utf-8")
|
|
183
|
+
self.assertIn("__pycache__/", content)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class RulesTestsAlias(unittest.TestCase):
|
|
187
|
+
def test_agents_md_references_eljay(self):
|
|
188
|
+
content = (PROJECT_ROOT / "AGENTS.md").read_text(encoding="utf-8")
|
|
189
|
+
self.assertIn("eljay.py", content.lower())
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class ProviderFixTests(unittest.TestCase):
|
|
193
|
+
"""Verify the _do_text fix in the Ollama provider."""
|
|
194
|
+
|
|
195
|
+
def test_ollama_provider_do_text_signature(self):
|
|
196
|
+
from agent.providers.ollama import OllamaProvider
|
|
197
|
+
# Just verify the method exists and is callable
|
|
198
|
+
self.assertTrue(hasattr(OllamaProvider, "_do_text"))
|
|
199
|
+
|
|
200
|
+
def test_ollama_provider_no_model_kwarg_in_client(self):
|
|
201
|
+
"""The provider should not pass model= to OllamaClient (it doesn't accept it)."""
|
|
202
|
+
from agent.providers.ollama import OllamaProvider
|
|
203
|
+
import inspect
|
|
204
|
+
src = inspect.getsource(OllamaProvider._do_text)
|
|
205
|
+
self.assertNotIn("OllamaClient(host=self.host, model=", src)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
if __name__ == "__main__":
|
|
209
|
+
unittest.main(verbosity=2)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Tests for the command runner — classification + real (safe) execution.
|
|
2
|
+
|
|
3
|
+
Run from the project root:
|
|
4
|
+
python tests/test_runner.py
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
import unittest
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
14
|
+
|
|
15
|
+
from agent.runner import analyse, classify_command, run_command # noqa: E402
|
|
16
|
+
from agent.tools import RISK_ASK, RISK_HIGH # noqa: E402
|
|
17
|
+
from agent.workspace import Workspace # noqa: E402
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ClassifyTests(unittest.TestCase):
|
|
21
|
+
def test_allow_listed_programs_are_ask(self):
|
|
22
|
+
for cmd in (
|
|
23
|
+
"python --version",
|
|
24
|
+
"npm run build",
|
|
25
|
+
"php artisan test",
|
|
26
|
+
"git status",
|
|
27
|
+
"node -v",
|
|
28
|
+
"pytest",
|
|
29
|
+
):
|
|
30
|
+
self.assertEqual(classify_command(cmd), RISK_ASK, cmd)
|
|
31
|
+
|
|
32
|
+
def test_unknown_program_is_high(self):
|
|
33
|
+
self.assertEqual(classify_command("some_random_tool --go"), RISK_HIGH)
|
|
34
|
+
|
|
35
|
+
def test_dangerous_commands_are_high(self):
|
|
36
|
+
for cmd in (
|
|
37
|
+
"rm -rf /",
|
|
38
|
+
"rmdir somedir",
|
|
39
|
+
"del important.txt",
|
|
40
|
+
"git push origin main",
|
|
41
|
+
"git reset --hard HEAD",
|
|
42
|
+
"git clean -fd",
|
|
43
|
+
"npm uninstall react",
|
|
44
|
+
"composer remove laravel/framework",
|
|
45
|
+
"pip uninstall requests",
|
|
46
|
+
"drop database prod",
|
|
47
|
+
"shutdown now",
|
|
48
|
+
"sudo reboot",
|
|
49
|
+
):
|
|
50
|
+
self.assertEqual(classify_command(cmd), RISK_HIGH, cmd)
|
|
51
|
+
|
|
52
|
+
def test_empty_command_is_high(self):
|
|
53
|
+
self.assertEqual(classify_command(""), RISK_HIGH)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class AnalyseTests(unittest.TestCase):
|
|
57
|
+
def test_simple_command_parses(self):
|
|
58
|
+
argv, err = analyse("npm run build")
|
|
59
|
+
self.assertEqual(argv, ["npm", "run", "build"])
|
|
60
|
+
self.assertEqual(err, "")
|
|
61
|
+
|
|
62
|
+
def test_shell_operators_rejected(self):
|
|
63
|
+
for cmd in ("a | b", "a > file.txt", "a && b", "a; b", "a `b`", "a $(b)"):
|
|
64
|
+
argv, err = analyse(cmd)
|
|
65
|
+
self.assertIsNone(argv, cmd)
|
|
66
|
+
self.assertIn("ERROR", err)
|
|
67
|
+
|
|
68
|
+
def test_empty_rejected(self):
|
|
69
|
+
argv, err = analyse(" ")
|
|
70
|
+
self.assertIsNone(argv)
|
|
71
|
+
self.assertIn("ERROR", err)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class RunTests(unittest.TestCase):
|
|
75
|
+
def setUp(self):
|
|
76
|
+
self._tmp = tempfile.TemporaryDirectory()
|
|
77
|
+
self.root = Path(self._tmp.name)
|
|
78
|
+
self.ws = Workspace(self.root)
|
|
79
|
+
|
|
80
|
+
def tearDown(self):
|
|
81
|
+
self._tmp.cleanup()
|
|
82
|
+
|
|
83
|
+
def test_runs_and_captures_output(self):
|
|
84
|
+
out = run_command(self.ws, "python --version")
|
|
85
|
+
self.assertIn("Exit code: 0", out)
|
|
86
|
+
self.assertIn("Python", out)
|
|
87
|
+
|
|
88
|
+
def test_failing_command_reports_failure(self):
|
|
89
|
+
out = run_command(self.ws, "python does_not_exist_xyz.py")
|
|
90
|
+
self.assertIn("FAILED", out)
|
|
91
|
+
self.assertNotIn("Exit code: 0", out)
|
|
92
|
+
|
|
93
|
+
def test_unknown_program_reports_error(self):
|
|
94
|
+
out = run_command(self.ws, "definitely_not_a_real_program_xyz")
|
|
95
|
+
self.assertIn("command not found", out)
|
|
96
|
+
|
|
97
|
+
def test_timeout_is_enforced(self):
|
|
98
|
+
(self.root / "slow.py").write_text(
|
|
99
|
+
"import time\ntime.sleep(10)\n", encoding="utf-8"
|
|
100
|
+
)
|
|
101
|
+
out = run_command(self.ws, "python slow.py", timeout=1)
|
|
102
|
+
self.assertIn("timed out", out)
|
|
103
|
+
|
|
104
|
+
def test_cwd_outside_root_rejected(self):
|
|
105
|
+
out = run_command(self.ws, "python --version", cwd="../..")
|
|
106
|
+
self.assertIn("ERROR", out)
|
|
107
|
+
|
|
108
|
+
def test_shell_operator_rejected_at_run(self):
|
|
109
|
+
out = run_command(self.ws, "python --version | more")
|
|
110
|
+
self.assertIn("ERROR", out)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
if __name__ == "__main__":
|
|
114
|
+
unittest.main(verbosity=2)
|