ara-cli 0.1.9.69__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 (150) hide show
  1. ara_cli/__init__.py +18 -2
  2. ara_cli/__main__.py +248 -62
  3. ara_cli/ara_command_action.py +155 -86
  4. ara_cli/ara_config.py +226 -80
  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 +649 -68
  28. ara_cli/artefact_creator.py +8 -11
  29. ara_cli/artefact_deleter.py +2 -4
  30. ara_cli/artefact_fuzzy_search.py +22 -10
  31. ara_cli/artefact_link_updater.py +4 -4
  32. ara_cli/artefact_lister.py +29 -55
  33. ara_cli/artefact_models/artefact_data_retrieval.py +23 -0
  34. ara_cli/artefact_models/artefact_load.py +11 -3
  35. ara_cli/artefact_models/artefact_model.py +146 -39
  36. ara_cli/artefact_models/artefact_templates.py +70 -44
  37. ara_cli/artefact_models/businessgoal_artefact_model.py +23 -25
  38. ara_cli/artefact_models/epic_artefact_model.py +34 -26
  39. ara_cli/artefact_models/feature_artefact_model.py +203 -64
  40. ara_cli/artefact_models/keyfeature_artefact_model.py +21 -24
  41. ara_cli/artefact_models/serialize_helper.py +1 -1
  42. ara_cli/artefact_models/task_artefact_model.py +83 -15
  43. ara_cli/artefact_models/userstory_artefact_model.py +37 -27
  44. ara_cli/artefact_models/vision_artefact_model.py +23 -42
  45. ara_cli/artefact_reader.py +92 -91
  46. ara_cli/artefact_renamer.py +8 -4
  47. ara_cli/artefact_scan.py +66 -3
  48. ara_cli/chat.py +622 -162
  49. ara_cli/chat_agent/__init__.py +0 -0
  50. ara_cli/chat_agent/agent_communicator.py +62 -0
  51. ara_cli/chat_agent/agent_process_manager.py +211 -0
  52. ara_cli/chat_agent/agent_status_manager.py +73 -0
  53. ara_cli/chat_agent/agent_workspace_manager.py +76 -0
  54. ara_cli/commands/__init__.py +0 -0
  55. ara_cli/commands/command.py +7 -0
  56. ara_cli/commands/extract_command.py +15 -0
  57. ara_cli/commands/load_command.py +65 -0
  58. ara_cli/commands/load_image_command.py +34 -0
  59. ara_cli/commands/read_command.py +117 -0
  60. ara_cli/completers.py +144 -0
  61. ara_cli/directory_navigator.py +37 -4
  62. ara_cli/error_handler.py +134 -0
  63. ara_cli/file_classifier.py +6 -5
  64. ara_cli/file_lister.py +1 -1
  65. ara_cli/file_loaders/__init__.py +0 -0
  66. ara_cli/file_loaders/binary_file_loader.py +33 -0
  67. ara_cli/file_loaders/document_file_loader.py +34 -0
  68. ara_cli/file_loaders/document_reader.py +245 -0
  69. ara_cli/file_loaders/document_readers.py +233 -0
  70. ara_cli/file_loaders/file_loader.py +50 -0
  71. ara_cli/file_loaders/file_loaders.py +123 -0
  72. ara_cli/file_loaders/image_processor.py +89 -0
  73. ara_cli/file_loaders/markdown_reader.py +75 -0
  74. ara_cli/file_loaders/text_file_loader.py +187 -0
  75. ara_cli/global_file_lister.py +51 -0
  76. ara_cli/list_filter.py +1 -1
  77. ara_cli/output_suppressor.py +1 -1
  78. ara_cli/prompt_extractor.py +215 -88
  79. ara_cli/prompt_handler.py +521 -134
  80. ara_cli/prompt_rag.py +2 -2
  81. ara_cli/tag_extractor.py +83 -38
  82. ara_cli/template_loader.py +245 -0
  83. ara_cli/template_manager.py +18 -13
  84. ara_cli/templates/prompt-modules/commands/empty.commands.md +2 -12
  85. ara_cli/templates/prompt-modules/commands/extract_general.commands.md +12 -0
  86. ara_cli/templates/prompt-modules/commands/extract_markdown.commands.md +11 -0
  87. ara_cli/templates/prompt-modules/commands/extract_python.commands.md +13 -0
  88. ara_cli/templates/prompt-modules/commands/feature_add_or_modifiy_specified_behavior.commands.md +36 -0
  89. ara_cli/templates/prompt-modules/commands/feature_generate_initial_specified_bevahior.commands.md +53 -0
  90. ara_cli/templates/prompt-modules/commands/prompt_template_tech_stack_transformer.commands.md +95 -0
  91. ara_cli/templates/prompt-modules/commands/python_bug_fixing_code.commands.md +34 -0
  92. ara_cli/templates/prompt-modules/commands/python_generate_code.commands.md +27 -0
  93. ara_cli/templates/prompt-modules/commands/python_refactoring_code.commands.md +39 -0
  94. ara_cli/templates/prompt-modules/commands/python_step_definitions_generation_and_fixing.commands.md +40 -0
  95. ara_cli/templates/prompt-modules/commands/python_unittest_generation_and_fixing.commands.md +48 -0
  96. ara_cli/update_config_prompt.py +9 -3
  97. ara_cli/version.py +1 -1
  98. ara_cli-0.1.10.8.dist-info/METADATA +241 -0
  99. ara_cli-0.1.10.8.dist-info/RECORD +193 -0
  100. tests/test_ara_command_action.py +73 -59
  101. tests/test_ara_config.py +341 -36
  102. tests/test_artefact_autofix.py +1060 -0
  103. tests/test_artefact_link_updater.py +3 -3
  104. tests/test_artefact_lister.py +52 -132
  105. tests/test_artefact_renamer.py +2 -2
  106. tests/test_artefact_scan.py +327 -33
  107. tests/test_chat.py +2063 -498
  108. tests/test_file_classifier.py +24 -1
  109. tests/test_file_creator.py +3 -5
  110. tests/test_file_lister.py +1 -1
  111. tests/test_global_file_lister.py +131 -0
  112. tests/test_list_filter.py +2 -2
  113. tests/test_prompt_handler.py +746 -0
  114. tests/test_tag_extractor.py +19 -13
  115. tests/test_template_loader.py +192 -0
  116. tests/test_template_manager.py +5 -4
  117. tests/test_update_config_prompt.py +2 -2
  118. ara_cli/ara_command_parser.py +0 -327
  119. ara_cli/templates/prompt-modules/blueprints/complete_pytest_unittest.blueprint.md +0 -27
  120. ara_cli/templates/prompt-modules/blueprints/task_todo_list_implement_feature_BDD_way.blueprint.md +0 -30
  121. ara_cli/templates/prompt-modules/commands/artefact_classification.commands.md +0 -9
  122. ara_cli/templates/prompt-modules/commands/artefact_extension.commands.md +0 -17
  123. ara_cli/templates/prompt-modules/commands/artefact_formulation.commands.md +0 -14
  124. ara_cli/templates/prompt-modules/commands/behave_step_generation.commands.md +0 -102
  125. ara_cli/templates/prompt-modules/commands/code_generation_complex.commands.md +0 -20
  126. ara_cli/templates/prompt-modules/commands/code_generation_simple.commands.md +0 -13
  127. ara_cli/templates/prompt-modules/commands/error_fixing.commands.md +0 -20
  128. ara_cli/templates/prompt-modules/commands/feature_file_update.commands.md +0 -18
  129. ara_cli/templates/prompt-modules/commands/feature_formulation.commands.md +0 -43
  130. ara_cli/templates/prompt-modules/commands/js_code_generation_simple.commands.md +0 -13
  131. ara_cli/templates/prompt-modules/commands/refactoring.commands.md +0 -15
  132. ara_cli/templates/prompt-modules/commands/refactoring_analysis.commands.md +0 -9
  133. ara_cli/templates/prompt-modules/commands/reverse_engineer_feature_file.commands.md +0 -15
  134. ara_cli/templates/prompt-modules/commands/reverse_engineer_program_flow.commands.md +0 -19
  135. ara_cli/templates/template.businessgoal +0 -10
  136. ara_cli/templates/template.capability +0 -10
  137. ara_cli/templates/template.epic +0 -15
  138. ara_cli/templates/template.example +0 -6
  139. ara_cli/templates/template.feature +0 -26
  140. ara_cli/templates/template.issue +0 -14
  141. ara_cli/templates/template.keyfeature +0 -15
  142. ara_cli/templates/template.task +0 -6
  143. ara_cli/templates/template.userstory +0 -17
  144. ara_cli/templates/template.vision +0 -14
  145. ara_cli-0.1.9.69.dist-info/METADATA +0 -16
  146. ara_cli-0.1.9.69.dist-info/RECORD +0 -158
  147. tests/test_ara_autofix.py +0 -219
  148. {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.10.8.dist-info}/WHEEL +0 -0
  149. {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.10.8.dist-info}/entry_points.txt +0 -0
  150. {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.10.8.dist-info}/top_level.txt +0 -0
tests/test_ara_config.py CHANGED
@@ -1,59 +1,364 @@
1
- from ara_cli.ara_config import ensure_directory_exists, read_data, ARAconfig
2
- from unittest.mock import patch, mock_open
1
+ import os
3
2
  import json
4
3
  import pytest
4
+ from unittest.mock import patch, mock_open, MagicMock
5
+ import sys
6
+ from io import StringIO
7
+ from pydantic import ValidationError
8
+
9
+ # Assuming the test file is structured to import from the production code module
10
+ from ara_cli.ara_config import (
11
+ ensure_directory_exists,
12
+ read_data,
13
+ save_data,
14
+ ARAconfig,
15
+ ConfigManager,
16
+ DEFAULT_CONFIG_LOCATION,
17
+ LLMConfigItem,
18
+ handle_unrecognized_keys,
19
+ )
5
20
 
6
21
 
7
22
  @pytest.fixture
8
23
  def default_config_data():
24
+ """Provides the default configuration as a dictionary."""
9
25
  return ARAconfig().model_dump()
10
26
 
11
27
 
12
- def test_ensure_directory_exists_when_directory_does_not_exist():
13
- directory = "/some/non/existent/directory"
28
+ @pytest.fixture
29
+ def valid_config_dict():
30
+ """A valid, non-default configuration dictionary for testing."""
31
+ return {
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"],
39
+ "llm_config": {
40
+ "gpt-4o-custom": {
41
+ "provider": "openai",
42
+ "model": "openai/gpt-4o",
43
+ "temperature": 0.5,
44
+ "max_tokens": 4096
45
+ }
46
+ },
47
+ "default_llm": "gpt-4o-custom"
48
+ }
49
+
50
+
51
+ @pytest.fixture
52
+ def corrupted_config_dict():
53
+ """A config dictionary with various type errors to test validation and fixing."""
54
+ return {
55
+ "ext_code_dirs": "should_be_a_list",
56
+ "glossary_dir": 123,
57
+ "llm_config": {
58
+ "bad-model": {
59
+ "provider": "test",
60
+ "model": "test/model",
61
+ "temperature": "not_a_float"
62
+ }
63
+ },
64
+ "default_llm": 999
65
+ }
66
+
67
+
68
+ @pytest.fixture(autouse=True)
69
+ def reset_config_manager():
70
+ """Ensures a clean state for each test by resetting the singleton and caches."""
71
+ ConfigManager.reset()
72
+ yield
73
+ ConfigManager.reset()
74
+
75
+ # --- Test Pydantic Models ---
76
+
77
+ class TestLLMConfigItem:
78
+ def test_valid_temperature(self):
79
+ """Tests that a valid temperature is accepted."""
80
+ config = LLMConfigItem(provider="test", model="test/model", temperature=0.7)
81
+ assert config.temperature == 0.7
82
+
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)
87
+
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)
92
+
93
+
94
+ class TestARAconfig:
95
+ def test_default_values_are_correct(self):
96
+ """Tests that the model initializes with correct default values."""
97
+ config = ARAconfig()
98
+ assert config.ext_code_dirs == [{"source_dir": "./src"}, {"source_dir": "./tests"}]
99
+ assert config.glossary_dir == "./glossary"
100
+ assert config.default_llm == "gpt-5"
101
+ assert "gpt-5" in config.llm_config
102
+
103
+ @patch('sys.stdout', new_callable=StringIO)
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."""
106
+ config = ARAconfig(ext_code_dirs=[])
107
+ assert len(config.ext_code_dirs) == 2
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()
110
+
111
+ @patch('sys.stdout', new_callable=StringIO)
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."""
114
+ config = ARAconfig(glossary_dir="")
115
+ assert config.glossary_dir == "./glossary"
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()
126
+
127
+ @patch('sys.stdout', new_callable=StringIO)
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
145
+
146
+ # --- Test Helper Functions ---
147
+
148
+ class TestEnsureDirectoryExists:
149
+ @patch('sys.stdout', new_callable=StringIO)
150
+ @patch("os.makedirs")
151
+ @patch("ara_cli.ara_config.exists", return_value=False)
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."""
154
+ ensure_directory_exists.cache_clear()
155
+ directory = "/tmp/new/dir"
156
+ result = ensure_directory_exists(directory)
157
+
158
+ mock_exists.assert_called_once_with(directory)
159
+ mock_makedirs.assert_called_once_with(directory)
160
+ assert result == directory
161
+ assert f"New directory created at {directory}" in mock_stdout.getvalue()
162
+
163
+ @patch("os.makedirs")
164
+ @patch("ara_cli.ara_config.exists", return_value=True)
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."""
167
+ ensure_directory_exists.cache_clear()
168
+ directory = "/tmp/existing/dir"
169
+ result = ensure_directory_exists(directory)
170
+
171
+ mock_exists.assert_called_once_with(directory)
172
+ mock_makedirs.assert_not_called()
173
+ assert result == directory
174
+
175
+
176
+ class TestHandleUnrecognizedKeys:
177
+ @patch('sys.stdout', new_callable=StringIO)
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)
182
+
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()
186
+
187
+ @patch('sys.stdout', new_callable=StringIO)
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)
192
+
193
+ assert cleaned_data == data
194
+ assert mock_stdout.getvalue() == ""
195
+
196
+ # --- Test Core I/O and Logic ---
197
+
198
+ class TestSaveData:
199
+ @patch("builtins.open", new_callable=mock_open)
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."""
202
+ config = ARAconfig()
203
+ save_data("config.json", config)
204
+
205
+ mock_file.assert_called_once_with("config.json", "w", encoding="utf-8")
206
+ handle = mock_file()
207
+ written_data = ''.join(call.args[0] for call in handle.write.call_args_list)
208
+
209
+ assert json.loads(written_data) == default_config_data
210
+
211
+
212
+ class TestReadData:
213
+ @patch('sys.stdout', new_callable=StringIO)
214
+ @patch('ara_cli.ara_config.save_data')
215
+ @patch('ara_cli.ara_config.ensure_directory_exists')
216
+ @patch('ara_cli.ara_config.exists', return_value=False)
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."""
219
+ with pytest.raises(SystemExit) as exc_info:
220
+ read_data.cache_clear()
221
+ read_data("config.json")
222
+
223
+ assert exc_info.value.code == 0
224
+ mock_ensure_dir.assert_called_once_with(os.path.dirname("config.json"))
225
+ mock_save.assert_called_once()
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
230
+
231
+ @patch('ara_cli.ara_config.save_data')
232
+ @patch('builtins.open')
233
+ @patch('ara_cli.ara_config.ensure_directory_exists')
234
+ @patch('ara_cli.ara_config.exists', return_value=True)
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()
14
246
 
15
- with patch("ara_cli.ara_config.exists", return_value=False) as mock_exists:
16
- with patch("ara_cli.ara_config.os.makedirs") as mock_makedirs:
17
- result = ensure_directory_exists(directory)
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()
255
+
256
+ result = read_data("config.json")
257
+
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
264
+ mock_save.assert_called_once()
18
265
 
19
- mock_exists.assert_called_once_with(directory)
20
- mock_makedirs.assert_called_once_with(directory)
21
- assert result == directory
266
+ @patch('sys.stdout', new_callable=StringIO)
267
+ @patch('ara_cli.ara_config.save_data')
268
+ @patch('builtins.open')
269
+ @patch('ara_cli.ara_config.ensure_directory_exists')
270
+ @patch('ara_cli.ara_config.exists', return_value=True)
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()
276
+
277
+ defaults = ARAconfig()
278
+ result = read_data("config.json")
22
279
 
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
23
285
 
24
- def test_ensure_directory_exists_when_directory_exists():
25
- directory = "/some/existent/directory"
286
+ output = mock_stdout.getvalue()
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)
26
294
 
27
- with patch("ara_cli.ara_config.exists", return_value=True) as mock_exists:
28
- with patch("ara_cli.ara_config.os.makedirs") as mock_makedirs:
29
- result = ensure_directory_exists(directory)
295
+ @patch('sys.stdout', new_callable=StringIO)
296
+ @patch('ara_cli.ara_config.save_data')
297
+ @patch('builtins.open')
298
+ @patch('ara_cli.ara_config.ensure_directory_exists')
299
+ @patch('ara_cli.ara_config.exists', return_value=True)
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
306
+ }
307
+ m = mock_open(read_data=json.dumps(mixed_config))
308
+ mock_open_func.return_value = m()
309
+ read_data.cache_clear()
310
+
311
+ defaults = ARAconfig()
312
+ result = read_data("config.json")
30
313
 
31
- mock_exists.assert_called_once_with(directory)
32
- mock_makedirs.assert_not_called()
33
- assert result == directory
314
+ assert result.glossary_dir == "./my-custom-glossary"
315
+ assert result.default_llm == defaults.default_llm
34
316
 
317
+ output = mock_stdout.getvalue()
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
35
321
 
36
- @pytest.mark.parametrize("file_exists", [False, True])
37
- def test_read_data(file_exists, default_config_data):
38
- filepath = '/path/to/ara_config.json'
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
39
326
 
40
- with patch('ara_cli.ara_config.exists', return_value=file_exists):
327
+ # --- Test Singleton Manager ---
41
328
 
42
- if file_exists:
43
- with patch('ara_cli.ara_config.open', mock_open(read_data=json.dumps(default_config_data))) as mock_file:
44
- result = read_data(filepath)
45
- else:
46
- m_open = mock_open()
47
- m_open.return_value.read.return_value = json.dumps(default_config_data)
329
+ class TestConfigManager:
330
+ @patch('ara_cli.ara_config.read_data')
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)
334
+
335
+ config1 = ConfigManager.get_config()
336
+ config2 = ConfigManager.get_config()
337
+
338
+ assert config1 is config2
339
+ mock_read.assert_called_once()
48
340
 
49
- with patch('ara_cli.ara_config.open', m_open) as mock_file:
50
- with patch('ara_cli.ara_config.json.dump') as mock_json_dump, \
51
- patch('ara_cli.ara_config.exit') as mock_exit:
341
+ @patch('ara_cli.ara_config.read_data')
342
+ def test_reset_clears_instance_and_caches(self, mock_read):
343
+ """Tests that the reset method clears the instance and underlying caches."""
344
+ mock_read.return_value = MagicMock(spec=ARAconfig)
52
345
 
53
- result = read_data(filepath)
346
+ ConfigManager.get_config()
347
+ mock_read.assert_called_once()
348
+
349
+ ConfigManager.reset()
350
+ assert ConfigManager._config_instance is None
351
+ mock_read.cache_clear.assert_called_once()
54
352
 
55
- mock_json_dump.assert_called_once_with(default_config_data, mock_file(), indent=4)
56
- mock_exit.assert_called_once()
353
+ ConfigManager.get_config()
354
+ assert mock_read.call_count == 2 # Called again after reset
57
355
 
58
- # Validate the returned configuration
59
- assert result.model_dump() == default_config_data
356
+ @patch('ara_cli.ara_config.read_data')
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)
363
+
364
+ mock_read.assert_called_once_with(custom_path)