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.

Files changed (122) hide show
  1. ara_cli/__init__.py +18 -2
  2. ara_cli/__main__.py +245 -66
  3. ara_cli/ara_command_action.py +128 -63
  4. ara_cli/ara_config.py +201 -177
  5. ara_cli/ara_subcommands/__init__.py +0 -0
  6. ara_cli/ara_subcommands/autofix.py +26 -0
  7. ara_cli/ara_subcommands/chat.py +27 -0
  8. ara_cli/ara_subcommands/classifier_directory.py +16 -0
  9. ara_cli/ara_subcommands/common.py +100 -0
  10. ara_cli/ara_subcommands/create.py +75 -0
  11. ara_cli/ara_subcommands/delete.py +22 -0
  12. ara_cli/ara_subcommands/extract.py +22 -0
  13. ara_cli/ara_subcommands/fetch_templates.py +14 -0
  14. ara_cli/ara_subcommands/list.py +65 -0
  15. ara_cli/ara_subcommands/list_tags.py +25 -0
  16. ara_cli/ara_subcommands/load.py +48 -0
  17. ara_cli/ara_subcommands/prompt.py +136 -0
  18. ara_cli/ara_subcommands/read.py +47 -0
  19. ara_cli/ara_subcommands/read_status.py +20 -0
  20. ara_cli/ara_subcommands/read_user.py +20 -0
  21. ara_cli/ara_subcommands/reconnect.py +27 -0
  22. ara_cli/ara_subcommands/rename.py +22 -0
  23. ara_cli/ara_subcommands/scan.py +14 -0
  24. ara_cli/ara_subcommands/set_status.py +22 -0
  25. ara_cli/ara_subcommands/set_user.py +22 -0
  26. ara_cli/ara_subcommands/template.py +16 -0
  27. ara_cli/artefact_autofix.py +214 -28
  28. ara_cli/artefact_creator.py +5 -8
  29. ara_cli/artefact_deleter.py +2 -4
  30. ara_cli/artefact_fuzzy_search.py +13 -6
  31. ara_cli/artefact_lister.py +29 -55
  32. ara_cli/artefact_models/artefact_data_retrieval.py +23 -0
  33. ara_cli/artefact_models/artefact_model.py +106 -25
  34. ara_cli/artefact_models/artefact_templates.py +23 -13
  35. ara_cli/artefact_models/epic_artefact_model.py +11 -2
  36. ara_cli/artefact_models/feature_artefact_model.py +56 -1
  37. ara_cli/artefact_models/userstory_artefact_model.py +15 -3
  38. ara_cli/artefact_reader.py +4 -5
  39. ara_cli/artefact_renamer.py +6 -2
  40. ara_cli/artefact_scan.py +2 -2
  41. ara_cli/chat.py +594 -219
  42. ara_cli/chat_agent/__init__.py +0 -0
  43. ara_cli/chat_agent/agent_communicator.py +62 -0
  44. ara_cli/chat_agent/agent_process_manager.py +211 -0
  45. ara_cli/chat_agent/agent_status_manager.py +73 -0
  46. ara_cli/chat_agent/agent_workspace_manager.py +76 -0
  47. ara_cli/commands/__init__.py +0 -0
  48. ara_cli/commands/command.py +7 -0
  49. ara_cli/commands/extract_command.py +15 -0
  50. ara_cli/commands/load_command.py +65 -0
  51. ara_cli/commands/load_image_command.py +34 -0
  52. ara_cli/commands/read_command.py +117 -0
  53. ara_cli/completers.py +144 -0
  54. ara_cli/directory_navigator.py +37 -4
  55. ara_cli/error_handler.py +134 -0
  56. ara_cli/file_classifier.py +3 -2
  57. ara_cli/file_loaders/__init__.py +0 -0
  58. ara_cli/file_loaders/binary_file_loader.py +33 -0
  59. ara_cli/file_loaders/document_file_loader.py +34 -0
  60. ara_cli/file_loaders/document_reader.py +245 -0
  61. ara_cli/file_loaders/document_readers.py +233 -0
  62. ara_cli/file_loaders/file_loader.py +50 -0
  63. ara_cli/file_loaders/file_loaders.py +123 -0
  64. ara_cli/file_loaders/image_processor.py +89 -0
  65. ara_cli/file_loaders/markdown_reader.py +75 -0
  66. ara_cli/file_loaders/text_file_loader.py +187 -0
  67. ara_cli/global_file_lister.py +51 -0
  68. ara_cli/prompt_extractor.py +214 -87
  69. ara_cli/prompt_handler.py +508 -146
  70. ara_cli/tag_extractor.py +54 -24
  71. ara_cli/template_loader.py +245 -0
  72. ara_cli/template_manager.py +14 -4
  73. ara_cli/templates/prompt-modules/commands/empty.commands.md +2 -12
  74. ara_cli/templates/prompt-modules/commands/extract_general.commands.md +12 -0
  75. ara_cli/templates/prompt-modules/commands/extract_markdown.commands.md +11 -0
  76. ara_cli/templates/prompt-modules/commands/extract_python.commands.md +13 -0
  77. ara_cli/templates/prompt-modules/commands/feature_add_or_modifiy_specified_behavior.commands.md +36 -0
  78. ara_cli/templates/prompt-modules/commands/feature_generate_initial_specified_bevahior.commands.md +53 -0
  79. ara_cli/templates/prompt-modules/commands/prompt_template_tech_stack_transformer.commands.md +95 -0
  80. ara_cli/templates/prompt-modules/commands/python_bug_fixing_code.commands.md +34 -0
  81. ara_cli/templates/prompt-modules/commands/python_generate_code.commands.md +27 -0
  82. ara_cli/templates/prompt-modules/commands/python_refactoring_code.commands.md +39 -0
  83. ara_cli/templates/prompt-modules/commands/python_step_definitions_generation_and_fixing.commands.md +40 -0
  84. ara_cli/templates/prompt-modules/commands/python_unittest_generation_and_fixing.commands.md +48 -0
  85. ara_cli/update_config_prompt.py +7 -1
  86. ara_cli/version.py +1 -1
  87. ara_cli-0.1.10.8.dist-info/METADATA +241 -0
  88. {ara_cli-0.1.9.77.dist-info → ara_cli-0.1.10.8.dist-info}/RECORD +104 -59
  89. tests/test_ara_command_action.py +66 -52
  90. tests/test_ara_config.py +200 -279
  91. tests/test_artefact_autofix.py +361 -5
  92. tests/test_artefact_lister.py +52 -132
  93. tests/test_artefact_scan.py +1 -1
  94. tests/test_chat.py +2009 -603
  95. tests/test_file_classifier.py +23 -0
  96. tests/test_file_creator.py +3 -5
  97. tests/test_global_file_lister.py +131 -0
  98. tests/test_prompt_handler.py +746 -0
  99. tests/test_tag_extractor.py +19 -13
  100. tests/test_template_loader.py +192 -0
  101. tests/test_template_manager.py +5 -4
  102. ara_cli/ara_command_parser.py +0 -536
  103. ara_cli/templates/prompt-modules/blueprints/complete_pytest_unittest.blueprint.md +0 -27
  104. ara_cli/templates/prompt-modules/blueprints/task_todo_list_implement_feature_BDD_way.blueprint.md +0 -30
  105. ara_cli/templates/prompt-modules/commands/artefact_classification.commands.md +0 -9
  106. ara_cli/templates/prompt-modules/commands/artefact_extension.commands.md +0 -17
  107. ara_cli/templates/prompt-modules/commands/artefact_formulation.commands.md +0 -14
  108. ara_cli/templates/prompt-modules/commands/behave_step_generation.commands.md +0 -102
  109. ara_cli/templates/prompt-modules/commands/code_generation_complex.commands.md +0 -20
  110. ara_cli/templates/prompt-modules/commands/code_generation_simple.commands.md +0 -13
  111. ara_cli/templates/prompt-modules/commands/error_fixing.commands.md +0 -20
  112. ara_cli/templates/prompt-modules/commands/feature_file_update.commands.md +0 -18
  113. ara_cli/templates/prompt-modules/commands/feature_formulation.commands.md +0 -43
  114. ara_cli/templates/prompt-modules/commands/js_code_generation_simple.commands.md +0 -13
  115. ara_cli/templates/prompt-modules/commands/refactoring.commands.md +0 -15
  116. ara_cli/templates/prompt-modules/commands/refactoring_analysis.commands.md +0 -9
  117. ara_cli/templates/prompt-modules/commands/reverse_engineer_feature_file.commands.md +0 -15
  118. ara_cli/templates/prompt-modules/commands/reverse_engineer_program_flow.commands.md +0 -19
  119. ara_cli-0.1.9.77.dist-info/METADATA +0 -18
  120. {ara_cli-0.1.9.77.dist-info → ara_cli-0.1.10.8.dist-info}/WHEEL +0 -0
  121. {ara_cli-0.1.9.77.dist-info → ara_cli-0.1.10.8.dist-info}/entry_points.txt +0 -0
  122. {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, call
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
- {"source_dir": "./src"},
35
- {"source_dir": "./tests"}
36
- ],
37
- "glossary_dir": "./glossary",
38
- "doc_dir": "./docs",
39
- "local_prompt_templates_dir": "./ara/.araconfig",
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.8,
48
- "max_tokens": 16384
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", # Wrong type
59
- "glossary_dir": 123, # Should be string
55
+ "ext_code_dirs": "should_be_a_list",
56
+ "glossary_dir": 123,
60
57
  "llm_config": {
61
- "gpt-4o": {
62
- "provider": "openai",
63
- "model": "openai/gpt-4o",
64
- "temperature": "should_be_float", # Wrong type
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
- """Reset ConfigManager before each test"""
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
- config = LLMConfigItem(
82
- provider="openai",
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 test_invalid_temperature_raises_validation_error(self):
89
- # The Field constraint prevents invalid temperatures from being created
90
- with pytest.raises(ValidationError) as exc_info:
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
- class TestExtCodeDirItem:
121
- def test_create_ext_code_dir_item(self):
122
- item = ExtCodeDirItem(source_dir="./src")
123
- assert item.source_dir == "./src"
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 test_default_values(self):
95
+ def test_default_values_are_correct(self):
96
+ """Tests that the model initializes with correct default values."""
128
97
  config = ARAconfig()
129
- assert len(config.ext_code_dirs) == 2
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-4o"
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 test_check_critical_fields_empty_list(self, mock_stdout):
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 "Warning: Value for 'ext_code_dirs' is missing or empty." in mock_stdout.getvalue()
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 test_check_critical_fields_empty_string(self, mock_stdout):
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 test_check_critical_fields_whitespace_string(self, mock_stdout):
154
- config = ARAconfig(local_prompt_templates_dir=" ")
155
- assert config.local_prompt_templates_dir == "./ara/.araconfig"
156
- assert "Warning: Value for 'local_prompt_templates_dir' is missing or empty." in mock_stdout.getvalue()
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 test_directory_does_not_exist(self, mock_exists, mock_makedirs, mock_stdout):
164
- directory = "/some/non/existent/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 test_directory_exists(self, mock_exists, mock_makedirs):
177
- directory = "/some/existent/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 test_handle_unrecognized_keys(self, mock_stdout):
190
- data = {
191
- "ext_code_dirs": [],
192
- "glossary_dir": "./glossary",
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
- result = fix_llm_temperatures(data)
256
- assert result["llm_config"]["gpt-4o"]["temperature"] == 0.7
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
- @patch("builtins.open", mock_open(read_data="invalid json"))
279
- def test_invalid_json(self, mock_stdout):
280
- result = validate_and_fix_config_data("config.json")
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 result == {}
283
- assert "Error: Invalid JSON in configuration file:" in mock_stdout.getvalue()
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 test_save_data(self, mock_file, default_config_data):
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 test_file_does_not_exist_creates_default(self, mock_exists, mock_ensure_dir, mock_save, mock_stdout):
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() # Clear cache
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
- assert "ara-cli configuration file 'config.json' created with default configuration." in mock_stdout.getvalue()
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 test_file_exists_valid_config(self, mock_exists, mock_ensure_dir, mock_file, mock_save, valid_config_dict):
339
- mock_file.return_value = mock_open(read_data=json.dumps(valid_config_dict))()
340
- read_data.cache_clear() # Clear cache
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 test_file_exists_with_validation_error(self, mock_exists, mock_ensure_dir, mock_file,
353
- mock_save, mock_stdout, corrupted_config_dict):
354
- mock_file.return_value = mock_open(read_data=json.dumps(corrupted_config_dict))()
355
- read_data.cache_clear() # Clear cache
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
- # Check for any error message related to type conversion
362
- assert ("Error reading configuration file:" in output or
363
- "ValidationError:" in output)
364
- mock_save.assert_called()
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 test_preserve_valid_fields_on_error(self, mock_exists, mock_ensure_dir, mock_file,
372
- mock_save, mock_stdout):
373
- partial_valid_config = {
374
- "glossary_dir": "./custom/glossary",
375
- "ext_code_dirs": "invalid", # This will cause validation error
376
- "doc_dir": "./custom/docs"
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
- mock_file.return_value = mock_open(read_data=json.dumps(partial_valid_config))()
380
- read_data.cache_clear() # Clear cache
381
-
311
+ defaults = ARAconfig()
382
312
  result = read_data("config.json")
383
-
384
- # The implementation actually preserves the invalid value
385
- # This is the actual behavior based on the error message
386
- assert isinstance(result, ARAconfig)
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 "ValidationError:" in output
393
- assert "Correcting configuration with default values..." in output
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 test_get_config_singleton(self, mock_read):
399
- mock_config = MagicMock(spec=ARAconfig)
400
- mock_read.return_value = mock_config
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
- assert config2 == config1
410
- mock_read.assert_called_once() # Still only called once
337
+
338
+ assert config1 is config2
339
+ mock_read.assert_called_once()
411
340
 
412
341
  @patch('ara_cli.ara_config.read_data')
413
- @patch('ara_cli.ara_config.makedirs')
414
- @patch('ara_cli.ara_config.exists', return_value=False)
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
- @patch('ara_cli.ara_config.read_data')
422
- def test_reset(self, mock_read):
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 test_custom_filepath(self, mock_read):
437
- custom_path = "./custom/ara_config.json"
438
- mock_config = MagicMock(spec=ARAconfig)
439
- mock_read.return_value = mock_config
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
- config = ConfigManager.get_config(custom_path)
442
- mock_read.assert_called_once_with(custom_path)
443
- assert config == mock_config
364
+ mock_read.assert_called_once_with(custom_path)