ara-cli 0.1.9.86__py3-none-any.whl → 0.1.9.87__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/ara_config.py +53 -105
- ara_cli/prompt_handler.py +2 -5
- ara_cli/version.py +1 -1
- {ara_cli-0.1.9.86.dist-info → ara_cli-0.1.9.87.dist-info}/METADATA +1 -1
- {ara_cli-0.1.9.86.dist-info → ara_cli-0.1.9.87.dist-info}/RECORD +10 -9
- tests/test_ara_config.py +173 -280
- tests/test_prompt_handler.py +303 -0
- {ara_cli-0.1.9.86.dist-info → ara_cli-0.1.9.87.dist-info}/WHEEL +0 -0
- {ara_cli-0.1.9.86.dist-info → ara_cli-0.1.9.87.dist-info}/entry_points.txt +0 -0
- {ara_cli-0.1.9.86.dist-info → ara_cli-0.1.9.87.dist-info}/top_level.txt +0 -0
ara_cli/ara_config.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from typing import List, Dict, Optional, Any
|
|
2
|
-
from pydantic import BaseModel, ValidationError, Field,
|
|
2
|
+
from pydantic import BaseModel, ValidationError, Field, model_validator
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
5
|
from os.path import exists, dirname
|
|
@@ -14,23 +14,11 @@ class LLMConfigItem(BaseModel):
|
|
|
14
14
|
model: str
|
|
15
15
|
temperature: float = Field(ge=0.0, le=1.0)
|
|
16
16
|
max_tokens: Optional[int] = None
|
|
17
|
-
|
|
18
|
-
@field_validator('temperature')
|
|
19
|
-
@classmethod
|
|
20
|
-
def validate_temperature(cls, v: float, info) -> float:
|
|
21
|
-
if not 0.0 <= v <= 1.0:
|
|
22
|
-
print(f"Warning: Temperature is outside the 0.0 to 1.0 range")
|
|
23
|
-
# Return a valid default
|
|
24
|
-
return 0.8
|
|
25
|
-
return v
|
|
26
|
-
|
|
27
|
-
class ExtCodeDirItem(BaseModel):
|
|
28
|
-
source_dir: str
|
|
29
17
|
|
|
30
18
|
class ARAconfig(BaseModel):
|
|
31
|
-
ext_code_dirs: List[
|
|
32
|
-
|
|
33
|
-
|
|
19
|
+
ext_code_dirs: List[Dict[str, str]] = Field(default_factory=lambda: [
|
|
20
|
+
{"source_dir": "./src"},
|
|
21
|
+
{"source_dir": "./tests"}
|
|
34
22
|
])
|
|
35
23
|
glossary_dir: str = "./glossary"
|
|
36
24
|
doc_dir: str = "./docs"
|
|
@@ -98,27 +86,21 @@ class ARAconfig(BaseModel):
|
|
|
98
86
|
)
|
|
99
87
|
})
|
|
100
88
|
default_llm: Optional[str] = "gpt-4o"
|
|
101
|
-
|
|
102
|
-
model_config = {
|
|
103
|
-
"extra": "forbid" # This will help identify unrecognized keys
|
|
104
|
-
}
|
|
105
89
|
|
|
106
90
|
@model_validator(mode='after')
|
|
107
91
|
def check_critical_fields(self) -> 'ARAconfig':
|
|
108
|
-
"""Check for empty critical fields and use defaults if needed"""
|
|
92
|
+
"""Check for empty critical fields and use defaults if needed."""
|
|
109
93
|
critical_fields = {
|
|
110
|
-
'ext_code_dirs': [
|
|
94
|
+
'ext_code_dirs': [{"source_dir": "./src"}, {"source_dir": "./tests"}],
|
|
111
95
|
'local_ara_templates_dir': "./ara/.araconfig/templates/",
|
|
112
96
|
'local_prompt_templates_dir': "./ara/.araconfig",
|
|
113
97
|
'glossary_dir': "./glossary"
|
|
114
98
|
}
|
|
115
|
-
|
|
99
|
+
|
|
116
100
|
for field, default_value in critical_fields.items():
|
|
117
101
|
current_value = getattr(self, field)
|
|
118
|
-
if
|
|
119
|
-
(
|
|
120
|
-
(isinstance(current_value, str) and current_value.strip() == "")):
|
|
121
|
-
print(f"Warning: Value for '{field}' is missing or empty.")
|
|
102
|
+
if not current_value:
|
|
103
|
+
print(f"Warning: Value for '{field}' is missing or empty. Using default.")
|
|
122
104
|
setattr(self, field, default_value)
|
|
123
105
|
|
|
124
106
|
return self
|
|
@@ -126,109 +108,80 @@ class ARAconfig(BaseModel):
|
|
|
126
108
|
# Function to ensure the necessary directories exist
|
|
127
109
|
@lru_cache(maxsize=None)
|
|
128
110
|
def ensure_directory_exists(directory: str):
|
|
111
|
+
"""Creates a directory if it doesn't exist."""
|
|
129
112
|
if not exists(directory):
|
|
130
113
|
os.makedirs(directory)
|
|
131
114
|
print(f"New directory created at {directory}")
|
|
132
115
|
return directory
|
|
133
116
|
|
|
134
|
-
def handle_unrecognized_keys(data: dict
|
|
135
|
-
"""
|
|
117
|
+
def handle_unrecognized_keys(data: dict) -> dict:
|
|
118
|
+
"""Removes unrecognized keys from the data and warns the user."""
|
|
119
|
+
known_fields = set(ARAconfig.model_fields.keys())
|
|
136
120
|
cleaned_data = {}
|
|
137
121
|
for key, value in data.items():
|
|
138
122
|
if key not in known_fields:
|
|
139
|
-
print(f"Warning: {key}
|
|
123
|
+
print(f"Warning: Unrecognized configuration key '{key}' will be ignored.")
|
|
140
124
|
else:
|
|
141
125
|
cleaned_data[key] = value
|
|
142
126
|
return cleaned_data
|
|
143
127
|
|
|
144
|
-
def fix_llm_temperatures(data: dict) -> dict:
|
|
145
|
-
"""Fix invalid temperatures in LLM configurations"""
|
|
146
|
-
if 'llm_config' in data:
|
|
147
|
-
for model_key, model_config in data['llm_config'].items():
|
|
148
|
-
if isinstance(model_config, dict) and 'temperature' in model_config:
|
|
149
|
-
temp = model_config['temperature']
|
|
150
|
-
if not 0.0 <= temp <= 1.0:
|
|
151
|
-
print(f"Warning: Temperature for model '{model_key}' is outside the 0.0 to 1.0 range")
|
|
152
|
-
model_config['temperature'] = 0.8
|
|
153
|
-
return data
|
|
154
|
-
|
|
155
|
-
def validate_and_fix_config_data(filepath: str) -> dict:
|
|
156
|
-
"""Load, validate, and fix configuration data"""
|
|
157
|
-
try:
|
|
158
|
-
with open(filepath, "r", encoding="utf-8") as file:
|
|
159
|
-
data = json.load(file)
|
|
160
|
-
|
|
161
|
-
# Get known fields from the ARAconfig model
|
|
162
|
-
known_fields = set(ARAconfig.model_fields.keys())
|
|
163
|
-
|
|
164
|
-
# Handle unrecognized keys
|
|
165
|
-
data = handle_unrecognized_keys(data, known_fields)
|
|
166
|
-
|
|
167
|
-
# Fix LLM temperatures before validation
|
|
168
|
-
data = fix_llm_temperatures(data)
|
|
169
|
-
|
|
170
|
-
return data
|
|
171
|
-
except json.JSONDecodeError as e:
|
|
172
|
-
print(f"Error: Invalid JSON in configuration file: {e}")
|
|
173
|
-
print("Creating new configuration with defaults...")
|
|
174
|
-
return {}
|
|
175
|
-
except Exception as e:
|
|
176
|
-
print(f"Error reading configuration file: {e}")
|
|
177
|
-
return {}
|
|
178
|
-
|
|
179
128
|
# Function to read the JSON file and return an ARAconfig model
|
|
180
129
|
@lru_cache(maxsize=1)
|
|
181
130
|
def read_data(filepath: str) -> ARAconfig:
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
131
|
+
"""
|
|
132
|
+
Reads, validates, and repairs the configuration file.
|
|
133
|
+
If the file doesn't exist, it creates a default one.
|
|
134
|
+
If the file is invalid, it corrects only the broken parts.
|
|
135
|
+
"""
|
|
136
|
+
ensure_directory_exists(dirname(filepath))
|
|
185
137
|
|
|
186
138
|
if not exists(filepath):
|
|
187
|
-
|
|
139
|
+
print(f"Configuration file not found. Creating a default one at '{filepath}'.")
|
|
188
140
|
default_config = ARAconfig()
|
|
189
141
|
save_data(filepath, default_config)
|
|
190
|
-
print(
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
142
|
+
print("Please review the default configuration and re-run your command.")
|
|
143
|
+
sys.exit(0)
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
with open(filepath, "r", encoding="utf-8") as file:
|
|
147
|
+
data = json.load(file)
|
|
148
|
+
except json.JSONDecodeError as e:
|
|
149
|
+
print(f"Error: Invalid JSON in configuration file: {e}")
|
|
150
|
+
print("Creating a new configuration with defaults...")
|
|
151
|
+
default_config = ARAconfig()
|
|
152
|
+
save_data(filepath, default_config)
|
|
153
|
+
return default_config
|
|
154
|
+
|
|
155
|
+
data = handle_unrecognized_keys(data)
|
|
195
156
|
|
|
196
|
-
# Validate and load the existing configuration
|
|
197
|
-
data = validate_and_fix_config_data(filepath)
|
|
198
|
-
|
|
199
157
|
try:
|
|
200
|
-
# Try to create the config with the loaded data
|
|
201
158
|
config = ARAconfig(**data)
|
|
202
|
-
|
|
203
|
-
# Save the potentially fixed configuration back
|
|
204
159
|
save_data(filepath, config)
|
|
205
|
-
|
|
206
160
|
return config
|
|
207
161
|
except ValidationError as e:
|
|
208
|
-
print(
|
|
209
|
-
print("
|
|
162
|
+
print("--- Configuration Error Detected ---")
|
|
163
|
+
print("Some settings in your configuration file are invalid. Attempting to fix them.")
|
|
210
164
|
|
|
211
|
-
|
|
212
|
-
|
|
165
|
+
corrected_data = data.copy()
|
|
166
|
+
defaults = ARAconfig().model_dump()
|
|
213
167
|
|
|
214
|
-
|
|
215
|
-
for field_name, field_value in data.items():
|
|
216
|
-
if field_name in ARAconfig.model_fields:
|
|
217
|
-
try:
|
|
218
|
-
# Attempt to set the field value
|
|
219
|
-
setattr(default_config, field_name, field_value)
|
|
220
|
-
except:
|
|
221
|
-
# If it fails, keep the default
|
|
222
|
-
pass
|
|
168
|
+
error_fields = {err['loc'][0] for err in e.errors() if err['loc']}
|
|
223
169
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
170
|
+
for field_name in error_fields:
|
|
171
|
+
print(f"-> Field '{field_name}' is invalid and will be reverted to its default value.")
|
|
172
|
+
corrected_data[field_name] = defaults.get(field_name)
|
|
173
|
+
|
|
174
|
+
print("--- End of Error Report ---")
|
|
175
|
+
|
|
176
|
+
final_config = ARAconfig(**corrected_data)
|
|
177
|
+
save_data(filepath, final_config)
|
|
178
|
+
print(f"Configuration has been corrected and saved to '{filepath}'.")
|
|
227
179
|
|
|
228
|
-
return
|
|
180
|
+
return final_config
|
|
229
181
|
|
|
230
182
|
# Function to save the modified configuration back to the JSON file
|
|
231
183
|
def save_data(filepath: str, config: ARAconfig):
|
|
184
|
+
"""Saves the Pydantic config model to a JSON file."""
|
|
232
185
|
with open(filepath, "w", encoding="utf-8") as file:
|
|
233
186
|
json.dump(config.model_dump(), file, indent=4)
|
|
234
187
|
|
|
@@ -237,18 +190,13 @@ class ConfigManager:
|
|
|
237
190
|
_config_instance = None
|
|
238
191
|
|
|
239
192
|
@classmethod
|
|
240
|
-
def get_config(cls, filepath=DEFAULT_CONFIG_LOCATION):
|
|
193
|
+
def get_config(cls, filepath=DEFAULT_CONFIG_LOCATION) -> ARAconfig:
|
|
241
194
|
if cls._config_instance is None:
|
|
242
|
-
config_dir = dirname(filepath)
|
|
243
|
-
|
|
244
|
-
if not exists(config_dir):
|
|
245
|
-
makedirs(config_dir)
|
|
246
|
-
|
|
247
195
|
cls._config_instance = read_data(filepath)
|
|
248
196
|
return cls._config_instance
|
|
249
197
|
|
|
250
198
|
@classmethod
|
|
251
199
|
def reset(cls):
|
|
252
|
-
"""Reset the configuration instance (useful for testing)"""
|
|
200
|
+
"""Reset the configuration instance (useful for testing)."""
|
|
253
201
|
cls._config_instance = None
|
|
254
202
|
read_data.cache_clear()
|
ara_cli/prompt_handler.py
CHANGED
|
@@ -15,8 +15,6 @@ import glob
|
|
|
15
15
|
import logging
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
18
|
class LLMSingleton:
|
|
21
19
|
_instance = None
|
|
22
20
|
_model = None
|
|
@@ -27,8 +25,7 @@ class LLMSingleton:
|
|
|
27
25
|
|
|
28
26
|
if not selected_config:
|
|
29
27
|
raise ValueError(f"No configuration found for the model: {model_id}")
|
|
30
|
-
|
|
31
|
-
selected_config = LLMConfigItem(**selected_config)
|
|
28
|
+
|
|
32
29
|
LLMSingleton._model = model_id
|
|
33
30
|
|
|
34
31
|
# Typesafe for None values inside the config.
|
|
@@ -495,7 +492,7 @@ def generate_config_prompt_template_file(prompt_data_path, config_prompt_templat
|
|
|
495
492
|
def generate_config_prompt_givens_file(prompt_data_path, config_prompt_givens_name, artefact_to_mark=None):
|
|
496
493
|
config_prompt_givens_path = os.path.join(prompt_data_path, config_prompt_givens_name)
|
|
497
494
|
config = ConfigManager.get_config()
|
|
498
|
-
dir_list = ["ara"] + [ext
|
|
495
|
+
dir_list = ["ara"] + [ext['source_dir'] for ext in config.ext_code_dirs] + [config.doc_dir] + [config.glossary_dir]
|
|
499
496
|
|
|
500
497
|
print(f"used {dir_list} for prompt givens file listing")
|
|
501
498
|
generate_markdown_listing(dir_list, config.ara_prompt_given_list_includes, config_prompt_givens_path)
|
ara_cli/version.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# version.py
|
|
2
|
-
__version__ = "0.1.9.
|
|
2
|
+
__version__ = "0.1.9.87" # fith parameter like .0 for local install test purposes only. official numbers should be 4 digit numbers
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ara_cli
|
|
3
|
-
Version: 0.1.9.
|
|
3
|
+
Version: 0.1.9.87
|
|
4
4
|
Summary: Powerful, open source command-line tool for managing, structuring and automating software development artifacts in line with Business-Driven Development (BDD) and AI-assisted processes
|
|
5
5
|
Description-Content-Type: text/markdown
|
|
6
6
|
Requires-Dist: litellm
|
|
@@ -2,7 +2,7 @@ ara_cli/__init__.py,sha256=0zl7IegxTid26EBGLav_fXZ4CCIV3H5TfAoFQiOHjvg,148
|
|
|
2
2
|
ara_cli/__main__.py,sha256=J5DCDLRZ6UcpYwM1-NkjaLo4PTetcSj2dB4HrrftkUw,2064
|
|
3
3
|
ara_cli/ara_command_action.py,sha256=_LHE2V5hbJxN7ccYiptuPktRfbTnXmQEt_D_FxDBlBY,22456
|
|
4
4
|
ara_cli/ara_command_parser.py,sha256=I-e9W-QwTIMKMzlHycSlCWCyBFQfiFYvGre1XsDbrFI,20573
|
|
5
|
-
ara_cli/ara_config.py,sha256=
|
|
5
|
+
ara_cli/ara_config.py,sha256=KVITofnYlIVyhf50qwUO5fu8vlxjDwRjPyKzqEhEC_M,6982
|
|
6
6
|
ara_cli/artefact_autofix.py,sha256=WVTiIR-jo4YKmmz4eS3qTFvl45W1YKwAk1XSuz9QX10,20015
|
|
7
7
|
ara_cli/artefact_creator.py,sha256=0Ory6cB-Ahkw-BDNb8QHnTbp_OHGABdkb9bhwcEdcIc,6063
|
|
8
8
|
ara_cli/artefact_deleter.py,sha256=Co4wwCH3yW8H9NrOq7_2p5571EeHr0TsfE-H8KqoOfY,1900
|
|
@@ -25,13 +25,13 @@ ara_cli/list_filter.py,sha256=qKGwwQsrWe7L5FbdxEbBYD1bbbi8c-RMypjXqXvLbgs,5291
|
|
|
25
25
|
ara_cli/output_suppressor.py,sha256=nwiHaQLwabOjMoJOeUESBnZszGMxrQZfJ3N2OvahX7Y,389
|
|
26
26
|
ara_cli/prompt_chat.py,sha256=kd_OINDQFit6jN04bb7mzgY259JBbRaTaNp9F-webkc,1346
|
|
27
27
|
ara_cli/prompt_extractor.py,sha256=6xLGd4ZJHDKkamEUQcdRbKM3ilBtxBjp0X2o8wrvHb0,7732
|
|
28
|
-
ara_cli/prompt_handler.py,sha256=
|
|
28
|
+
ara_cli/prompt_handler.py,sha256=5FoVCNmmzrS4hjHL4qKteQt2A5MIycoZStkJrVL5l_4,20136
|
|
29
29
|
ara_cli/prompt_rag.py,sha256=ydlhe4CUqz0jdzlY7jBbpKaf_5fjMrAZKnriKea3ZAg,7485
|
|
30
30
|
ara_cli/run_file_lister.py,sha256=XbrrDTJXp1LFGx9Lv91SNsEHZPP-PyEMBF_P4btjbDA,2360
|
|
31
31
|
ara_cli/tag_extractor.py,sha256=TGdaQOVnjy25R0zDsAifB67C5oom0Fwo24s0_fr5A_I,3151
|
|
32
32
|
ara_cli/template_manager.py,sha256=YwrN6AYPpl6ZrW8BVQpVXx8yTRf-oNpJUIKeg4NAggs,6606
|
|
33
33
|
ara_cli/update_config_prompt.py,sha256=Oy9vNTw6UhDohyTEfSKkqE5ifEMPlmWNYkKHgUrK_pY,4607
|
|
34
|
-
ara_cli/version.py,sha256=
|
|
34
|
+
ara_cli/version.py,sha256=27waJFP_fluwJJNMwbe3_NlHJaeI6oPvAK2CQIhI63w,146
|
|
35
35
|
ara_cli/artefact_models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
36
36
|
ara_cli/artefact_models/artefact_load.py,sha256=IXzWxP-Q_j_oDGMno0m-OuXCQ7Vd5c_NctshGr4ROBw,621
|
|
37
37
|
ara_cli/artefact_models/artefact_mapping.py,sha256=8aD0spBjkJ8toMAmFawc6UTUxB6-tEEViZXv2I-r88Q,1874
|
|
@@ -134,7 +134,7 @@ ara_cli/templates/specification_breakdown_files/template.technology.exploration.
|
|
|
134
134
|
ara_cli/templates/specification_breakdown_files/template.technology.md,sha256=bySiksz-8xtq0Nnj4svqe2MgUftWrVkbK9AcrDUE3KY,952
|
|
135
135
|
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
136
136
|
tests/test_ara_command_action.py,sha256=JTLqXM9BSMlU33OQgrk_sZnoowFJZKZAx8q-st-wa34,25821
|
|
137
|
-
tests/test_ara_config.py,sha256=
|
|
137
|
+
tests/test_ara_config.py,sha256=gYqBB4Z7lB0PTSZrJRL7ekC0t9HZ_Rq3JF9EnOFmN5U,14280
|
|
138
138
|
tests/test_artefact_autofix.py,sha256=pApZ-N0dW8Ujt-cNLbgvd4bhiIIK8oXb-saLf6QlA-8,25022
|
|
139
139
|
tests/test_artefact_fuzzy_search.py,sha256=5Sh3_l9QK8-WHn6JpGPU1b6h4QEnl2JoMq1Tdp2cj1U,1261
|
|
140
140
|
tests/test_artefact_link_updater.py,sha256=biqbEp2jCOz8giv72hu2P2hDfeJfJ9OrVGdAv5d9cK4,2191
|
|
@@ -149,11 +149,12 @@ tests/test_file_classifier.py,sha256=kLWPiePu3F5mkVuI_lK_2QlLh2kXD_Mt2K8KZZ1fAnA
|
|
|
149
149
|
tests/test_file_creator.py,sha256=D3G7MbgE0m8JmZihxnTryxLco6iZdbV--2CGc0L20FM,2109
|
|
150
150
|
tests/test_file_lister.py,sha256=Q9HwhKKx540EPzTmfzOCnvtAgON0aMmpJE2eOe1J3EA,4324
|
|
151
151
|
tests/test_list_filter.py,sha256=fJA3d_SdaOAUkE7jn68MOVS0THXGghy1fye_64Zvo1U,7964
|
|
152
|
+
tests/test_prompt_handler.py,sha256=7V_AwXd2co1krnx5RKZRK-hqXS50nq77mX-Yx_QO0w0,13084
|
|
152
153
|
tests/test_tag_extractor.py,sha256=nSiAYlTKZ7TLAOtcJpwK5zTWHhFYU0tI5xKnivLc1dU,2712
|
|
153
154
|
tests/test_template_manager.py,sha256=q-LMHRG4rHkD6ON6YW4cpZxUx9hul6Or8wVVRC2kb-8,4099
|
|
154
155
|
tests/test_update_config_prompt.py,sha256=xsqj1WTn4BsG5Q2t-sNPfu7EoMURFcS-hfb5VSXUnJc,6765
|
|
155
|
-
ara_cli-0.1.9.
|
|
156
|
-
ara_cli-0.1.9.
|
|
157
|
-
ara_cli-0.1.9.
|
|
158
|
-
ara_cli-0.1.9.
|
|
159
|
-
ara_cli-0.1.9.
|
|
156
|
+
ara_cli-0.1.9.87.dist-info/METADATA,sha256=WKN4uhNXDHMDwwlxkCW5KbBV_2cbeixRkQYsLZNBdLY,6739
|
|
157
|
+
ara_cli-0.1.9.87.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
158
|
+
ara_cli-0.1.9.87.dist-info/entry_points.txt,sha256=v4h7MzysTgSIDYfEo3oj4Kz_8lzsRa3hq-KJHEcLVX8,45
|
|
159
|
+
ara_cli-0.1.9.87.dist-info/top_level.txt,sha256=WM4cLHT5DYUaWzLtRj-gu3yVNFpGQ6lLRI3FMmC-38I,14
|
|
160
|
+
ara_cli-0.1.9.87.dist-info/RECORD,,
|
tests/test_ara_config.py
CHANGED
|
@@ -1,169 +1,130 @@
|
|
|
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
100
|
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)
|
|
101
|
+
assert "gpt-4o" 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()
|
|
151
|
-
|
|
152
|
-
@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()
|
|
116
|
+
assert "Warning: Value for 'glossary_dir' is missing or empty. Using default." in mock_stdout.getvalue()
|
|
157
117
|
|
|
118
|
+
# --- Test Helper Functions ---
|
|
158
119
|
|
|
159
120
|
class TestEnsureDirectoryExists:
|
|
160
121
|
@patch('sys.stdout', new_callable=StringIO)
|
|
161
122
|
@patch("os.makedirs")
|
|
162
123
|
@patch("ara_cli.ara_config.exists", return_value=False)
|
|
163
|
-
def
|
|
164
|
-
directory
|
|
165
|
-
# Clear the cache before test
|
|
124
|
+
def test_directory_creation_when_not_exists(self, mock_exists, mock_makedirs, mock_stdout):
|
|
125
|
+
"""Tests that a directory is created if it doesn't exist."""
|
|
166
126
|
ensure_directory_exists.cache_clear()
|
|
127
|
+
directory = "/tmp/new/dir"
|
|
167
128
|
result = ensure_directory_exists(directory)
|
|
168
129
|
|
|
169
130
|
mock_exists.assert_called_once_with(directory)
|
|
@@ -173,10 +134,10 @@ class TestEnsureDirectoryExists:
|
|
|
173
134
|
|
|
174
135
|
@patch("os.makedirs")
|
|
175
136
|
@patch("ara_cli.ara_config.exists", return_value=True)
|
|
176
|
-
def
|
|
177
|
-
directory
|
|
178
|
-
# Clear the cache before test
|
|
137
|
+
def test_directory_no_creation_when_exists(self, mock_exists, mock_makedirs):
|
|
138
|
+
"""Tests that a directory is not created if it already exists."""
|
|
179
139
|
ensure_directory_exists.cache_clear()
|
|
140
|
+
directory = "/tmp/existing/dir"
|
|
180
141
|
result = ensure_directory_exists(directory)
|
|
181
142
|
|
|
182
143
|
mock_exists.assert_called_once_with(directory)
|
|
@@ -186,134 +147,37 @@ class TestEnsureDirectoryExists:
|
|
|
186
147
|
|
|
187
148
|
class TestHandleUnrecognizedKeys:
|
|
188
149
|
@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)
|
|
150
|
+
def test_removes_unrecognized_keys_and_warns(self, mock_stdout):
|
|
151
|
+
"""Tests that unknown keys are removed and a warning is printed."""
|
|
152
|
+
data = {"glossary_dir": "./glossary", "unknown_key": "some_value"}
|
|
153
|
+
cleaned_data = handle_unrecognized_keys(data)
|
|
242
154
|
|
|
243
|
-
assert
|
|
244
|
-
assert "
|
|
245
|
-
|
|
246
|
-
def test_valid_temperature_not_changed(self):
|
|
247
|
-
data = {
|
|
248
|
-
"llm_config": {
|
|
249
|
-
"gpt-4o": {
|
|
250
|
-
"temperature": 0.7
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
result = fix_llm_temperatures(data)
|
|
256
|
-
assert result["llm_config"]["gpt-4o"]["temperature"] == 0.7
|
|
155
|
+
assert "unknown_key" not in cleaned_data
|
|
156
|
+
assert "glossary_dir" in cleaned_data
|
|
157
|
+
assert "Warning: Unrecognized configuration key 'unknown_key' will be ignored." in mock_stdout.getvalue()
|
|
257
158
|
|
|
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
159
|
@patch('sys.stdout', new_callable=StringIO)
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
result = validate_and_fix_config_data("config.json")
|
|
160
|
+
def test_no_action_for_valid_data(self, mock_stdout):
|
|
161
|
+
"""Tests that no changes are made when there are no unrecognized keys."""
|
|
162
|
+
data = {"glossary_dir": "./glossary", "doc_dir": "./docs"}
|
|
163
|
+
cleaned_data = handle_unrecognized_keys(data)
|
|
272
164
|
|
|
273
|
-
assert
|
|
274
|
-
assert "
|
|
275
|
-
assert "Warning: unknown_key is not recognized as a valid configuration option." in mock_stdout.getvalue()
|
|
276
|
-
|
|
277
|
-
@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")
|
|
281
|
-
|
|
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()
|
|
165
|
+
assert cleaned_data == data
|
|
166
|
+
assert mock_stdout.getvalue() == ""
|
|
304
167
|
|
|
168
|
+
# --- Test Core I/O and Logic ---
|
|
305
169
|
|
|
306
170
|
class TestSaveData:
|
|
307
171
|
@patch("builtins.open", new_callable=mock_open)
|
|
308
|
-
def
|
|
172
|
+
def test_save_data_writes_correct_json(self, mock_file, default_config_data):
|
|
173
|
+
"""Tests that the config is correctly serialized to a JSON file."""
|
|
309
174
|
config = ARAconfig()
|
|
310
|
-
|
|
311
175
|
save_data("config.json", config)
|
|
312
|
-
|
|
176
|
+
|
|
313
177
|
mock_file.assert_called_once_with("config.json", "w", encoding="utf-8")
|
|
314
|
-
# Check that json.dump was called with correct data
|
|
315
178
|
handle = mock_file()
|
|
316
179
|
written_data = ''.join(call.args[0] for call in handle.write.call_args_list)
|
|
180
|
+
|
|
317
181
|
assert json.loads(written_data) == default_config_data
|
|
318
182
|
|
|
319
183
|
|
|
@@ -322,26 +186,53 @@ class TestReadData:
|
|
|
322
186
|
@patch('ara_cli.ara_config.save_data')
|
|
323
187
|
@patch('ara_cli.ara_config.ensure_directory_exists')
|
|
324
188
|
@patch('ara_cli.ara_config.exists', return_value=False)
|
|
325
|
-
def
|
|
189
|
+
def test_file_not_found_creates_default_and_exits(self, mock_exists, mock_ensure_dir, mock_save, mock_stdout):
|
|
190
|
+
"""Tests that a default config is created and the program exits if no config file is found."""
|
|
326
191
|
with pytest.raises(SystemExit) as exc_info:
|
|
327
|
-
read_data.cache_clear()
|
|
192
|
+
read_data.cache_clear()
|
|
328
193
|
read_data("config.json")
|
|
329
|
-
|
|
194
|
+
|
|
330
195
|
assert exc_info.value.code == 0
|
|
196
|
+
mock_ensure_dir.assert_called_once_with(os.path.dirname("config.json"))
|
|
331
197
|
mock_save.assert_called_once()
|
|
332
|
-
|
|
198
|
+
|
|
199
|
+
output = mock_stdout.getvalue()
|
|
200
|
+
assert "Configuration file not found. Creating a default one at 'config.json'." in output
|
|
201
|
+
assert "Please review the default configuration and re-run your command." in output
|
|
333
202
|
|
|
334
203
|
@patch('ara_cli.ara_config.save_data')
|
|
335
204
|
@patch('builtins.open')
|
|
336
205
|
@patch('ara_cli.ara_config.ensure_directory_exists')
|
|
337
206
|
@patch('ara_cli.ara_config.exists', return_value=True)
|
|
338
|
-
def
|
|
339
|
-
|
|
340
|
-
read_data.
|
|
207
|
+
def test_valid_config_is_loaded_and_resaved(self, mock_exists, mock_ensure_dir, mock_open_func, mock_save, valid_config_dict):
|
|
208
|
+
"""Tests that a valid config is loaded correctly and re-saved (to clean it)."""
|
|
209
|
+
m = mock_open(read_data=json.dumps(valid_config_dict))
|
|
210
|
+
mock_open_func.return_value = m()
|
|
211
|
+
read_data.cache_clear()
|
|
212
|
+
|
|
213
|
+
result = read_data("config.json")
|
|
214
|
+
|
|
215
|
+
assert isinstance(result, ARAconfig)
|
|
216
|
+
assert result.default_llm == "gpt-4o-custom"
|
|
217
|
+
mock_save.assert_called_once()
|
|
218
|
+
|
|
219
|
+
@patch('sys.stdout', new_callable=StringIO)
|
|
220
|
+
@patch('ara_cli.ara_config.save_data')
|
|
221
|
+
@patch('builtins.open', new_callable=mock_open, read_data="this is not json")
|
|
222
|
+
@patch('ara_cli.ara_config.ensure_directory_exists')
|
|
223
|
+
@patch('ara_cli.ara_config.exists', return_value=True)
|
|
224
|
+
def test_invalid_json_creates_default_config(self, mock_exists, mock_ensure_dir, mock_open_func, mock_save, mock_stdout):
|
|
225
|
+
"""Tests that a JSON decoding error results in a new default configuration."""
|
|
226
|
+
read_data.cache_clear()
|
|
341
227
|
|
|
342
228
|
result = read_data("config.json")
|
|
343
229
|
|
|
344
230
|
assert isinstance(result, ARAconfig)
|
|
231
|
+
assert result.default_llm == "gpt-4o" # Should be the default config
|
|
232
|
+
|
|
233
|
+
output = mock_stdout.getvalue()
|
|
234
|
+
assert "Error: Invalid JSON in configuration file" in output
|
|
235
|
+
assert "Creating a new configuration with defaults..." in output
|
|
345
236
|
mock_save.assert_called_once()
|
|
346
237
|
|
|
347
238
|
@patch('sys.stdout', new_callable=StringIO)
|
|
@@ -349,95 +240,97 @@ class TestReadData:
|
|
|
349
240
|
@patch('builtins.open')
|
|
350
241
|
@patch('ara_cli.ara_config.ensure_directory_exists')
|
|
351
242
|
@patch('ara_cli.ara_config.exists', return_value=True)
|
|
352
|
-
def
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
243
|
+
def test_config_with_validation_errors_is_fixed(self, mock_exists, mock_ensure_dir, mock_open_func, mock_save, mock_stdout, corrupted_config_dict):
|
|
244
|
+
"""Tests that a config with invalid fields is automatically corrected to defaults."""
|
|
245
|
+
m = mock_open(read_data=json.dumps(corrupted_config_dict))
|
|
246
|
+
mock_open_func.return_value = m()
|
|
247
|
+
read_data.cache_clear()
|
|
356
248
|
|
|
249
|
+
defaults = ARAconfig()
|
|
357
250
|
result = read_data("config.json")
|
|
358
|
-
|
|
251
|
+
|
|
359
252
|
assert isinstance(result, ARAconfig)
|
|
253
|
+
assert result.ext_code_dirs == defaults.ext_code_dirs
|
|
254
|
+
assert result.glossary_dir == defaults.glossary_dir
|
|
255
|
+
assert result.llm_config == defaults.llm_config
|
|
256
|
+
assert result.default_llm == defaults.default_llm
|
|
257
|
+
|
|
360
258
|
output = mock_stdout.getvalue()
|
|
361
|
-
|
|
362
|
-
assert
|
|
363
|
-
|
|
364
|
-
|
|
259
|
+
assert "--- Configuration Error Detected ---" in output
|
|
260
|
+
assert "-> Field 'ext_code_dirs' is invalid and will be reverted to its default value." in output
|
|
261
|
+
assert "-> Field 'glossary_dir' is invalid and will be reverted to its default value." in output
|
|
262
|
+
assert "-> Field 'llm_config' is invalid and will be reverted to its default value." in output
|
|
263
|
+
assert "Configuration has been corrected and saved" in output
|
|
264
|
+
|
|
265
|
+
mock_save.assert_called_once_with("config.json", result)
|
|
365
266
|
|
|
366
267
|
@patch('sys.stdout', new_callable=StringIO)
|
|
367
268
|
@patch('ara_cli.ara_config.save_data')
|
|
368
269
|
@patch('builtins.open')
|
|
369
270
|
@patch('ara_cli.ara_config.ensure_directory_exists')
|
|
370
271
|
@patch('ara_cli.ara_config.exists', return_value=True)
|
|
371
|
-
def
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
"glossary_dir": "./custom
|
|
375
|
-
"
|
|
376
|
-
"
|
|
272
|
+
def test_preserves_valid_fields_when_fixing_errors(self, mock_exists, mock_ensure_dir, mock_open_func, mock_save, mock_stdout):
|
|
273
|
+
"""Tests that valid, non-default values are preserved during a fix."""
|
|
274
|
+
mixed_config = {
|
|
275
|
+
"glossary_dir": "./my-custom-glossary", # Valid, non-default
|
|
276
|
+
"default_llm": 12345, # Invalid type
|
|
277
|
+
"unrecognized_key": "will_be_ignored" # Unrecognized
|
|
377
278
|
}
|
|
279
|
+
m = mock_open(read_data=json.dumps(mixed_config))
|
|
280
|
+
mock_open_func.return_value = m()
|
|
281
|
+
read_data.cache_clear()
|
|
378
282
|
|
|
379
|
-
|
|
380
|
-
read_data.cache_clear() # Clear cache
|
|
381
|
-
|
|
283
|
+
defaults = ARAconfig()
|
|
382
284
|
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
|
-
|
|
285
|
+
|
|
286
|
+
assert result.glossary_dir == "./my-custom-glossary"
|
|
287
|
+
assert result.default_llm == defaults.default_llm
|
|
288
|
+
|
|
391
289
|
output = mock_stdout.getvalue()
|
|
392
|
-
assert "
|
|
393
|
-
assert "
|
|
290
|
+
assert "Warning: Unrecognized configuration key 'unrecognized_key' will be ignored." in output
|
|
291
|
+
assert "-> Field 'default_llm' is invalid" in output
|
|
292
|
+
assert "-> Field 'glossary_dir' is invalid" not in output
|
|
394
293
|
|
|
294
|
+
mock_save.assert_called_once()
|
|
295
|
+
saved_config = mock_save.call_args[0][1]
|
|
296
|
+
assert saved_config.glossary_dir == "./my-custom-glossary"
|
|
297
|
+
assert saved_config.default_llm == defaults.default_llm
|
|
298
|
+
|
|
299
|
+
# --- Test Singleton Manager ---
|
|
395
300
|
|
|
396
301
|
class TestConfigManager:
|
|
397
302
|
@patch('ara_cli.ara_config.read_data')
|
|
398
|
-
def
|
|
399
|
-
|
|
400
|
-
mock_read.return_value =
|
|
303
|
+
def test_get_config_is_singleton(self, mock_read):
|
|
304
|
+
"""Tests that get_config returns the same instance on subsequent calls."""
|
|
305
|
+
mock_read.return_value = MagicMock(spec=ARAconfig)
|
|
401
306
|
|
|
402
|
-
# First call
|
|
403
307
|
config1 = ConfigManager.get_config()
|
|
404
|
-
assert config1 == mock_config
|
|
405
|
-
mock_read.assert_called_once()
|
|
406
|
-
|
|
407
|
-
# Second call should return cached instance
|
|
408
308
|
config2 = ConfigManager.get_config()
|
|
409
|
-
|
|
410
|
-
|
|
309
|
+
|
|
310
|
+
assert config1 is config2
|
|
311
|
+
mock_read.assert_called_once()
|
|
411
312
|
|
|
412
313
|
@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):
|
|
314
|
+
def test_reset_clears_instance_and_caches(self, mock_read):
|
|
315
|
+
"""Tests that the reset method clears the instance and underlying caches."""
|
|
416
316
|
mock_read.return_value = MagicMock(spec=ARAconfig)
|
|
417
|
-
|
|
418
|
-
ConfigManager.get_config("./custom/config.json")
|
|
419
|
-
mock_makedirs.assert_called_once_with("./custom")
|
|
420
317
|
|
|
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
|
|
318
|
+
ConfigManager.get_config()
|
|
319
|
+
mock_read.assert_called_once()
|
|
429
320
|
|
|
430
|
-
# Reset
|
|
431
321
|
ConfigManager.reset()
|
|
432
322
|
assert ConfigManager._config_instance is None
|
|
433
323
|
mock_read.cache_clear.assert_called_once()
|
|
434
324
|
|
|
325
|
+
ConfigManager.get_config()
|
|
326
|
+
assert mock_read.call_count == 2 # Called again after reset
|
|
327
|
+
|
|
435
328
|
@patch('ara_cli.ara_config.read_data')
|
|
436
|
-
def
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
329
|
+
def test_get_config_with_custom_filepath(self, mock_read):
|
|
330
|
+
"""Tests that get_config can be called with a custom file path."""
|
|
331
|
+
mock_read.return_value = MagicMock(spec=ARAconfig)
|
|
332
|
+
custom_path = "/custom/path/config.json"
|
|
333
|
+
|
|
334
|
+
ConfigManager.get_config(custom_path)
|
|
440
335
|
|
|
441
|
-
|
|
442
|
-
mock_read.assert_called_once_with(custom_path)
|
|
443
|
-
assert config == mock_config
|
|
336
|
+
mock_read.assert_called_once_with(custom_path)
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import base64
|
|
5
|
+
from unittest.mock import patch, MagicMock, mock_open, call
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from ara_cli import prompt_handler
|
|
9
|
+
from ara_cli.ara_config import ARAconfig, LLMConfigItem
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def mock_config():
|
|
13
|
+
"""Mocks a standard ARAconfig object for testing."""
|
|
14
|
+
config = ARAconfig(
|
|
15
|
+
ext_code_dirs=[{"source_dir": "./src"}],
|
|
16
|
+
glossary_dir="./glossary",
|
|
17
|
+
doc_dir="./docs",
|
|
18
|
+
local_prompt_templates_dir="./ara/.araconfig",
|
|
19
|
+
custom_prompt_templates_subdir="custom-prompt-modules",
|
|
20
|
+
ara_prompt_given_list_includes=["*.py"],
|
|
21
|
+
llm_config={
|
|
22
|
+
"gpt-4o": LLMConfigItem(provider="openai", model="openai/gpt-4o", temperature=0.8, max_tokens=1024),
|
|
23
|
+
"o3-mini": LLMConfigItem(provider="openai", model="openai/o3-mini", temperature=0.9, max_tokens=2048),
|
|
24
|
+
},
|
|
25
|
+
default_llm="gpt-4o"
|
|
26
|
+
)
|
|
27
|
+
return config
|
|
28
|
+
|
|
29
|
+
@pytest.fixture
|
|
30
|
+
def mock_config_manager(mock_config):
|
|
31
|
+
"""Patches ConfigManager to ensure it always returns the mock_config."""
|
|
32
|
+
with patch('ara_cli.ara_config.ConfigManager.get_config') as mock_get_config:
|
|
33
|
+
mock_get_config.return_value = mock_config
|
|
34
|
+
yield mock_get_config
|
|
35
|
+
|
|
36
|
+
@pytest.fixture(autouse=True)
|
|
37
|
+
def reset_singleton():
|
|
38
|
+
"""Resets the LLMSingleton before each test for isolation."""
|
|
39
|
+
prompt_handler.LLMSingleton._instance = None
|
|
40
|
+
prompt_handler.LLMSingleton._model = None
|
|
41
|
+
yield
|
|
42
|
+
|
|
43
|
+
class TestLLMSingleton:
|
|
44
|
+
"""Tests the behavior of the LLMSingleton class."""
|
|
45
|
+
|
|
46
|
+
def test_get_instance_creates_with_default_model(self, mock_config_manager):
|
|
47
|
+
instance = prompt_handler.LLMSingleton.get_instance()
|
|
48
|
+
assert instance is not None
|
|
49
|
+
assert prompt_handler.LLMSingleton.get_model() == "gpt-4o"
|
|
50
|
+
assert instance.config_parameters['temperature'] == 0.8
|
|
51
|
+
|
|
52
|
+
def test_get_instance_creates_with_first_model_if_no_default(self, mock_config_manager, mock_config):
|
|
53
|
+
mock_config.default_llm = None
|
|
54
|
+
instance = prompt_handler.LLMSingleton.get_instance()
|
|
55
|
+
assert instance is not None
|
|
56
|
+
assert prompt_handler.LLMSingleton.get_model() == "gpt-4o"
|
|
57
|
+
|
|
58
|
+
def test_get_instance_returns_same_instance(self, mock_config_manager):
|
|
59
|
+
instance1 = prompt_handler.LLMSingleton.get_instance()
|
|
60
|
+
instance2 = prompt_handler.LLMSingleton.get_instance()
|
|
61
|
+
assert instance1 is instance2
|
|
62
|
+
|
|
63
|
+
def test_set_model_switches_model(self, mock_config_manager):
|
|
64
|
+
initial_instance = prompt_handler.LLMSingleton.get_instance()
|
|
65
|
+
assert prompt_handler.LLMSingleton.get_model() == "gpt-4o"
|
|
66
|
+
|
|
67
|
+
with patch('builtins.print') as mock_print:
|
|
68
|
+
new_instance = prompt_handler.LLMSingleton.set_model("o3-mini")
|
|
69
|
+
mock_print.assert_called_with("Language model switched to 'o3-mini'")
|
|
70
|
+
|
|
71
|
+
assert prompt_handler.LLMSingleton.get_model() == "o3-mini"
|
|
72
|
+
assert new_instance.config_parameters['temperature'] == 0.9
|
|
73
|
+
assert initial_instance is not new_instance
|
|
74
|
+
|
|
75
|
+
def test_set_model_to_invalid_raises_error(self, mock_config_manager):
|
|
76
|
+
with pytest.raises(ValueError, match="No configuration found for the model: invalid-model"):
|
|
77
|
+
prompt_handler.LLMSingleton.set_model("invalid-model")
|
|
78
|
+
|
|
79
|
+
def test_get_model_initializes_if_needed(self, mock_config_manager):
|
|
80
|
+
assert prompt_handler.LLMSingleton._instance is None
|
|
81
|
+
model = prompt_handler.LLMSingleton.get_model()
|
|
82
|
+
assert model == "gpt-4o"
|
|
83
|
+
assert prompt_handler.LLMSingleton._instance is not None
|
|
84
|
+
|
|
85
|
+
class TestFileIO:
|
|
86
|
+
"""Tests file I/O helper functions."""
|
|
87
|
+
|
|
88
|
+
def test_write_and_read_string_from_file(self, tmp_path):
|
|
89
|
+
file_path = tmp_path / "test.txt"
|
|
90
|
+
test_string = "Hello World"
|
|
91
|
+
|
|
92
|
+
prompt_handler.write_string_to_file(file_path, test_string, 'w')
|
|
93
|
+
|
|
94
|
+
content = prompt_handler.read_string_from_file(file_path)
|
|
95
|
+
assert test_string in content
|
|
96
|
+
|
|
97
|
+
content_get = prompt_handler.get_file_content(file_path)
|
|
98
|
+
assert content == content_get
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class TestCoreLogic:
|
|
102
|
+
"""Tests functions related to the main business logic."""
|
|
103
|
+
|
|
104
|
+
@patch('ara_cli.prompt_handler.litellm.completion')
|
|
105
|
+
@patch('ara_cli.prompt_handler.LLMSingleton.get_instance')
|
|
106
|
+
def test_send_prompt(self, mock_get_instance, mock_completion, mock_config):
|
|
107
|
+
mock_llm_instance = MagicMock()
|
|
108
|
+
mock_llm_instance.config_parameters = mock_config.llm_config['gpt-4o'].model_dump()
|
|
109
|
+
mock_get_instance.return_value = mock_llm_instance
|
|
110
|
+
|
|
111
|
+
mock_chunk = MagicMock()
|
|
112
|
+
mock_chunk.choices[0].delta.content = "test chunk"
|
|
113
|
+
mock_completion.return_value = [mock_chunk]
|
|
114
|
+
|
|
115
|
+
prompt = [{"role": "user", "content": "A test"}]
|
|
116
|
+
|
|
117
|
+
result = list(prompt_handler.send_prompt(prompt))
|
|
118
|
+
|
|
119
|
+
expected_params = mock_config.llm_config['gpt-4o'].model_dump(exclude_none=True)
|
|
120
|
+
del expected_params['provider']
|
|
121
|
+
|
|
122
|
+
mock_completion.assert_called_once_with(
|
|
123
|
+
messages=prompt,
|
|
124
|
+
stream=True,
|
|
125
|
+
**expected_params
|
|
126
|
+
)
|
|
127
|
+
assert len(result) == 1
|
|
128
|
+
assert result[0].choices[0].delta.content == "test chunk"
|
|
129
|
+
|
|
130
|
+
@patch('ara_cli.prompt_handler.send_prompt')
|
|
131
|
+
def test_describe_image(self, mock_send_prompt, tmp_path):
|
|
132
|
+
fake_image_path = tmp_path / "test.png"
|
|
133
|
+
fake_image_content = b"fakeimagedata"
|
|
134
|
+
fake_image_path.write_bytes(fake_image_content)
|
|
135
|
+
|
|
136
|
+
mock_send_prompt.return_value = iter([])
|
|
137
|
+
|
|
138
|
+
prompt_handler.describe_image(fake_image_path)
|
|
139
|
+
|
|
140
|
+
mock_send_prompt.assert_called_once()
|
|
141
|
+
called_args = mock_send_prompt.call_args[0][0]
|
|
142
|
+
|
|
143
|
+
assert len(called_args) == 1
|
|
144
|
+
message_content = called_args[0]['content']
|
|
145
|
+
assert isinstance(message_content, list)
|
|
146
|
+
assert message_content[0]['type'] == 'text'
|
|
147
|
+
assert message_content[1]['type'] == 'image_url'
|
|
148
|
+
|
|
149
|
+
encoded_image = base64.b64encode(fake_image_content).decode('utf-8')
|
|
150
|
+
expected_url = f"data:image/png;base64,{encoded_image}"
|
|
151
|
+
assert message_content[1]['image_url']['url'] == expected_url
|
|
152
|
+
|
|
153
|
+
@patch('ara_cli.prompt_handler.Classifier.get_sub_directory', return_value="test_classifier")
|
|
154
|
+
def test_append_headings(self, mock_get_sub, tmp_path):
|
|
155
|
+
os.chdir(tmp_path)
|
|
156
|
+
os.makedirs("ara/test_classifier/my_param.data", exist_ok=True)
|
|
157
|
+
|
|
158
|
+
log_file = tmp_path / "ara/test_classifier/my_param.data/test_classifier.prompt_log.md"
|
|
159
|
+
|
|
160
|
+
prompt_handler.append_headings("test_classifier", "my_param", "PROMPT")
|
|
161
|
+
assert "## PROMPT_1" in log_file.read_text()
|
|
162
|
+
|
|
163
|
+
prompt_handler.append_headings("test_classifier", "my_param", "PROMPT")
|
|
164
|
+
assert "## PROMPT_2" in log_file.read_text()
|
|
165
|
+
|
|
166
|
+
prompt_handler.append_headings("test_classifier", "my_param", "RESULT")
|
|
167
|
+
assert "## RESULT_1" in log_file.read_text()
|
|
168
|
+
|
|
169
|
+
class TestArtefactAndTemplateHandling:
|
|
170
|
+
"""Tests functions that manage artefact and template files."""
|
|
171
|
+
|
|
172
|
+
@pytest.fixture(autouse=True)
|
|
173
|
+
def setup_fs(self, tmp_path):
|
|
174
|
+
self.root = tmp_path
|
|
175
|
+
os.chdir(self.root)
|
|
176
|
+
self.mock_classifier = "my_artefact"
|
|
177
|
+
self.mock_param = "my_param"
|
|
178
|
+
|
|
179
|
+
self.classifier_patch = patch('ara_cli.prompt_handler.Classifier.get_sub_directory', return_value=self.mock_classifier)
|
|
180
|
+
self.mock_get_sub_dir = self.classifier_patch.start()
|
|
181
|
+
|
|
182
|
+
yield
|
|
183
|
+
|
|
184
|
+
self.classifier_patch.stop()
|
|
185
|
+
|
|
186
|
+
def test_prompt_data_directory_creation(self):
|
|
187
|
+
path = prompt_handler.prompt_data_directory_creation(self.mock_classifier, self.mock_param)
|
|
188
|
+
expected_path = self.root / "ara" / self.mock_classifier / f"{self.mock_param}.data" / "prompt.data"
|
|
189
|
+
assert os.path.exists(expected_path)
|
|
190
|
+
assert Path(path).resolve() == expected_path.resolve()
|
|
191
|
+
|
|
192
|
+
@patch('ara_cli.prompt_handler.generate_markdown_listing')
|
|
193
|
+
def test_generate_config_prompt_givens_file(self, mock_generate_listing, mock_config_manager):
|
|
194
|
+
prompt_data_path = prompt_handler.prompt_data_directory_creation(self.mock_classifier, self.mock_param)
|
|
195
|
+
|
|
196
|
+
prompt_handler.generate_config_prompt_givens_file(prompt_data_path, "config.givens.md")
|
|
197
|
+
|
|
198
|
+
mock_generate_listing.assert_called_once()
|
|
199
|
+
args, _ = mock_generate_listing.call_args
|
|
200
|
+
assert "ara" in args[0]
|
|
201
|
+
assert "./src" in args[0]
|
|
202
|
+
assert "./docs" in args[0]
|
|
203
|
+
assert "./glossary" in args[0]
|
|
204
|
+
assert args[1] == ["*.py"]
|
|
205
|
+
assert args[2] == os.path.join(prompt_data_path, "config.givens.md")
|
|
206
|
+
|
|
207
|
+
@patch('ara_cli.prompt_handler.generate_markdown_listing')
|
|
208
|
+
def test_generate_config_prompt_givens_file_marks_artefact(self, mock_generate_listing, mock_config_manager):
|
|
209
|
+
prompt_data_path = Path(prompt_handler.prompt_data_directory_creation(self.mock_classifier, self.mock_param))
|
|
210
|
+
config_path = prompt_data_path / "config.givens.md"
|
|
211
|
+
artefact_to_mark = "file.py"
|
|
212
|
+
|
|
213
|
+
def create_fake_file(*args, **kwargs):
|
|
214
|
+
content = f"- [] some_other_file.txt\n- [] {artefact_to_mark}\n"
|
|
215
|
+
with open(args[2], 'w') as f:
|
|
216
|
+
f.write(content)
|
|
217
|
+
|
|
218
|
+
mock_generate_listing.side_effect = create_fake_file
|
|
219
|
+
|
|
220
|
+
prompt_handler.generate_config_prompt_givens_file(
|
|
221
|
+
str(prompt_data_path), "config.givens.md", artefact_to_mark=artefact_to_mark
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
content = config_path.read_text()
|
|
225
|
+
assert f"- [x] {artefact_to_mark}" in content
|
|
226
|
+
assert f"- [] some_other_file.txt" in content
|
|
227
|
+
|
|
228
|
+
@patch('ara_cli.prompt_handler.extract_and_load_markdown_files')
|
|
229
|
+
@patch('ara_cli.prompt_handler.move_and_copy_files')
|
|
230
|
+
@patch('ara_cli.prompt_handler.TemplatePathManager.get_template_base_path', return_value="/global/templates")
|
|
231
|
+
def test_load_selected_prompt_templates(self, mock_base_path, mock_move, mock_extract, mock_config_manager):
|
|
232
|
+
prompt_data_path = prompt_handler.prompt_data_directory_creation(self.mock_classifier, self.mock_param)
|
|
233
|
+
config_file = Path(prompt_data_path) / "config.prompt_templates.md"
|
|
234
|
+
config_file.touch()
|
|
235
|
+
|
|
236
|
+
mock_extract.return_value = [
|
|
237
|
+
"custom-prompt-modules/my_custom.rules.md",
|
|
238
|
+
"prompt-modules/global.intention.md",
|
|
239
|
+
"unrecognized/file.md"
|
|
240
|
+
]
|
|
241
|
+
|
|
242
|
+
prompt_handler.load_selected_prompt_templates(self.mock_classifier, self.mock_param)
|
|
243
|
+
|
|
244
|
+
archive_path = os.path.join(prompt_data_path, "prompt.archive")
|
|
245
|
+
|
|
246
|
+
assert mock_move.call_count == 2
|
|
247
|
+
expected_calls = [
|
|
248
|
+
call(
|
|
249
|
+
os.path.join(mock_config_manager.return_value.local_prompt_templates_dir, "custom-prompt-modules/my_custom.rules.md"),
|
|
250
|
+
prompt_data_path,
|
|
251
|
+
archive_path
|
|
252
|
+
),
|
|
253
|
+
call(
|
|
254
|
+
os.path.join("/global/templates", "prompt-modules/global.intention.md"),
|
|
255
|
+
prompt_data_path,
|
|
256
|
+
archive_path
|
|
257
|
+
)
|
|
258
|
+
]
|
|
259
|
+
mock_move.assert_has_calls(expected_calls, any_order=True)
|
|
260
|
+
|
|
261
|
+
def test_extract_and_load_markdown_files(self):
|
|
262
|
+
md_content = """
|
|
263
|
+
# prompt-modules
|
|
264
|
+
## a-category
|
|
265
|
+
- [x] first.rules.md
|
|
266
|
+
- [] second.rules.md
|
|
267
|
+
# custom-prompt-modules
|
|
268
|
+
- [x] custom.intention.md
|
|
269
|
+
"""
|
|
270
|
+
m = mock_open(read_data=md_content)
|
|
271
|
+
with patch('builtins.open', m):
|
|
272
|
+
paths = prompt_handler.extract_and_load_markdown_files("dummy_path")
|
|
273
|
+
|
|
274
|
+
assert len(paths) == 2
|
|
275
|
+
assert 'prompt-modules/a-category/first.rules.md' in paths
|
|
276
|
+
assert 'custom-prompt-modules/custom.intention.md' in paths
|
|
277
|
+
|
|
278
|
+
@patch('ara_cli.prompt_handler.send_prompt')
|
|
279
|
+
@patch('ara_cli.prompt_handler.collect_file_content_by_extension')
|
|
280
|
+
@patch('ara_cli.prompt_handler.append_images_to_message')
|
|
281
|
+
def test_create_and_send_custom_prompt(self, mock_append_images, mock_collect, mock_send):
|
|
282
|
+
prompt_data_path = Path(prompt_handler.prompt_data_directory_creation(self.mock_classifier, self.mock_param))
|
|
283
|
+
|
|
284
|
+
mock_collect.return_value = ("### GIVENS\ncontent", [{"type": "image_url", "image_url": {}}])
|
|
285
|
+
|
|
286
|
+
final_message_list = [{"role": "user", "content": [{"type": "text", "text": "### GIVENS\ncontent"}, {"type": "image_url", "image_url": {}}]}]
|
|
287
|
+
mock_append_images.return_value = final_message_list
|
|
288
|
+
|
|
289
|
+
mock_send.return_value = iter([MagicMock(choices=[MagicMock(delta=MagicMock(content="llm response"))])])
|
|
290
|
+
|
|
291
|
+
prompt_handler.create_and_send_custom_prompt(self.mock_classifier, self.mock_param)
|
|
292
|
+
|
|
293
|
+
mock_collect.assert_called_once()
|
|
294
|
+
mock_append_images.assert_called_once()
|
|
295
|
+
mock_send.assert_called_once_with(final_message_list)
|
|
296
|
+
|
|
297
|
+
artefact_root = self.root / "ara" / self.mock_classifier
|
|
298
|
+
log_file = artefact_root / f"{self.mock_param}.data" / f"{self.mock_classifier}.prompt_log.md"
|
|
299
|
+
|
|
300
|
+
assert log_file.exists()
|
|
301
|
+
log_content = log_file.read_text()
|
|
302
|
+
assert "### GIVENS\ncontent" in log_content
|
|
303
|
+
assert "llm response" in log_content
|
|
File without changes
|
|
File without changes
|
|
File without changes
|