ara-cli 0.1.9.95__py3-none-any.whl → 0.1.10.0__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.
- ara_cli/__init__.py +5 -2
- ara_cli/__main__.py +61 -13
- ara_cli/ara_command_action.py +85 -20
- ara_cli/ara_command_parser.py +42 -2
- ara_cli/ara_config.py +118 -94
- ara_cli/artefact_autofix.py +131 -2
- ara_cli/artefact_creator.py +2 -7
- ara_cli/artefact_deleter.py +2 -4
- ara_cli/artefact_fuzzy_search.py +13 -6
- ara_cli/artefact_models/artefact_templates.py +3 -3
- ara_cli/artefact_models/feature_artefact_model.py +25 -0
- ara_cli/artefact_reader.py +4 -5
- ara_cli/chat.py +210 -150
- ara_cli/commands/extract_command.py +4 -11
- ara_cli/error_handler.py +134 -0
- ara_cli/file_classifier.py +3 -2
- ara_cli/prompt_extractor.py +1 -1
- ara_cli/prompt_handler.py +268 -127
- ara_cli/template_loader.py +245 -0
- ara_cli/version.py +1 -1
- {ara_cli-0.1.9.95.dist-info → ara_cli-0.1.10.0.dist-info}/METADATA +2 -1
- {ara_cli-0.1.9.95.dist-info → ara_cli-0.1.10.0.dist-info}/RECORD +32 -29
- tests/test_ara_command_action.py +66 -52
- tests/test_artefact_autofix.py +361 -5
- tests/test_chat.py +1894 -546
- tests/test_file_classifier.py +23 -0
- tests/test_file_creator.py +3 -5
- tests/test_prompt_handler.py +40 -4
- tests/test_template_loader.py +192 -0
- {ara_cli-0.1.9.95.dist-info → ara_cli-0.1.10.0.dist-info}/WHEEL +0 -0
- {ara_cli-0.1.9.95.dist-info → ara_cli-0.1.10.0.dist-info}/entry_points.txt +0 -0
- {ara_cli-0.1.9.95.dist-info → ara_cli-0.1.10.0.dist-info}/top_level.txt +0 -0
tests/test_file_classifier.py
CHANGED
|
@@ -11,6 +11,12 @@ def mock_file_system():
|
|
|
11
11
|
return MagicMock()
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
@pytest.fixture
|
|
15
|
+
def mock_error_handler():
|
|
16
|
+
with patch('ara_cli.file_classifier.error_handler') as mock_handler:
|
|
17
|
+
yield mock_handler
|
|
18
|
+
|
|
19
|
+
|
|
14
20
|
@pytest.fixture
|
|
15
21
|
def mock_classifier():
|
|
16
22
|
with patch.object(Classifier, 'ordered_classifiers', return_value=['py', 'txt', 'bin']):
|
|
@@ -64,6 +70,23 @@ def test_is_binary_file(mock_file_system):
|
|
|
64
70
|
assert result is False
|
|
65
71
|
|
|
66
72
|
|
|
73
|
+
def test_is_binary_file_handles_error(mock_file_system, mock_error_handler):
|
|
74
|
+
classifier = FileClassifier(mock_file_system)
|
|
75
|
+
test_binary_file_path = "test_binary_file.bin"
|
|
76
|
+
|
|
77
|
+
# Simulate an exception being raised when attempting to open the file
|
|
78
|
+
with patch("builtins.open", side_effect=Exception("Unexpected error")):
|
|
79
|
+
result = classifier.is_binary_file(test_binary_file_path)
|
|
80
|
+
assert result is False
|
|
81
|
+
|
|
82
|
+
# Check that the error handler's report_error method was called
|
|
83
|
+
mock_error_handler.report_error.assert_called_once()
|
|
84
|
+
# You can also verify the specific arguments if needed
|
|
85
|
+
args, kwargs = mock_error_handler.report_error.call_args
|
|
86
|
+
assert "Unexpected error" in str(args[0])
|
|
87
|
+
assert "checking if file is binary" in args[1]
|
|
88
|
+
|
|
89
|
+
|
|
67
90
|
def test_read_file_with_fallback(mock_file_system):
|
|
68
91
|
classifier = FileClassifier(mock_file_system)
|
|
69
92
|
test_file_path = "test_file.txt"
|
tests/test_file_creator.py
CHANGED
|
@@ -15,12 +15,10 @@ def test_template_exists_with_valid_path():
|
|
|
15
15
|
assert result
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
def
|
|
18
|
+
def test_run_with_invalid_classifier_raises_error(capfd):
|
|
19
19
|
fc = ArtefactCreator()
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
captured = capfd.readouterr()
|
|
23
|
-
assert "Invalid classifier provided. Please provide a valid classifier." in captured.out
|
|
20
|
+
with pytest.raises(ValueError):
|
|
21
|
+
fc.run("filename", "invalid_classifier")
|
|
24
22
|
|
|
25
23
|
|
|
26
24
|
@patch("ara_cli.artefact_creator.input", return_value="n")
|
tests/test_prompt_handler.py
CHANGED
|
@@ -10,6 +10,30 @@ from ara_cli import prompt_handler
|
|
|
10
10
|
from ara_cli.ara_config import ARAconfig, LLMConfigItem, ConfigManager
|
|
11
11
|
from ara_cli.classifier import Classifier
|
|
12
12
|
|
|
13
|
+
from langfuse.api.resources.commons.errors import NotFoundError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture(autouse=True)
|
|
17
|
+
def mock_langfuse():
|
|
18
|
+
"""Mock Langfuse client to prevent network calls during tests."""
|
|
19
|
+
with patch.object(prompt_handler.LLMSingleton, 'langfuse', None):
|
|
20
|
+
mock_langfuse_instance = MagicMock()
|
|
21
|
+
|
|
22
|
+
# Mock the get_prompt method to raise NotFoundError (simulating prompt not found)
|
|
23
|
+
mock_langfuse_instance.get_prompt.side_effect = NotFoundError(
|
|
24
|
+
# status_code=404,
|
|
25
|
+
body={'message': "Prompt not found", 'error': 'LangfuseNotFoundError'}
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Mock the span context manager
|
|
29
|
+
mock_span = MagicMock()
|
|
30
|
+
mock_span.__enter__ = MagicMock(return_value=mock_span)
|
|
31
|
+
mock_span.__exit__ = MagicMock(return_value=None)
|
|
32
|
+
mock_langfuse_instance.start_as_current_span.return_value = mock_span
|
|
33
|
+
|
|
34
|
+
with patch.object(prompt_handler.LLMSingleton, 'langfuse', mock_langfuse_instance):
|
|
35
|
+
yield mock_langfuse_instance
|
|
36
|
+
|
|
13
37
|
|
|
14
38
|
@pytest.fixture
|
|
15
39
|
def mock_config():
|
|
@@ -30,7 +54,7 @@ def mock_config():
|
|
|
30
54
|
return config
|
|
31
55
|
|
|
32
56
|
|
|
33
|
-
@pytest.fixture
|
|
57
|
+
@pytest.fixture(autouse=True)
|
|
34
58
|
def mock_config_manager(mock_config):
|
|
35
59
|
"""Patches ConfigManager to ensure it always returns the mock_config."""
|
|
36
60
|
with patch.object(ConfigManager, 'get_config') as mock_get_config:
|
|
@@ -243,13 +267,17 @@ class TestCoreLogic:
|
|
|
243
267
|
)
|
|
244
268
|
|
|
245
269
|
@patch('ara_cli.prompt_handler.send_prompt')
|
|
246
|
-
def test_describe_image(self, mock_send_prompt, tmp_path):
|
|
270
|
+
def test_describe_image(self, mock_send_prompt, tmp_path, mock_langfuse):
|
|
247
271
|
fake_image_path = tmp_path / "test.jpeg"
|
|
248
272
|
fake_image_content = b"fakeimagedata"
|
|
249
273
|
fake_image_path.write_bytes(fake_image_content)
|
|
250
274
|
|
|
251
275
|
mock_send_prompt.return_value = iter([])
|
|
252
276
|
|
|
277
|
+
# Ensure the langfuse mock is properly set up for this instance
|
|
278
|
+
instance = prompt_handler.LLMSingleton.get_instance()
|
|
279
|
+
instance.langfuse = mock_langfuse
|
|
280
|
+
|
|
253
281
|
prompt_handler.describe_image(fake_image_path)
|
|
254
282
|
|
|
255
283
|
mock_send_prompt.assert_called_once()
|
|
@@ -265,7 +293,7 @@ class TestCoreLogic:
|
|
|
265
293
|
assert message_content[1]['image_url']['url'] == expected_url
|
|
266
294
|
|
|
267
295
|
@patch('ara_cli.prompt_handler.send_prompt')
|
|
268
|
-
def test_describe_image_returns_response_text(self, mock_send_prompt, tmp_path):
|
|
296
|
+
def test_describe_image_returns_response_text(self, mock_send_prompt, tmp_path, mock_langfuse):
|
|
269
297
|
fake_image_path = tmp_path / "test.gif"
|
|
270
298
|
fake_image_path.touch()
|
|
271
299
|
|
|
@@ -277,6 +305,10 @@ class TestCoreLogic:
|
|
|
277
305
|
mock_chunk3.choices[0].delta.content = None # Test empty chunk
|
|
278
306
|
mock_send_prompt.return_value = iter([mock_chunk1, mock_chunk3, mock_chunk2])
|
|
279
307
|
|
|
308
|
+
# Ensure the langfuse mock is properly set up for this instance
|
|
309
|
+
instance = prompt_handler.LLMSingleton.get_instance()
|
|
310
|
+
instance.langfuse = mock_langfuse
|
|
311
|
+
|
|
280
312
|
description = prompt_handler.describe_image(fake_image_path)
|
|
281
313
|
assert description == "This is a description."
|
|
282
314
|
|
|
@@ -310,7 +342,11 @@ class TestCoreLogic:
|
|
|
310
342
|
prompt_handler.write_prompt_result("test_classifier", "my_param", "Test content")
|
|
311
343
|
assert "Test content" in log_file.read_text()
|
|
312
344
|
|
|
313
|
-
def test_prepend_system_prompt(self):
|
|
345
|
+
def test_prepend_system_prompt(self, mock_langfuse):
|
|
346
|
+
# Ensure the langfuse mock is properly set up for this instance
|
|
347
|
+
instance = prompt_handler.LLMSingleton.get_instance()
|
|
348
|
+
instance.langfuse = mock_langfuse
|
|
349
|
+
|
|
314
350
|
messages = [{"role": "user", "content": "Hi"}]
|
|
315
351
|
result = prompt_handler.prepend_system_prompt(messages)
|
|
316
352
|
assert len(result) == 2
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pytest
|
|
3
|
+
from unittest.mock import MagicMock, patch, mock_open
|
|
4
|
+
from ara_cli.template_loader import TemplateLoader
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.fixture
|
|
8
|
+
def mock_chat_instance():
|
|
9
|
+
"""Fixture for a mocked chat instance."""
|
|
10
|
+
mock = MagicMock()
|
|
11
|
+
mock.choose_file_to_load.return_value = "chosen_file.md"
|
|
12
|
+
mock.load_file.return_value = True
|
|
13
|
+
return mock
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def template_loader_cli():
|
|
18
|
+
"""Fixture for a TemplateLoader in CLI mode."""
|
|
19
|
+
return TemplateLoader()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.fixture
|
|
23
|
+
def template_loader_chat(mock_chat_instance):
|
|
24
|
+
"""Fixture for a TemplateLoader in chat mode."""
|
|
25
|
+
return TemplateLoader(chat_instance=mock_chat_instance)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_init(mock_chat_instance):
|
|
29
|
+
"""Test the constructor."""
|
|
30
|
+
loader_cli = TemplateLoader()
|
|
31
|
+
assert loader_cli.chat_instance is None
|
|
32
|
+
|
|
33
|
+
loader_chat = TemplateLoader(chat_instance=mock_chat_instance)
|
|
34
|
+
assert loader_chat.chat_instance == mock_chat_instance
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.mark.parametrize("template_name, default_pattern, expected_method_to_call", [
|
|
38
|
+
("", "*.rules.md", "load_template_from_prompt_data"),
|
|
39
|
+
("my_rule", "*.rules.md", "load_template_from_global_or_local"),
|
|
40
|
+
])
|
|
41
|
+
def test_load_template_routing(template_loader_cli, template_name, default_pattern, expected_method_to_call):
|
|
42
|
+
"""Test that load_template calls the correct downstream method based on inputs."""
|
|
43
|
+
with patch.object(TemplateLoader, 'load_template_from_prompt_data') as mock_from_prompt, \
|
|
44
|
+
patch.object(TemplateLoader, 'load_template_from_global_or_local') as mock_from_global_local:
|
|
45
|
+
|
|
46
|
+
template_loader_cli.load_template(
|
|
47
|
+
template_name, "rules", "chat.md", default_pattern)
|
|
48
|
+
|
|
49
|
+
if expected_method_to_call == "load_template_from_prompt_data":
|
|
50
|
+
mock_from_prompt.assert_called_once()
|
|
51
|
+
mock_from_global_local.assert_not_called()
|
|
52
|
+
else:
|
|
53
|
+
mock_from_prompt.assert_not_called()
|
|
54
|
+
mock_from_global_local.assert_called_once()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_load_template_no_name_no_pattern(template_loader_cli, capsys):
|
|
58
|
+
"""Test load_template fails gracefully when no name or pattern is given."""
|
|
59
|
+
result = template_loader_cli.load_template("", "blueprint", "chat.md", None)
|
|
60
|
+
assert result is False
|
|
61
|
+
captured = capsys.readouterr()
|
|
62
|
+
assert "A template name is required for template type 'blueprint'" in captured.out
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.mark.parametrize("template_type, expected_plural", [
|
|
66
|
+
("rules", "rules"),
|
|
67
|
+
("commands", "commands"),
|
|
68
|
+
("intention", "intentions"),
|
|
69
|
+
("blueprint", "blueprints"),
|
|
70
|
+
("custom", "customs"),
|
|
71
|
+
])
|
|
72
|
+
def test_get_plural_template_type(template_loader_cli, template_type, expected_plural):
|
|
73
|
+
"""Test the pluralization of template types."""
|
|
74
|
+
assert template_loader_cli.get_plural_template_type(
|
|
75
|
+
template_type) == expected_plural
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@pytest.mark.parametrize("template_name, expected_method_to_call", [
|
|
79
|
+
("global/my_rule", "_load_global_template"),
|
|
80
|
+
("my_rule", "_load_local_template"),
|
|
81
|
+
])
|
|
82
|
+
def test_load_template_from_global_or_local_routing(template_loader_cli, template_name, expected_method_to_call):
|
|
83
|
+
"""Test routing between global and local template loading."""
|
|
84
|
+
with patch.object(TemplateLoader, '_load_global_template') as mock_global, \
|
|
85
|
+
patch.object(TemplateLoader, '_load_local_template') as mock_local:
|
|
86
|
+
|
|
87
|
+
template_loader_cli.load_template_from_global_or_local(
|
|
88
|
+
template_name, "rules", "chat.md")
|
|
89
|
+
|
|
90
|
+
if expected_method_to_call == "_load_global_template":
|
|
91
|
+
mock_global.assert_called_once()
|
|
92
|
+
mock_local.assert_not_called()
|
|
93
|
+
else:
|
|
94
|
+
mock_global.assert_not_called()
|
|
95
|
+
mock_local.assert_called_once()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@pytest.mark.parametrize("files, pattern, user_input, expected_return", [
|
|
99
|
+
(["one.md"], "*.md", "", "one.md"),
|
|
100
|
+
([], "*.md", "", None),
|
|
101
|
+
(["a.md", "b.md"], "*", "1", "a.md"),
|
|
102
|
+
(["a.md", "b.md"], "*", "2", "b.md"),
|
|
103
|
+
(["a.md", "b.md"], "*", "3", None),
|
|
104
|
+
(["a.md", "b.md"], "*", "invalid", None),
|
|
105
|
+
])
|
|
106
|
+
def test_choose_file_for_cli(template_loader_cli, files, pattern, user_input, expected_return):
|
|
107
|
+
"""Test the interactive file selection for the CLI."""
|
|
108
|
+
with patch('builtins.input', return_value=user_input):
|
|
109
|
+
result = template_loader_cli._choose_file_for_cli(files, pattern)
|
|
110
|
+
assert result == expected_return
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_load_file_to_chat_cli_context(tmp_path):
|
|
114
|
+
"""Test writing template content to a chat file in a CLI context."""
|
|
115
|
+
chat_file = tmp_path / "chat.md"
|
|
116
|
+
chat_file.write_text("# ara prompt:\n")
|
|
117
|
+
template_file = tmp_path / "template.md"
|
|
118
|
+
template_content = "This is the template content."
|
|
119
|
+
template_file.write_text(template_content)
|
|
120
|
+
|
|
121
|
+
loader = TemplateLoader()
|
|
122
|
+
result = loader._load_file_to_chat(
|
|
123
|
+
str(template_file), "rules", str(chat_file))
|
|
124
|
+
|
|
125
|
+
assert result is True
|
|
126
|
+
final_content = chat_file.read_text()
|
|
127
|
+
expected_content = f"# ara prompt:\n\n{template_content}\n"
|
|
128
|
+
assert final_content == expected_content
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_load_file_to_chat_chat_context(template_loader_chat, mock_chat_instance):
|
|
132
|
+
"""Test delegating file loading to the chat instance."""
|
|
133
|
+
result = template_loader_chat._load_file_to_chat(
|
|
134
|
+
"file.md", "rules", "chat.md")
|
|
135
|
+
|
|
136
|
+
assert result is True
|
|
137
|
+
mock_chat_instance.add_prompt_tag_if_needed.assert_called_once_with(
|
|
138
|
+
"chat.md")
|
|
139
|
+
mock_chat_instance.load_file.assert_called_once_with("file.md")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_find_project_root(tmp_path):
|
|
143
|
+
"""Test finding the project root directory."""
|
|
144
|
+
project_root = tmp_path / "project"
|
|
145
|
+
ara_dir = project_root / "ara"
|
|
146
|
+
nested_dir = project_root / "src" / "component"
|
|
147
|
+
ara_dir.mkdir(parents=True)
|
|
148
|
+
nested_dir.mkdir(parents=True)
|
|
149
|
+
|
|
150
|
+
no_ara_dir = tmp_path / "other"
|
|
151
|
+
no_ara_dir.mkdir()
|
|
152
|
+
|
|
153
|
+
loader = TemplateLoader()
|
|
154
|
+
|
|
155
|
+
# Test finding the root from a nested directory
|
|
156
|
+
assert loader._find_project_root(str(nested_dir)) == str(project_root)
|
|
157
|
+
# Test finding the root from the root itself
|
|
158
|
+
assert loader._find_project_root(str(project_root)) == str(project_root)
|
|
159
|
+
# Test not finding the root
|
|
160
|
+
assert loader._find_project_root(str(no_ara_dir)) is None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@patch('ara_cli.template_loader.TemplatePathManager')
|
|
164
|
+
@patch('ara_cli.template_loader.ConfigManager')
|
|
165
|
+
def test_get_available_templates(MockConfigManager, MockTemplatePathManager, tmp_path):
|
|
166
|
+
"""Test the discovery of global and local templates."""
|
|
167
|
+
# Setup mock paths and config
|
|
168
|
+
project_root = tmp_path / "project"
|
|
169
|
+
ara_dir = project_root / "ara"
|
|
170
|
+
araconfig_dir = project_root / ".araconfig"
|
|
171
|
+
custom_modules_dir = araconfig_dir / "custom-prompt-modules" / "rules"
|
|
172
|
+
global_modules_dir = tmp_path / "global_templates" / "prompt-modules" / "rules"
|
|
173
|
+
|
|
174
|
+
for d in [ara_dir, custom_modules_dir, global_modules_dir]:
|
|
175
|
+
d.mkdir(parents=True)
|
|
176
|
+
|
|
177
|
+
(custom_modules_dir / "local_rule.md").touch()
|
|
178
|
+
(global_modules_dir / "global_rule.md").touch()
|
|
179
|
+
|
|
180
|
+
mock_config = MagicMock()
|
|
181
|
+
mock_config.local_prompt_templates_dir = ".araconfig"
|
|
182
|
+
mock_config.custom_prompt_templates_subdir = "custom-prompt-modules"
|
|
183
|
+
MockConfigManager.get_config.return_value = mock_config
|
|
184
|
+
MockTemplatePathManager.get_template_base_path.return_value = str(
|
|
185
|
+
tmp_path / "global_templates")
|
|
186
|
+
|
|
187
|
+
loader = TemplateLoader()
|
|
188
|
+
templates = loader.get_available_templates(
|
|
189
|
+
"rules", context_path=str(project_root))
|
|
190
|
+
|
|
191
|
+
assert sorted(templates) == sorted(
|
|
192
|
+
["global/global_rule.md", "local_rule.md"])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|