ara-cli 0.1.9.77__py3-none-any.whl → 0.1.10.8__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.
Potentially problematic release.
This version of ara-cli might be problematic. Click here for more details.
- ara_cli/__init__.py +18 -2
- ara_cli/__main__.py +245 -66
- ara_cli/ara_command_action.py +128 -63
- ara_cli/ara_config.py +201 -177
- ara_cli/ara_subcommands/__init__.py +0 -0
- ara_cli/ara_subcommands/autofix.py +26 -0
- ara_cli/ara_subcommands/chat.py +27 -0
- ara_cli/ara_subcommands/classifier_directory.py +16 -0
- ara_cli/ara_subcommands/common.py +100 -0
- ara_cli/ara_subcommands/create.py +75 -0
- ara_cli/ara_subcommands/delete.py +22 -0
- ara_cli/ara_subcommands/extract.py +22 -0
- ara_cli/ara_subcommands/fetch_templates.py +14 -0
- ara_cli/ara_subcommands/list.py +65 -0
- ara_cli/ara_subcommands/list_tags.py +25 -0
- ara_cli/ara_subcommands/load.py +48 -0
- ara_cli/ara_subcommands/prompt.py +136 -0
- ara_cli/ara_subcommands/read.py +47 -0
- ara_cli/ara_subcommands/read_status.py +20 -0
- ara_cli/ara_subcommands/read_user.py +20 -0
- ara_cli/ara_subcommands/reconnect.py +27 -0
- ara_cli/ara_subcommands/rename.py +22 -0
- ara_cli/ara_subcommands/scan.py +14 -0
- ara_cli/ara_subcommands/set_status.py +22 -0
- ara_cli/ara_subcommands/set_user.py +22 -0
- ara_cli/ara_subcommands/template.py +16 -0
- ara_cli/artefact_autofix.py +214 -28
- ara_cli/artefact_creator.py +5 -8
- ara_cli/artefact_deleter.py +2 -4
- ara_cli/artefact_fuzzy_search.py +13 -6
- ara_cli/artefact_lister.py +29 -55
- ara_cli/artefact_models/artefact_data_retrieval.py +23 -0
- ara_cli/artefact_models/artefact_model.py +106 -25
- ara_cli/artefact_models/artefact_templates.py +23 -13
- ara_cli/artefact_models/epic_artefact_model.py +11 -2
- ara_cli/artefact_models/feature_artefact_model.py +56 -1
- ara_cli/artefact_models/userstory_artefact_model.py +15 -3
- ara_cli/artefact_reader.py +4 -5
- ara_cli/artefact_renamer.py +6 -2
- ara_cli/artefact_scan.py +2 -2
- ara_cli/chat.py +594 -219
- ara_cli/chat_agent/__init__.py +0 -0
- ara_cli/chat_agent/agent_communicator.py +62 -0
- ara_cli/chat_agent/agent_process_manager.py +211 -0
- ara_cli/chat_agent/agent_status_manager.py +73 -0
- ara_cli/chat_agent/agent_workspace_manager.py +76 -0
- ara_cli/commands/__init__.py +0 -0
- ara_cli/commands/command.py +7 -0
- ara_cli/commands/extract_command.py +15 -0
- ara_cli/commands/load_command.py +65 -0
- ara_cli/commands/load_image_command.py +34 -0
- ara_cli/commands/read_command.py +117 -0
- ara_cli/completers.py +144 -0
- ara_cli/directory_navigator.py +37 -4
- ara_cli/error_handler.py +134 -0
- ara_cli/file_classifier.py +3 -2
- ara_cli/file_loaders/__init__.py +0 -0
- ara_cli/file_loaders/binary_file_loader.py +33 -0
- ara_cli/file_loaders/document_file_loader.py +34 -0
- ara_cli/file_loaders/document_reader.py +245 -0
- ara_cli/file_loaders/document_readers.py +233 -0
- ara_cli/file_loaders/file_loader.py +50 -0
- ara_cli/file_loaders/file_loaders.py +123 -0
- ara_cli/file_loaders/image_processor.py +89 -0
- ara_cli/file_loaders/markdown_reader.py +75 -0
- ara_cli/file_loaders/text_file_loader.py +187 -0
- ara_cli/global_file_lister.py +51 -0
- ara_cli/prompt_extractor.py +214 -87
- ara_cli/prompt_handler.py +508 -146
- ara_cli/tag_extractor.py +54 -24
- ara_cli/template_loader.py +245 -0
- ara_cli/template_manager.py +14 -4
- ara_cli/templates/prompt-modules/commands/empty.commands.md +2 -12
- ara_cli/templates/prompt-modules/commands/extract_general.commands.md +12 -0
- ara_cli/templates/prompt-modules/commands/extract_markdown.commands.md +11 -0
- ara_cli/templates/prompt-modules/commands/extract_python.commands.md +13 -0
- ara_cli/templates/prompt-modules/commands/feature_add_or_modifiy_specified_behavior.commands.md +36 -0
- ara_cli/templates/prompt-modules/commands/feature_generate_initial_specified_bevahior.commands.md +53 -0
- ara_cli/templates/prompt-modules/commands/prompt_template_tech_stack_transformer.commands.md +95 -0
- ara_cli/templates/prompt-modules/commands/python_bug_fixing_code.commands.md +34 -0
- ara_cli/templates/prompt-modules/commands/python_generate_code.commands.md +27 -0
- ara_cli/templates/prompt-modules/commands/python_refactoring_code.commands.md +39 -0
- ara_cli/templates/prompt-modules/commands/python_step_definitions_generation_and_fixing.commands.md +40 -0
- ara_cli/templates/prompt-modules/commands/python_unittest_generation_and_fixing.commands.md +48 -0
- ara_cli/update_config_prompt.py +7 -1
- ara_cli/version.py +1 -1
- ara_cli-0.1.10.8.dist-info/METADATA +241 -0
- {ara_cli-0.1.9.77.dist-info → ara_cli-0.1.10.8.dist-info}/RECORD +104 -59
- tests/test_ara_command_action.py +66 -52
- tests/test_ara_config.py +200 -279
- tests/test_artefact_autofix.py +361 -5
- tests/test_artefact_lister.py +52 -132
- tests/test_artefact_scan.py +1 -1
- tests/test_chat.py +2009 -603
- tests/test_file_classifier.py +23 -0
- tests/test_file_creator.py +3 -5
- tests/test_global_file_lister.py +131 -0
- tests/test_prompt_handler.py +746 -0
- tests/test_tag_extractor.py +19 -13
- tests/test_template_loader.py +192 -0
- tests/test_template_manager.py +5 -4
- ara_cli/ara_command_parser.py +0 -536
- ara_cli/templates/prompt-modules/blueprints/complete_pytest_unittest.blueprint.md +0 -27
- ara_cli/templates/prompt-modules/blueprints/task_todo_list_implement_feature_BDD_way.blueprint.md +0 -30
- ara_cli/templates/prompt-modules/commands/artefact_classification.commands.md +0 -9
- ara_cli/templates/prompt-modules/commands/artefact_extension.commands.md +0 -17
- ara_cli/templates/prompt-modules/commands/artefact_formulation.commands.md +0 -14
- ara_cli/templates/prompt-modules/commands/behave_step_generation.commands.md +0 -102
- ara_cli/templates/prompt-modules/commands/code_generation_complex.commands.md +0 -20
- ara_cli/templates/prompt-modules/commands/code_generation_simple.commands.md +0 -13
- ara_cli/templates/prompt-modules/commands/error_fixing.commands.md +0 -20
- ara_cli/templates/prompt-modules/commands/feature_file_update.commands.md +0 -18
- ara_cli/templates/prompt-modules/commands/feature_formulation.commands.md +0 -43
- ara_cli/templates/prompt-modules/commands/js_code_generation_simple.commands.md +0 -13
- ara_cli/templates/prompt-modules/commands/refactoring.commands.md +0 -15
- ara_cli/templates/prompt-modules/commands/refactoring_analysis.commands.md +0 -9
- ara_cli/templates/prompt-modules/commands/reverse_engineer_feature_file.commands.md +0 -15
- ara_cli/templates/prompt-modules/commands/reverse_engineer_program_flow.commands.md +0 -19
- ara_cli-0.1.9.77.dist-info/METADATA +0 -18
- {ara_cli-0.1.9.77.dist-info → ara_cli-0.1.10.8.dist-info}/WHEEL +0 -0
- {ara_cli-0.1.9.77.dist-info → ara_cli-0.1.10.8.dist-info}/entry_points.txt +0 -0
- {ara_cli-0.1.9.77.dist-info → ara_cli-0.1.10.8.dist-info}/top_level.txt +0 -0
tests/test_ara_config.py
CHANGED
|
@@ -1,169 +1,158 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import json
|
|
3
3
|
import pytest
|
|
4
|
-
from unittest.mock import patch, mock_open, MagicMock
|
|
5
|
-
from tempfile import TemporaryDirectory
|
|
6
|
-
from pydantic import ValidationError
|
|
4
|
+
from unittest.mock import patch, mock_open, MagicMock
|
|
7
5
|
import sys
|
|
8
6
|
from io import StringIO
|
|
7
|
+
from pydantic import ValidationError
|
|
9
8
|
|
|
9
|
+
# Assuming the test file is structured to import from the production code module
|
|
10
10
|
from ara_cli.ara_config import (
|
|
11
|
-
ensure_directory_exists,
|
|
12
|
-
read_data,
|
|
11
|
+
ensure_directory_exists,
|
|
12
|
+
read_data,
|
|
13
13
|
save_data,
|
|
14
|
-
ARAconfig,
|
|
15
|
-
ConfigManager,
|
|
14
|
+
ARAconfig,
|
|
15
|
+
ConfigManager,
|
|
16
16
|
DEFAULT_CONFIG_LOCATION,
|
|
17
17
|
LLMConfigItem,
|
|
18
|
-
ExtCodeDirItem,
|
|
19
18
|
handle_unrecognized_keys,
|
|
20
|
-
fix_llm_temperatures,
|
|
21
|
-
validate_and_fix_config_data
|
|
22
19
|
)
|
|
23
20
|
|
|
24
21
|
|
|
25
22
|
@pytest.fixture
|
|
26
23
|
def default_config_data():
|
|
24
|
+
"""Provides the default configuration as a dictionary."""
|
|
27
25
|
return ARAconfig().model_dump()
|
|
28
26
|
|
|
29
27
|
|
|
30
28
|
@pytest.fixture
|
|
31
29
|
def valid_config_dict():
|
|
30
|
+
"""A valid, non-default configuration dictionary for testing."""
|
|
32
31
|
return {
|
|
33
|
-
"ext_code_dirs": [
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"custom_prompt_templates_subdir": "custom-prompt-modules",
|
|
41
|
-
"local_ara_templates_dir": "./ara/.araconfig/templates/",
|
|
42
|
-
"ara_prompt_given_list_includes": ["*.py", "*.md"],
|
|
32
|
+
"ext_code_dirs": [{"source_dir": "./app"}],
|
|
33
|
+
"glossary_dir": "./custom_glossary",
|
|
34
|
+
"doc_dir": "./custom_docs",
|
|
35
|
+
"local_prompt_templates_dir": "./custom_prompts",
|
|
36
|
+
"custom_prompt_templates_subdir": "custom_subdir",
|
|
37
|
+
"local_ara_templates_dir": "./custom_templates/",
|
|
38
|
+
"ara_prompt_given_list_includes": ["*.py", "*.md", "*.json"],
|
|
43
39
|
"llm_config": {
|
|
44
|
-
"gpt-4o": {
|
|
40
|
+
"gpt-4o-custom": {
|
|
45
41
|
"provider": "openai",
|
|
46
42
|
"model": "openai/gpt-4o",
|
|
47
|
-
"temperature": 0.
|
|
48
|
-
"max_tokens":
|
|
43
|
+
"temperature": 0.5,
|
|
44
|
+
"max_tokens": 4096
|
|
49
45
|
}
|
|
50
46
|
},
|
|
51
|
-
"default_llm": "gpt-4o"
|
|
47
|
+
"default_llm": "gpt-4o-custom"
|
|
52
48
|
}
|
|
53
49
|
|
|
54
50
|
|
|
55
51
|
@pytest.fixture
|
|
56
52
|
def corrupted_config_dict():
|
|
53
|
+
"""A config dictionary with various type errors to test validation and fixing."""
|
|
57
54
|
return {
|
|
58
|
-
"ext_code_dirs": "should_be_a_list",
|
|
59
|
-
"glossary_dir": 123,
|
|
55
|
+
"ext_code_dirs": "should_be_a_list",
|
|
56
|
+
"glossary_dir": 123,
|
|
60
57
|
"llm_config": {
|
|
61
|
-
"
|
|
62
|
-
"provider": "
|
|
63
|
-
"model": "
|
|
64
|
-
"temperature": "
|
|
65
|
-
"max_tokens": "16384" # Should be int
|
|
58
|
+
"bad-model": {
|
|
59
|
+
"provider": "test",
|
|
60
|
+
"model": "test/model",
|
|
61
|
+
"temperature": "not_a_float"
|
|
66
62
|
}
|
|
67
|
-
}
|
|
63
|
+
},
|
|
64
|
+
"default_llm": 999
|
|
68
65
|
}
|
|
69
66
|
|
|
70
67
|
|
|
71
68
|
@pytest.fixture(autouse=True)
|
|
72
69
|
def reset_config_manager():
|
|
73
|
-
"""
|
|
70
|
+
"""Ensures a clean state for each test by resetting the singleton and caches."""
|
|
74
71
|
ConfigManager.reset()
|
|
75
72
|
yield
|
|
76
73
|
ConfigManager.reset()
|
|
77
74
|
|
|
75
|
+
# --- Test Pydantic Models ---
|
|
78
76
|
|
|
79
77
|
class TestLLMConfigItem:
|
|
80
78
|
def test_valid_temperature(self):
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
model="gpt-4",
|
|
84
|
-
temperature=0.7
|
|
85
|
-
)
|
|
79
|
+
"""Tests that a valid temperature is accepted."""
|
|
80
|
+
config = LLMConfigItem(provider="test", model="test/model", temperature=0.7)
|
|
86
81
|
assert config.temperature == 0.7
|
|
87
82
|
|
|
88
|
-
def
|
|
89
|
-
|
|
90
|
-
with pytest.raises(ValidationError
|
|
91
|
-
LLMConfigItem(
|
|
92
|
-
provider="openai",
|
|
93
|
-
model="gpt-4",
|
|
94
|
-
temperature=1.5
|
|
95
|
-
)
|
|
96
|
-
assert "less than or equal to 1" in str(exc_info.value)
|
|
97
|
-
|
|
98
|
-
def test_negative_temperature_raises_validation_error(self):
|
|
99
|
-
# The Field constraint prevents negative temperatures
|
|
100
|
-
with pytest.raises(ValidationError) as exc_info:
|
|
101
|
-
LLMConfigItem(
|
|
102
|
-
provider="openai",
|
|
103
|
-
model="gpt-4",
|
|
104
|
-
temperature=-0.5
|
|
105
|
-
)
|
|
106
|
-
assert "greater than or equal to 0" in str(exc_info.value)
|
|
107
|
-
|
|
108
|
-
def test_temperature_validator_with_dict_input(self):
|
|
109
|
-
# Test the validator through dict input (simulating JSON load)
|
|
110
|
-
# This tests the fix_llm_temperatures function behavior
|
|
111
|
-
data = {
|
|
112
|
-
"provider": "openai",
|
|
113
|
-
"model": "gpt-4",
|
|
114
|
-
"temperature": 0.8
|
|
115
|
-
}
|
|
116
|
-
config = LLMConfigItem(**data)
|
|
117
|
-
assert config.temperature == 0.8
|
|
118
|
-
|
|
83
|
+
def test_invalid_temperature_too_high_raises_error(self):
|
|
84
|
+
"""Tests that temperature > 1.0 raises a ValidationError."""
|
|
85
|
+
with pytest.raises(ValidationError, match="Input should be less than or equal to 1"):
|
|
86
|
+
LLMConfigItem(provider="test", model="test/model", temperature=1.5)
|
|
119
87
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
88
|
+
def test_invalid_temperature_too_low_raises_error(self):
|
|
89
|
+
"""Tests that temperature < 0.0 raises a ValidationError."""
|
|
90
|
+
with pytest.raises(ValidationError, match="Input should be greater than or equal to 0"):
|
|
91
|
+
LLMConfigItem(provider="test", model="test/model", temperature=-0.5)
|
|
124
92
|
|
|
125
93
|
|
|
126
94
|
class TestARAconfig:
|
|
127
|
-
def
|
|
95
|
+
def test_default_values_are_correct(self):
|
|
96
|
+
"""Tests that the model initializes with correct default values."""
|
|
128
97
|
config = ARAconfig()
|
|
129
|
-
assert
|
|
130
|
-
assert config.ext_code_dirs[0].source_dir == "./src"
|
|
131
|
-
assert config.ext_code_dirs[1].source_dir == "./tests"
|
|
98
|
+
assert config.ext_code_dirs == [{"source_dir": "./src"}, {"source_dir": "./tests"}]
|
|
132
99
|
assert config.glossary_dir == "./glossary"
|
|
133
|
-
assert config.default_llm == "gpt-
|
|
134
|
-
|
|
135
|
-
def test_forbid_extra_fields(self):
|
|
136
|
-
with pytest.raises(ValidationError) as exc_info:
|
|
137
|
-
ARAconfig(unknown_field="value")
|
|
138
|
-
assert "Extra inputs are not permitted" in str(exc_info.value)
|
|
100
|
+
assert config.default_llm == "gpt-5"
|
|
101
|
+
assert "gpt-5" in config.llm_config
|
|
139
102
|
|
|
140
103
|
@patch('sys.stdout', new_callable=StringIO)
|
|
141
|
-
def
|
|
104
|
+
def test_check_critical_fields_with_empty_list_reverts_to_default(self, mock_stdout):
|
|
105
|
+
"""Tests that an empty list for a critical field is reverted to its default."""
|
|
142
106
|
config = ARAconfig(ext_code_dirs=[])
|
|
143
107
|
assert len(config.ext_code_dirs) == 2
|
|
144
|
-
assert
|
|
108
|
+
assert config.ext_code_dirs[0] == {"source_dir": "./src"}
|
|
109
|
+
assert "Warning: Value for 'ext_code_dirs' is missing or empty. Using default." in mock_stdout.getvalue()
|
|
145
110
|
|
|
146
111
|
@patch('sys.stdout', new_callable=StringIO)
|
|
147
|
-
def
|
|
112
|
+
def test_check_critical_fields_with_empty_string_reverts_to_default(self, mock_stdout):
|
|
113
|
+
"""Tests that an empty string for a critical field is reverted to its default."""
|
|
148
114
|
config = ARAconfig(glossary_dir="")
|
|
149
115
|
assert config.glossary_dir == "./glossary"
|
|
150
|
-
assert "Warning: Value for 'glossary_dir' is missing or empty." in mock_stdout.getvalue()
|
|
116
|
+
assert "Warning: Value for 'glossary_dir' is missing or empty. Using default." in mock_stdout.getvalue()
|
|
117
|
+
|
|
118
|
+
@patch('sys.stdout', new_callable=StringIO)
|
|
119
|
+
def test_validator_with_empty_llm_config(self, mock_stdout):
|
|
120
|
+
"""Tests validator when llm_config is empty, setting default and extraction to None."""
|
|
121
|
+
config = ARAconfig(llm_config={})
|
|
122
|
+
assert config.llm_config == {}
|
|
123
|
+
assert config.default_llm is None
|
|
124
|
+
assert config.extraction_llm is None
|
|
125
|
+
assert "Warning: 'llm_config' is empty" in mock_stdout.getvalue()
|
|
151
126
|
|
|
152
127
|
@patch('sys.stdout', new_callable=StringIO)
|
|
153
|
-
def
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
128
|
+
def test_validator_with_invalid_default_llm(self, mock_stdout):
|
|
129
|
+
"""Tests that an invalid default_llm is reverted to the first available model."""
|
|
130
|
+
config = ARAconfig(default_llm="non_existent_model")
|
|
131
|
+
first_llm = next(iter(config.llm_config))
|
|
132
|
+
assert config.default_llm == first_llm
|
|
133
|
+
output = mock_stdout.getvalue()
|
|
134
|
+
assert "Warning: The configured 'default_llm' ('non_existent_model') does not exist" in output
|
|
135
|
+
assert f"-> Reverting to the first available model: '{first_llm}'" in output
|
|
136
|
+
|
|
137
|
+
@patch('sys.stdout', new_callable=StringIO)
|
|
138
|
+
def test_validator_with_invalid_extraction_llm(self, mock_stdout):
|
|
139
|
+
"""Tests that an invalid extraction_llm is reverted to the default_llm."""
|
|
140
|
+
config = ARAconfig(default_llm="gpt-4o", extraction_llm="non_existent_model")
|
|
141
|
+
assert config.extraction_llm == "gpt-4o"
|
|
142
|
+
output = mock_stdout.getvalue()
|
|
143
|
+
assert "Warning: The configured 'extraction_llm' ('non_existent_model') does not exist" in output
|
|
144
|
+
assert "-> Reverting to the 'default_llm' value: 'gpt-4o'" in output
|
|
157
145
|
|
|
146
|
+
# --- Test Helper Functions ---
|
|
158
147
|
|
|
159
148
|
class TestEnsureDirectoryExists:
|
|
160
149
|
@patch('sys.stdout', new_callable=StringIO)
|
|
161
150
|
@patch("os.makedirs")
|
|
162
151
|
@patch("ara_cli.ara_config.exists", return_value=False)
|
|
163
|
-
def
|
|
164
|
-
directory
|
|
165
|
-
# Clear the cache before test
|
|
152
|
+
def test_directory_creation_when_not_exists(self, mock_exists, mock_makedirs, mock_stdout):
|
|
153
|
+
"""Tests that a directory is created if it doesn't exist."""
|
|
166
154
|
ensure_directory_exists.cache_clear()
|
|
155
|
+
directory = "/tmp/new/dir"
|
|
167
156
|
result = ensure_directory_exists(directory)
|
|
168
157
|
|
|
169
158
|
mock_exists.assert_called_once_with(directory)
|
|
@@ -173,10 +162,10 @@ class TestEnsureDirectoryExists:
|
|
|
173
162
|
|
|
174
163
|
@patch("os.makedirs")
|
|
175
164
|
@patch("ara_cli.ara_config.exists", return_value=True)
|
|
176
|
-
def
|
|
177
|
-
directory
|
|
178
|
-
# Clear the cache before test
|
|
165
|
+
def test_directory_no_creation_when_exists(self, mock_exists, mock_makedirs):
|
|
166
|
+
"""Tests that a directory is not created if it already exists."""
|
|
179
167
|
ensure_directory_exists.cache_clear()
|
|
168
|
+
directory = "/tmp/existing/dir"
|
|
180
169
|
result = ensure_directory_exists(directory)
|
|
181
170
|
|
|
182
171
|
mock_exists.assert_called_once_with(directory)
|
|
@@ -186,134 +175,37 @@ class TestEnsureDirectoryExists:
|
|
|
186
175
|
|
|
187
176
|
class TestHandleUnrecognizedKeys:
|
|
188
177
|
@patch('sys.stdout', new_callable=StringIO)
|
|
189
|
-
def
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
"unknown_key": "value"
|
|
194
|
-
}
|
|
195
|
-
known_fields = {"ext_code_dirs", "glossary_dir"}
|
|
196
|
-
|
|
197
|
-
result = handle_unrecognized_keys(data, known_fields)
|
|
198
|
-
|
|
199
|
-
assert "unknown_key" not in result
|
|
200
|
-
assert "ext_code_dirs" in result
|
|
201
|
-
assert "glossary_dir" in result
|
|
202
|
-
assert "Warning: unknown_key is not recognized as a valid configuration option." in mock_stdout.getvalue()
|
|
203
|
-
|
|
204
|
-
def test_handle_no_unrecognized_keys(self):
|
|
205
|
-
data = {
|
|
206
|
-
"ext_code_dirs": [],
|
|
207
|
-
"glossary_dir": "./glossary"
|
|
208
|
-
}
|
|
209
|
-
known_fields = {"ext_code_dirs", "glossary_dir"}
|
|
210
|
-
|
|
211
|
-
result = handle_unrecognized_keys(data, known_fields)
|
|
212
|
-
assert result == data
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
class TestFixLLMTemperatures:
|
|
216
|
-
@patch('sys.stdout', new_callable=StringIO)
|
|
217
|
-
def test_fix_invalid_temperature_too_high(self, mock_stdout):
|
|
218
|
-
data = {
|
|
219
|
-
"llm_config": {
|
|
220
|
-
"gpt-4o": {
|
|
221
|
-
"temperature": 1.5
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
result = fix_llm_temperatures(data)
|
|
227
|
-
|
|
228
|
-
assert result["llm_config"]["gpt-4o"]["temperature"] == 0.8
|
|
229
|
-
assert "Warning: Temperature for model 'gpt-4o' is outside the 0.0 to 1.0 range" in mock_stdout.getvalue()
|
|
230
|
-
|
|
231
|
-
@patch('sys.stdout', new_callable=StringIO)
|
|
232
|
-
def test_fix_invalid_temperature_too_low(self, mock_stdout):
|
|
233
|
-
data = {
|
|
234
|
-
"llm_config": {
|
|
235
|
-
"gpt-4o": {
|
|
236
|
-
"temperature": -0.5
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
result = fix_llm_temperatures(data)
|
|
242
|
-
|
|
243
|
-
assert result["llm_config"]["gpt-4o"]["temperature"] == 0.8
|
|
244
|
-
assert "Warning: Temperature for model 'gpt-4o' is outside the 0.0 to 1.0 range" in mock_stdout.getvalue()
|
|
245
|
-
|
|
246
|
-
def test_valid_temperature_not_changed(self):
|
|
247
|
-
data = {
|
|
248
|
-
"llm_config": {
|
|
249
|
-
"gpt-4o": {
|
|
250
|
-
"temperature": 0.7
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
178
|
+
def test_removes_unrecognized_keys_and_warns(self, mock_stdout):
|
|
179
|
+
"""Tests that unknown keys are removed and a warning is printed."""
|
|
180
|
+
data = {"glossary_dir": "./glossary", "unknown_key": "some_value"}
|
|
181
|
+
cleaned_data = handle_unrecognized_keys(data)
|
|
254
182
|
|
|
255
|
-
|
|
256
|
-
assert
|
|
257
|
-
|
|
258
|
-
def test_no_llm_config(self):
|
|
259
|
-
data = {"other_field": "value"}
|
|
260
|
-
result = fix_llm_temperatures(data)
|
|
261
|
-
assert result == data
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
class TestValidateAndFixConfigData:
|
|
265
|
-
@patch('sys.stdout', new_callable=StringIO)
|
|
266
|
-
@patch("builtins.open")
|
|
267
|
-
def test_valid_json_with_unrecognized_keys(self, mock_file, mock_stdout, valid_config_dict):
|
|
268
|
-
valid_config_dict["unknown_key"] = "value"
|
|
269
|
-
mock_file.return_value = mock_open(read_data=json.dumps(valid_config_dict))()
|
|
270
|
-
|
|
271
|
-
result = validate_and_fix_config_data("config.json")
|
|
272
|
-
|
|
273
|
-
assert "unknown_key" not in result
|
|
274
|
-
assert "ext_code_dirs" in result
|
|
275
|
-
assert "Warning: unknown_key is not recognized as a valid configuration option." in mock_stdout.getvalue()
|
|
183
|
+
assert "unknown_key" not in cleaned_data
|
|
184
|
+
assert "glossary_dir" in cleaned_data
|
|
185
|
+
assert "Warning: Unrecognized configuration key 'unknown_key' will be ignored." in mock_stdout.getvalue()
|
|
276
186
|
|
|
277
187
|
@patch('sys.stdout', new_callable=StringIO)
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
188
|
+
def test_no_action_for_valid_data(self, mock_stdout):
|
|
189
|
+
"""Tests that no changes are made when there are no unrecognized keys."""
|
|
190
|
+
data = {"glossary_dir": "./glossary", "doc_dir": "./docs"}
|
|
191
|
+
cleaned_data = handle_unrecognized_keys(data)
|
|
281
192
|
|
|
282
|
-
assert
|
|
283
|
-
assert
|
|
284
|
-
assert "Creating new configuration with defaults..." in mock_stdout.getvalue()
|
|
285
|
-
|
|
286
|
-
@patch('sys.stdout', new_callable=StringIO)
|
|
287
|
-
@patch("builtins.open", side_effect=IOError("File not found"))
|
|
288
|
-
def test_file_read_error(self, mock_file, mock_stdout):
|
|
289
|
-
result = validate_and_fix_config_data("config.json")
|
|
290
|
-
|
|
291
|
-
assert result == {}
|
|
292
|
-
assert "Error reading configuration file: File not found" in mock_stdout.getvalue()
|
|
293
|
-
|
|
294
|
-
@patch('sys.stdout', new_callable=StringIO)
|
|
295
|
-
@patch("builtins.open")
|
|
296
|
-
def test_fix_invalid_temperatures(self, mock_file, mock_stdout, valid_config_dict):
|
|
297
|
-
valid_config_dict["llm_config"]["gpt-4o"]["temperature"] = 2.0
|
|
298
|
-
mock_file.return_value = mock_open(read_data=json.dumps(valid_config_dict))()
|
|
299
|
-
|
|
300
|
-
result = validate_and_fix_config_data("config.json")
|
|
301
|
-
|
|
302
|
-
assert result["llm_config"]["gpt-4o"]["temperature"] == 0.8
|
|
303
|
-
assert "Warning: Temperature for model 'gpt-4o' is outside the 0.0 to 1.0 range" in mock_stdout.getvalue()
|
|
193
|
+
assert cleaned_data == data
|
|
194
|
+
assert mock_stdout.getvalue() == ""
|
|
304
195
|
|
|
196
|
+
# --- Test Core I/O and Logic ---
|
|
305
197
|
|
|
306
198
|
class TestSaveData:
|
|
307
199
|
@patch("builtins.open", new_callable=mock_open)
|
|
308
|
-
def
|
|
200
|
+
def test_save_data_writes_correct_json(self, mock_file, default_config_data):
|
|
201
|
+
"""Tests that the config is correctly serialized to a JSON file."""
|
|
309
202
|
config = ARAconfig()
|
|
310
|
-
|
|
311
203
|
save_data("config.json", config)
|
|
312
|
-
|
|
204
|
+
|
|
313
205
|
mock_file.assert_called_once_with("config.json", "w", encoding="utf-8")
|
|
314
|
-
# Check that json.dump was called with correct data
|
|
315
206
|
handle = mock_file()
|
|
316
207
|
written_data = ''.join(call.args[0] for call in handle.write.call_args_list)
|
|
208
|
+
|
|
317
209
|
assert json.loads(written_data) == default_config_data
|
|
318
210
|
|
|
319
211
|
|
|
@@ -322,26 +214,53 @@ class TestReadData:
|
|
|
322
214
|
@patch('ara_cli.ara_config.save_data')
|
|
323
215
|
@patch('ara_cli.ara_config.ensure_directory_exists')
|
|
324
216
|
@patch('ara_cli.ara_config.exists', return_value=False)
|
|
325
|
-
def
|
|
217
|
+
def test_file_not_found_creates_default_and_exits(self, mock_exists, mock_ensure_dir, mock_save, mock_stdout):
|
|
218
|
+
"""Tests that a default config is created and the program exits if no config file is found."""
|
|
326
219
|
with pytest.raises(SystemExit) as exc_info:
|
|
327
|
-
read_data.cache_clear()
|
|
220
|
+
read_data.cache_clear()
|
|
328
221
|
read_data("config.json")
|
|
329
|
-
|
|
222
|
+
|
|
330
223
|
assert exc_info.value.code == 0
|
|
224
|
+
mock_ensure_dir.assert_called_once_with(os.path.dirname("config.json"))
|
|
331
225
|
mock_save.assert_called_once()
|
|
332
|
-
|
|
226
|
+
|
|
227
|
+
output = mock_stdout.getvalue()
|
|
228
|
+
assert "Configuration file not found. Creating a default one at 'config.json'." in output
|
|
229
|
+
assert "Please review the default configuration and re-run your command." in output
|
|
333
230
|
|
|
334
231
|
@patch('ara_cli.ara_config.save_data')
|
|
335
232
|
@patch('builtins.open')
|
|
336
233
|
@patch('ara_cli.ara_config.ensure_directory_exists')
|
|
337
234
|
@patch('ara_cli.ara_config.exists', return_value=True)
|
|
338
|
-
def
|
|
339
|
-
|
|
340
|
-
read_data.
|
|
235
|
+
def test_valid_config_is_loaded_and_resaved(self, mock_exists, mock_ensure_dir, mock_open_func, mock_save, valid_config_dict):
|
|
236
|
+
"""Tests that a valid config is loaded correctly and re-saved (to clean it)."""
|
|
237
|
+
m = mock_open(read_data=json.dumps(valid_config_dict))
|
|
238
|
+
mock_open_func.return_value = m()
|
|
239
|
+
read_data.cache_clear()
|
|
240
|
+
|
|
241
|
+
result = read_data("config.json")
|
|
242
|
+
|
|
243
|
+
assert isinstance(result, ARAconfig)
|
|
244
|
+
assert result.default_llm == "gpt-4o-custom"
|
|
245
|
+
mock_save.assert_called_once()
|
|
246
|
+
|
|
247
|
+
@patch('sys.stdout', new_callable=StringIO)
|
|
248
|
+
@patch('ara_cli.ara_config.save_data')
|
|
249
|
+
@patch('builtins.open', new_callable=mock_open, read_data="this is not json")
|
|
250
|
+
@patch('ara_cli.ara_config.ensure_directory_exists')
|
|
251
|
+
@patch('ara_cli.ara_config.exists', return_value=True)
|
|
252
|
+
def test_invalid_json_creates_default_config(self, mock_exists, mock_ensure_dir, mock_open_func, mock_save, mock_stdout):
|
|
253
|
+
"""Tests that a JSON decoding error results in a new default configuration."""
|
|
254
|
+
read_data.cache_clear()
|
|
341
255
|
|
|
342
256
|
result = read_data("config.json")
|
|
343
257
|
|
|
344
258
|
assert isinstance(result, ARAconfig)
|
|
259
|
+
assert result.default_llm == "gpt-5" # Should be the default config
|
|
260
|
+
|
|
261
|
+
output = mock_stdout.getvalue()
|
|
262
|
+
assert "Error: Invalid JSON in configuration file" in output
|
|
263
|
+
assert "Creating a new configuration with defaults..." in output
|
|
345
264
|
mock_save.assert_called_once()
|
|
346
265
|
|
|
347
266
|
@patch('sys.stdout', new_callable=StringIO)
|
|
@@ -349,95 +268,97 @@ class TestReadData:
|
|
|
349
268
|
@patch('builtins.open')
|
|
350
269
|
@patch('ara_cli.ara_config.ensure_directory_exists')
|
|
351
270
|
@patch('ara_cli.ara_config.exists', return_value=True)
|
|
352
|
-
def
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
271
|
+
def test_config_with_validation_errors_is_fixed(self, mock_exists, mock_ensure_dir, mock_open_func, mock_save, mock_stdout, corrupted_config_dict):
|
|
272
|
+
"""Tests that a config with invalid fields is automatically corrected to defaults."""
|
|
273
|
+
m = mock_open(read_data=json.dumps(corrupted_config_dict))
|
|
274
|
+
mock_open_func.return_value = m()
|
|
275
|
+
read_data.cache_clear()
|
|
356
276
|
|
|
277
|
+
defaults = ARAconfig()
|
|
357
278
|
result = read_data("config.json")
|
|
358
|
-
|
|
279
|
+
|
|
359
280
|
assert isinstance(result, ARAconfig)
|
|
281
|
+
assert result.ext_code_dirs == defaults.ext_code_dirs
|
|
282
|
+
assert result.glossary_dir == defaults.glossary_dir
|
|
283
|
+
assert result.llm_config == defaults.llm_config
|
|
284
|
+
assert result.default_llm == defaults.default_llm
|
|
285
|
+
|
|
360
286
|
output = mock_stdout.getvalue()
|
|
361
|
-
|
|
362
|
-
assert
|
|
363
|
-
|
|
364
|
-
|
|
287
|
+
assert "--- Configuration Error Detected ---" in output
|
|
288
|
+
assert "-> Field 'ext_code_dirs' is invalid and will be reverted to its default value." in output
|
|
289
|
+
assert "-> Field 'glossary_dir' is invalid and will be reverted to its default value." in output
|
|
290
|
+
assert "-> Field 'llm_config' is invalid and will be reverted to its default value." in output
|
|
291
|
+
assert "Configuration has been corrected and saved" in output
|
|
292
|
+
|
|
293
|
+
mock_save.assert_called_once_with("config.json", result)
|
|
365
294
|
|
|
366
295
|
@patch('sys.stdout', new_callable=StringIO)
|
|
367
296
|
@patch('ara_cli.ara_config.save_data')
|
|
368
297
|
@patch('builtins.open')
|
|
369
298
|
@patch('ara_cli.ara_config.ensure_directory_exists')
|
|
370
299
|
@patch('ara_cli.ara_config.exists', return_value=True)
|
|
371
|
-
def
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
"glossary_dir": "./custom
|
|
375
|
-
"
|
|
376
|
-
"
|
|
300
|
+
def test_preserves_valid_fields_when_fixing_errors(self, mock_exists, mock_ensure_dir, mock_open_func, mock_save, mock_stdout):
|
|
301
|
+
"""Tests that valid, non-default values are preserved during a fix."""
|
|
302
|
+
mixed_config = {
|
|
303
|
+
"glossary_dir": "./my-custom-glossary", # Valid, non-default
|
|
304
|
+
"default_llm": 12345, # Invalid type
|
|
305
|
+
"unrecognized_key": "will_be_ignored" # Unrecognized
|
|
377
306
|
}
|
|
307
|
+
m = mock_open(read_data=json.dumps(mixed_config))
|
|
308
|
+
mock_open_func.return_value = m()
|
|
309
|
+
read_data.cache_clear()
|
|
378
310
|
|
|
379
|
-
|
|
380
|
-
read_data.cache_clear() # Clear cache
|
|
381
|
-
|
|
311
|
+
defaults = ARAconfig()
|
|
382
312
|
result = read_data("config.json")
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
assert result.ext_code_dirs == "invalid" # The invalid value is preserved
|
|
388
|
-
assert result.glossary_dir == "./custom/glossary"
|
|
389
|
-
assert result.doc_dir == "./custom/docs"
|
|
390
|
-
|
|
313
|
+
|
|
314
|
+
assert result.glossary_dir == "./my-custom-glossary"
|
|
315
|
+
assert result.default_llm == defaults.default_llm
|
|
316
|
+
|
|
391
317
|
output = mock_stdout.getvalue()
|
|
392
|
-
assert "
|
|
393
|
-
assert "
|
|
318
|
+
assert "Warning: Unrecognized configuration key 'unrecognized_key' will be ignored." in output
|
|
319
|
+
assert "-> Field 'default_llm' is invalid" in output
|
|
320
|
+
assert "-> Field 'glossary_dir' is invalid" not in output
|
|
321
|
+
|
|
322
|
+
mock_save.assert_called_once()
|
|
323
|
+
saved_config = mock_save.call_args[0][1]
|
|
324
|
+
assert saved_config.glossary_dir == "./my-custom-glossary"
|
|
325
|
+
assert saved_config.default_llm == defaults.default_llm
|
|
394
326
|
|
|
327
|
+
# --- Test Singleton Manager ---
|
|
395
328
|
|
|
396
329
|
class TestConfigManager:
|
|
397
330
|
@patch('ara_cli.ara_config.read_data')
|
|
398
|
-
def
|
|
399
|
-
|
|
400
|
-
mock_read.return_value =
|
|
331
|
+
def test_get_config_is_singleton(self, mock_read):
|
|
332
|
+
"""Tests that get_config returns the same instance on subsequent calls."""
|
|
333
|
+
mock_read.return_value = MagicMock(spec=ARAconfig)
|
|
401
334
|
|
|
402
|
-
# First call
|
|
403
335
|
config1 = ConfigManager.get_config()
|
|
404
|
-
assert config1 == mock_config
|
|
405
|
-
mock_read.assert_called_once()
|
|
406
|
-
|
|
407
|
-
# Second call should return cached instance
|
|
408
336
|
config2 = ConfigManager.get_config()
|
|
409
|
-
|
|
410
|
-
|
|
337
|
+
|
|
338
|
+
assert config1 is config2
|
|
339
|
+
mock_read.assert_called_once()
|
|
411
340
|
|
|
412
341
|
@patch('ara_cli.ara_config.read_data')
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
def test_get_config_creates_directory_if_not_exists(self, mock_exists, mock_makedirs, mock_read):
|
|
342
|
+
def test_reset_clears_instance_and_caches(self, mock_read):
|
|
343
|
+
"""Tests that the reset method clears the instance and underlying caches."""
|
|
416
344
|
mock_read.return_value = MagicMock(spec=ARAconfig)
|
|
417
|
-
|
|
418
|
-
ConfigManager.get_config("./custom/config.json")
|
|
419
|
-
mock_makedirs.assert_called_once_with("./custom")
|
|
420
345
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
mock_config = MagicMock(spec=ARAconfig)
|
|
424
|
-
mock_read.return_value = mock_config
|
|
425
|
-
|
|
426
|
-
# Get config
|
|
427
|
-
config1 = ConfigManager.get_config()
|
|
428
|
-
assert ConfigManager._config_instance is not None
|
|
346
|
+
ConfigManager.get_config()
|
|
347
|
+
mock_read.assert_called_once()
|
|
429
348
|
|
|
430
|
-
# Reset
|
|
431
349
|
ConfigManager.reset()
|
|
432
350
|
assert ConfigManager._config_instance is None
|
|
433
351
|
mock_read.cache_clear.assert_called_once()
|
|
434
352
|
|
|
353
|
+
ConfigManager.get_config()
|
|
354
|
+
assert mock_read.call_count == 2 # Called again after reset
|
|
355
|
+
|
|
435
356
|
@patch('ara_cli.ara_config.read_data')
|
|
436
|
-
def
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
357
|
+
def test_get_config_with_custom_filepath(self, mock_read):
|
|
358
|
+
"""Tests that get_config can be called with a custom file path."""
|
|
359
|
+
mock_read.return_value = MagicMock(spec=ARAconfig)
|
|
360
|
+
custom_path = "/custom/path/config.json"
|
|
361
|
+
|
|
362
|
+
ConfigManager.get_config(custom_path)
|
|
440
363
|
|
|
441
|
-
|
|
442
|
-
mock_read.assert_called_once_with(custom_path)
|
|
443
|
-
assert config == mock_config
|
|
364
|
+
mock_read.assert_called_once_with(custom_path)
|