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.
- ara_cli/__init__.py +18 -2
- ara_cli/__main__.py +248 -62
- ara_cli/ara_command_action.py +155 -86
- ara_cli/ara_config.py +226 -80
- ara_cli/ara_subcommands/__init__.py +0 -0
- ara_cli/ara_subcommands/autofix.py +26 -0
- ara_cli/ara_subcommands/chat.py +27 -0
- ara_cli/ara_subcommands/classifier_directory.py +16 -0
- ara_cli/ara_subcommands/common.py +100 -0
- ara_cli/ara_subcommands/create.py +75 -0
- ara_cli/ara_subcommands/delete.py +22 -0
- ara_cli/ara_subcommands/extract.py +22 -0
- ara_cli/ara_subcommands/fetch_templates.py +14 -0
- ara_cli/ara_subcommands/list.py +65 -0
- ara_cli/ara_subcommands/list_tags.py +25 -0
- ara_cli/ara_subcommands/load.py +48 -0
- ara_cli/ara_subcommands/prompt.py +136 -0
- ara_cli/ara_subcommands/read.py +47 -0
- ara_cli/ara_subcommands/read_status.py +20 -0
- ara_cli/ara_subcommands/read_user.py +20 -0
- ara_cli/ara_subcommands/reconnect.py +27 -0
- ara_cli/ara_subcommands/rename.py +22 -0
- ara_cli/ara_subcommands/scan.py +14 -0
- ara_cli/ara_subcommands/set_status.py +22 -0
- ara_cli/ara_subcommands/set_user.py +22 -0
- ara_cli/ara_subcommands/template.py +16 -0
- ara_cli/artefact_autofix.py +649 -68
- ara_cli/artefact_creator.py +8 -11
- ara_cli/artefact_deleter.py +2 -4
- ara_cli/artefact_fuzzy_search.py +22 -10
- ara_cli/artefact_link_updater.py +4 -4
- ara_cli/artefact_lister.py +29 -55
- ara_cli/artefact_models/artefact_data_retrieval.py +23 -0
- ara_cli/artefact_models/artefact_load.py +11 -3
- ara_cli/artefact_models/artefact_model.py +146 -39
- ara_cli/artefact_models/artefact_templates.py +70 -44
- ara_cli/artefact_models/businessgoal_artefact_model.py +23 -25
- ara_cli/artefact_models/epic_artefact_model.py +34 -26
- ara_cli/artefact_models/feature_artefact_model.py +203 -64
- ara_cli/artefact_models/keyfeature_artefact_model.py +21 -24
- ara_cli/artefact_models/serialize_helper.py +1 -1
- ara_cli/artefact_models/task_artefact_model.py +83 -15
- ara_cli/artefact_models/userstory_artefact_model.py +37 -27
- ara_cli/artefact_models/vision_artefact_model.py +23 -42
- ara_cli/artefact_reader.py +92 -91
- ara_cli/artefact_renamer.py +8 -4
- ara_cli/artefact_scan.py +66 -3
- ara_cli/chat.py +622 -162
- ara_cli/chat_agent/__init__.py +0 -0
- ara_cli/chat_agent/agent_communicator.py +62 -0
- ara_cli/chat_agent/agent_process_manager.py +211 -0
- ara_cli/chat_agent/agent_status_manager.py +73 -0
- ara_cli/chat_agent/agent_workspace_manager.py +76 -0
- ara_cli/commands/__init__.py +0 -0
- ara_cli/commands/command.py +7 -0
- ara_cli/commands/extract_command.py +15 -0
- ara_cli/commands/load_command.py +65 -0
- ara_cli/commands/load_image_command.py +34 -0
- ara_cli/commands/read_command.py +117 -0
- ara_cli/completers.py +144 -0
- ara_cli/directory_navigator.py +37 -4
- ara_cli/error_handler.py +134 -0
- ara_cli/file_classifier.py +6 -5
- ara_cli/file_lister.py +1 -1
- ara_cli/file_loaders/__init__.py +0 -0
- ara_cli/file_loaders/binary_file_loader.py +33 -0
- ara_cli/file_loaders/document_file_loader.py +34 -0
- ara_cli/file_loaders/document_reader.py +245 -0
- ara_cli/file_loaders/document_readers.py +233 -0
- ara_cli/file_loaders/file_loader.py +50 -0
- ara_cli/file_loaders/file_loaders.py +123 -0
- ara_cli/file_loaders/image_processor.py +89 -0
- ara_cli/file_loaders/markdown_reader.py +75 -0
- ara_cli/file_loaders/text_file_loader.py +187 -0
- ara_cli/global_file_lister.py +51 -0
- ara_cli/list_filter.py +1 -1
- ara_cli/output_suppressor.py +1 -1
- ara_cli/prompt_extractor.py +215 -88
- ara_cli/prompt_handler.py +521 -134
- ara_cli/prompt_rag.py +2 -2
- ara_cli/tag_extractor.py +83 -38
- ara_cli/template_loader.py +245 -0
- ara_cli/template_manager.py +18 -13
- ara_cli/templates/prompt-modules/commands/empty.commands.md +2 -12
- ara_cli/templates/prompt-modules/commands/extract_general.commands.md +12 -0
- ara_cli/templates/prompt-modules/commands/extract_markdown.commands.md +11 -0
- ara_cli/templates/prompt-modules/commands/extract_python.commands.md +13 -0
- ara_cli/templates/prompt-modules/commands/feature_add_or_modifiy_specified_behavior.commands.md +36 -0
- ara_cli/templates/prompt-modules/commands/feature_generate_initial_specified_bevahior.commands.md +53 -0
- ara_cli/templates/prompt-modules/commands/prompt_template_tech_stack_transformer.commands.md +95 -0
- ara_cli/templates/prompt-modules/commands/python_bug_fixing_code.commands.md +34 -0
- ara_cli/templates/prompt-modules/commands/python_generate_code.commands.md +27 -0
- ara_cli/templates/prompt-modules/commands/python_refactoring_code.commands.md +39 -0
- ara_cli/templates/prompt-modules/commands/python_step_definitions_generation_and_fixing.commands.md +40 -0
- ara_cli/templates/prompt-modules/commands/python_unittest_generation_and_fixing.commands.md +48 -0
- ara_cli/update_config_prompt.py +9 -3
- ara_cli/version.py +1 -1
- ara_cli-0.1.10.8.dist-info/METADATA +241 -0
- ara_cli-0.1.10.8.dist-info/RECORD +193 -0
- tests/test_ara_command_action.py +73 -59
- tests/test_ara_config.py +341 -36
- tests/test_artefact_autofix.py +1060 -0
- tests/test_artefact_link_updater.py +3 -3
- tests/test_artefact_lister.py +52 -132
- tests/test_artefact_renamer.py +2 -2
- tests/test_artefact_scan.py +327 -33
- tests/test_chat.py +2063 -498
- tests/test_file_classifier.py +24 -1
- tests/test_file_creator.py +3 -5
- tests/test_file_lister.py +1 -1
- tests/test_global_file_lister.py +131 -0
- tests/test_list_filter.py +2 -2
- tests/test_prompt_handler.py +746 -0
- tests/test_tag_extractor.py +19 -13
- tests/test_template_loader.py +192 -0
- tests/test_template_manager.py +5 -4
- tests/test_update_config_prompt.py +2 -2
- ara_cli/ara_command_parser.py +0 -327
- ara_cli/templates/prompt-modules/blueprints/complete_pytest_unittest.blueprint.md +0 -27
- ara_cli/templates/prompt-modules/blueprints/task_todo_list_implement_feature_BDD_way.blueprint.md +0 -30
- ara_cli/templates/prompt-modules/commands/artefact_classification.commands.md +0 -9
- ara_cli/templates/prompt-modules/commands/artefact_extension.commands.md +0 -17
- ara_cli/templates/prompt-modules/commands/artefact_formulation.commands.md +0 -14
- ara_cli/templates/prompt-modules/commands/behave_step_generation.commands.md +0 -102
- ara_cli/templates/prompt-modules/commands/code_generation_complex.commands.md +0 -20
- ara_cli/templates/prompt-modules/commands/code_generation_simple.commands.md +0 -13
- ara_cli/templates/prompt-modules/commands/error_fixing.commands.md +0 -20
- ara_cli/templates/prompt-modules/commands/feature_file_update.commands.md +0 -18
- ara_cli/templates/prompt-modules/commands/feature_formulation.commands.md +0 -43
- ara_cli/templates/prompt-modules/commands/js_code_generation_simple.commands.md +0 -13
- ara_cli/templates/prompt-modules/commands/refactoring.commands.md +0 -15
- ara_cli/templates/prompt-modules/commands/refactoring_analysis.commands.md +0 -9
- ara_cli/templates/prompt-modules/commands/reverse_engineer_feature_file.commands.md +0 -15
- ara_cli/templates/prompt-modules/commands/reverse_engineer_program_flow.commands.md +0 -19
- ara_cli/templates/template.businessgoal +0 -10
- ara_cli/templates/template.capability +0 -10
- ara_cli/templates/template.epic +0 -15
- ara_cli/templates/template.example +0 -6
- ara_cli/templates/template.feature +0 -26
- ara_cli/templates/template.issue +0 -14
- ara_cli/templates/template.keyfeature +0 -15
- ara_cli/templates/template.task +0 -6
- ara_cli/templates/template.userstory +0 -17
- ara_cli/templates/template.vision +0 -14
- ara_cli-0.1.9.69.dist-info/METADATA +0 -16
- ara_cli-0.1.9.69.dist-info/RECORD +0 -158
- tests/test_ara_autofix.py +0 -219
- {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.10.8.dist-info}/WHEEL +0 -0
- {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.10.8.dist-info}/entry_points.txt +0 -0
- {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.10.8.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1060 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from unittest.mock import patch, mock_open, MagicMock
|
|
3
|
+
from ara_cli.error_handler import AraError
|
|
4
|
+
from ara_cli.artefact_autofix import (
|
|
5
|
+
read_report_file,
|
|
6
|
+
parse_report,
|
|
7
|
+
apply_autofix,
|
|
8
|
+
read_artefact,
|
|
9
|
+
determine_artefact_type_and_class,
|
|
10
|
+
run_agent,
|
|
11
|
+
write_corrected_artefact,
|
|
12
|
+
construct_prompt,
|
|
13
|
+
fix_title_mismatch,
|
|
14
|
+
ask_for_correct_contribution,
|
|
15
|
+
ask_for_contribution_choice,
|
|
16
|
+
_has_valid_contribution,
|
|
17
|
+
set_closest_contribution,
|
|
18
|
+
fix_contribution,
|
|
19
|
+
fix_rule,
|
|
20
|
+
fix_scenario_placeholder_mismatch,
|
|
21
|
+
_extract_scenario_block,
|
|
22
|
+
_is_scenario_boundary,
|
|
23
|
+
_process_scenario_block,
|
|
24
|
+
_get_line_indentation,
|
|
25
|
+
_extract_placeholders_from_scenario,
|
|
26
|
+
_update_docstring_state,
|
|
27
|
+
_convert_to_scenario_outline,
|
|
28
|
+
_create_examples_table,
|
|
29
|
+
populate_classified_artefact_info,
|
|
30
|
+
should_skip_issue,
|
|
31
|
+
determine_attempt_count,
|
|
32
|
+
apply_deterministic_fix,
|
|
33
|
+
apply_non_deterministic_fix,
|
|
34
|
+
attempt_autofix_loop,
|
|
35
|
+
)
|
|
36
|
+
from ara_cli.artefact_models.artefact_model import Artefact, ArtefactType, Contribution
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.fixture
|
|
40
|
+
def mock_artefact_type():
|
|
41
|
+
"""Provides a mock for the ArtefactType enum member."""
|
|
42
|
+
mock_type = MagicMock()
|
|
43
|
+
mock_type.value = "feature"
|
|
44
|
+
return mock_type
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@pytest.fixture
|
|
48
|
+
def mock_artefact_class():
|
|
49
|
+
"""Provides a mock for the Artefact class."""
|
|
50
|
+
mock_class = MagicMock()
|
|
51
|
+
mock_class._title_prefix.return_value = "Feature:"
|
|
52
|
+
# Mock the serialize method for the agent tests
|
|
53
|
+
mock_class.serialize.return_value = "llm corrected content"
|
|
54
|
+
return mock_class
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@pytest.fixture
|
|
58
|
+
def mock_classified_artefact_info():
|
|
59
|
+
"""Provides a mock for the classified artefact info dictionary."""
|
|
60
|
+
return MagicMock()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@pytest.fixture
|
|
64
|
+
def mock_artefact_with_contribution():
|
|
65
|
+
"""Provides a mock Artefact with a mock Contribution."""
|
|
66
|
+
mock_contribution = MagicMock(spec=Contribution)
|
|
67
|
+
mock_contribution.artefact_name = "some_artefact"
|
|
68
|
+
mock_contribution.classifier = "feature"
|
|
69
|
+
mock_contribution.rule = "some rule"
|
|
70
|
+
|
|
71
|
+
mock_artefact = MagicMock(spec=Artefact)
|
|
72
|
+
mock_artefact.contribution = mock_contribution
|
|
73
|
+
mock_artefact.title = "my_test_artefact"
|
|
74
|
+
mock_artefact._artefact_type.return_value.value = "requirement"
|
|
75
|
+
mock_artefact.serialize.return_value = "serialized artefact text"
|
|
76
|
+
|
|
77
|
+
return mock_artefact
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@pytest.fixture
|
|
81
|
+
def mock_contribution():
|
|
82
|
+
m = MagicMock()
|
|
83
|
+
m.artefact_name = "parent_name"
|
|
84
|
+
m.classifier = "feature"
|
|
85
|
+
m.rule = "my_rule"
|
|
86
|
+
return m
|
|
87
|
+
|
|
88
|
+
@pytest.fixture
|
|
89
|
+
def mock_artefact(mock_contribution):
|
|
90
|
+
m = MagicMock()
|
|
91
|
+
m.contribution = mock_contribution
|
|
92
|
+
m._artefact_type.return_value.value = "requirement"
|
|
93
|
+
m.title = "my_title"
|
|
94
|
+
m.serialize.return_value = "serialized-text"
|
|
95
|
+
return m
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_read_report_file_success():
|
|
99
|
+
"""Tests successful reading of the report file."""
|
|
100
|
+
mock_content = "# Artefact Check Report\n- `file.feature`: reason"
|
|
101
|
+
with patch("builtins.open", mock_open(read_data=mock_content)) as m:
|
|
102
|
+
content = read_report_file()
|
|
103
|
+
assert content == mock_content
|
|
104
|
+
m.assert_called_once_with(
|
|
105
|
+
"incompatible_artefacts_report.md", "r", encoding="utf-8"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_read_report_file_not_found(capsys):
|
|
110
|
+
with patch("builtins.open", side_effect=OSError("File not found")):
|
|
111
|
+
content = read_report_file()
|
|
112
|
+
assert content is None
|
|
113
|
+
assert "Artefact scan results file not found" in capsys.readouterr().out
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_parse_report_with_issues():
|
|
117
|
+
content = (
|
|
118
|
+
"# Artefact Check Report\n\n## feature\n- `path/to/file.feature`: A reason\n"
|
|
119
|
+
)
|
|
120
|
+
expected = {"feature": [("path/to/file.feature", "A reason")]}
|
|
121
|
+
assert parse_report(content) == expected
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_parse_report_no_issues():
|
|
125
|
+
content = "# Artefact Check Report\n\nNo problems found.\n"
|
|
126
|
+
assert parse_report(content) == {}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_parse_report_invalid_format():
|
|
130
|
+
assert parse_report("This is not a valid report") == {}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_parse_report_invalid_line_format():
|
|
134
|
+
content = "# Artefact Check Report\n\n## feature\n- an invalid line\n"
|
|
135
|
+
assert parse_report(content) == {"feature": []}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_read_artefact_success():
|
|
139
|
+
mock_content = "Feature: My Feature"
|
|
140
|
+
with patch("builtins.open", mock_open(read_data=mock_content)) as m:
|
|
141
|
+
content = read_artefact("file.feature")
|
|
142
|
+
assert content == mock_content
|
|
143
|
+
m.assert_called_once_with("file.feature", "r", encoding="utf-8")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_read_artefact_file_not_found(capsys):
|
|
147
|
+
with patch("builtins.open", side_effect=FileNotFoundError):
|
|
148
|
+
result = read_artefact("nonexistent.feature")
|
|
149
|
+
assert result is None
|
|
150
|
+
assert "File not found: nonexistent.feature" in capsys.readouterr().out
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@patch("ara_cli.artefact_models.artefact_mapping.artefact_type_mapping")
|
|
154
|
+
def test_determine_artefact_type_and_class_no_class_found(mock_mapping, capsys):
|
|
155
|
+
mock_mapping.get.return_value = None
|
|
156
|
+
with pytest.raises(AraError):
|
|
157
|
+
artefact_type, artefact_class = determine_artefact_type_and_class("feature")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@patch("ara_cli.artefact_models.artefact_model.ArtefactType", side_effect=ValueError)
|
|
161
|
+
def test_determine_artefact_type_and_class_invalid(mock_artefact_type_enum, capsys):
|
|
162
|
+
artefact_type, artefact_class = determine_artefact_type_and_class(
|
|
163
|
+
"invalid_classifier"
|
|
164
|
+
)
|
|
165
|
+
assert artefact_type is None
|
|
166
|
+
assert artefact_class is None
|
|
167
|
+
assert "Invalid classifier: invalid_classifier" in capsys.readouterr().out
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_write_corrected_artefact():
|
|
171
|
+
with patch("builtins.open", mock_open()) as m:
|
|
172
|
+
write_corrected_artefact("file.feature", "corrected content")
|
|
173
|
+
m.assert_called_once_with("file.feature", "w", encoding="utf-8")
|
|
174
|
+
m().write.assert_called_once_with("corrected content")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_construct_prompt_for_task():
|
|
178
|
+
prompt = construct_prompt(ArtefactType.task, "some reason", "file.task", "text")
|
|
179
|
+
assert (
|
|
180
|
+
"For task artefacts, if the action items looks like template or empty"
|
|
181
|
+
in prompt
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@patch("ara_cli.artefact_autofix.run_agent")
|
|
186
|
+
@patch(
|
|
187
|
+
"ara_cli.artefact_autofix.determine_artefact_type_and_class",
|
|
188
|
+
return_value=(None, None),
|
|
189
|
+
)
|
|
190
|
+
@patch("ara_cli.artefact_autofix.read_artefact")
|
|
191
|
+
def test_apply_autofix_exits_when_classifier_is_invalid(
|
|
192
|
+
mock_read, mock_determine, mock_run_agent, mock_classified_artefact_info
|
|
193
|
+
):
|
|
194
|
+
"""Tests that apply_autofix exits early if the classifier is invalid."""
|
|
195
|
+
result = apply_autofix(
|
|
196
|
+
file_path="file.feature",
|
|
197
|
+
classifier="invalid",
|
|
198
|
+
reason="reason",
|
|
199
|
+
deterministic=True,
|
|
200
|
+
non_deterministic=True,
|
|
201
|
+
classified_artefact_info=mock_classified_artefact_info,
|
|
202
|
+
)
|
|
203
|
+
assert result is False
|
|
204
|
+
mock_determine.assert_called_once_with("invalid")
|
|
205
|
+
mock_read.assert_not_called()
|
|
206
|
+
mock_run_agent.assert_not_called()
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@patch("ara_cli.artefact_autofix.FileClassifier")
|
|
210
|
+
@patch("ara_cli.artefact_autofix.check_file")
|
|
211
|
+
@patch("ara_cli.artefact_autofix.run_agent")
|
|
212
|
+
@patch("ara_cli.artefact_autofix.write_corrected_artefact")
|
|
213
|
+
@patch("ara_cli.artefact_autofix.fix_title_mismatch")
|
|
214
|
+
@patch("ara_cli.artefact_autofix.determine_artefact_type_and_class")
|
|
215
|
+
@patch("ara_cli.artefact_autofix.read_artefact", return_value="original text")
|
|
216
|
+
def test_apply_autofix_for_title_mismatch_with_deterministic_flag(
|
|
217
|
+
mock_read,
|
|
218
|
+
mock_determine,
|
|
219
|
+
mock_fix_title,
|
|
220
|
+
mock_write,
|
|
221
|
+
mock_run_agent,
|
|
222
|
+
mock_check_file,
|
|
223
|
+
mock_file_classifier,
|
|
224
|
+
mock_artefact_type,
|
|
225
|
+
mock_artefact_class,
|
|
226
|
+
mock_classified_artefact_info,
|
|
227
|
+
):
|
|
228
|
+
"""Tests that a deterministic fix is applied when the flag is True."""
|
|
229
|
+
mock_determine.return_value = (mock_artefact_type, mock_artefact_class)
|
|
230
|
+
mock_check_file.side_effect = [
|
|
231
|
+
(False, "Filename-Title Mismatch: some details"),
|
|
232
|
+
(True, ""),
|
|
233
|
+
]
|
|
234
|
+
mock_fix_title.return_value = "fixed text"
|
|
235
|
+
|
|
236
|
+
result = apply_autofix(
|
|
237
|
+
file_path="file.feature",
|
|
238
|
+
classifier="feature",
|
|
239
|
+
reason="Filename-Title Mismatch: some details",
|
|
240
|
+
deterministic=True,
|
|
241
|
+
non_deterministic=False,
|
|
242
|
+
classified_artefact_info=mock_classified_artefact_info,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
assert result is True
|
|
246
|
+
assert mock_check_file.call_count == 2
|
|
247
|
+
mock_fix_title.assert_called_once_with(
|
|
248
|
+
file_path="file.feature",
|
|
249
|
+
artefact_text="original text",
|
|
250
|
+
artefact_class=mock_artefact_class,
|
|
251
|
+
classified_artefact_info=mock_classified_artefact_info,
|
|
252
|
+
)
|
|
253
|
+
mock_write.assert_called_once_with("file.feature", "fixed text")
|
|
254
|
+
mock_run_agent.assert_not_called()
|
|
255
|
+
mock_file_classifier.assert_called_once()
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@patch("ara_cli.artefact_autofix.check_file")
|
|
259
|
+
@patch("ara_cli.artefact_autofix.run_agent")
|
|
260
|
+
@patch("ara_cli.artefact_autofix.write_corrected_artefact")
|
|
261
|
+
@patch("ara_cli.artefact_autofix.fix_title_mismatch")
|
|
262
|
+
@patch("ara_cli.artefact_autofix.determine_artefact_type_and_class")
|
|
263
|
+
@patch("ara_cli.artefact_autofix.read_artefact", return_value="original text")
|
|
264
|
+
def test_apply_autofix_skips_title_mismatch_without_deterministic_flag(
|
|
265
|
+
mock_read,
|
|
266
|
+
mock_determine,
|
|
267
|
+
mock_fix_title,
|
|
268
|
+
mock_write,
|
|
269
|
+
mock_run_agent,
|
|
270
|
+
mock_check_file,
|
|
271
|
+
mock_artefact_type,
|
|
272
|
+
mock_artefact_class,
|
|
273
|
+
mock_classified_artefact_info,
|
|
274
|
+
):
|
|
275
|
+
"""Tests that a deterministic fix is skipped when the flag is False."""
|
|
276
|
+
mock_determine.return_value = (mock_artefact_type, mock_artefact_class)
|
|
277
|
+
mock_check_file.return_value = (False, "Filename-Title Mismatch: some details")
|
|
278
|
+
|
|
279
|
+
result = apply_autofix(
|
|
280
|
+
file_path="file.feature",
|
|
281
|
+
classifier="feature",
|
|
282
|
+
reason="Filename-Title Mismatch: some details",
|
|
283
|
+
deterministic=False,
|
|
284
|
+
non_deterministic=True,
|
|
285
|
+
classified_artefact_info=mock_classified_artefact_info,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
assert result is False
|
|
289
|
+
mock_check_file.assert_called_once()
|
|
290
|
+
mock_read.assert_called_once_with("file.feature")
|
|
291
|
+
mock_fix_title.assert_not_called()
|
|
292
|
+
mock_write.assert_not_called()
|
|
293
|
+
mock_run_agent.assert_not_called()
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@patch("ara_cli.artefact_autofix.FileClassifier")
|
|
297
|
+
@patch("ara_cli.artefact_autofix.check_file")
|
|
298
|
+
@patch("ara_cli.artefact_autofix.write_corrected_artefact")
|
|
299
|
+
@patch("ara_cli.artefact_autofix.run_agent")
|
|
300
|
+
@patch("ara_cli.artefact_autofix.determine_artefact_type_and_class")
|
|
301
|
+
@patch("ara_cli.artefact_autofix.read_artefact", return_value="original text")
|
|
302
|
+
def test_apply_autofix_for_llm_fix_with_non_deterministic_flag(
|
|
303
|
+
mock_read,
|
|
304
|
+
mock_determine,
|
|
305
|
+
mock_run_agent,
|
|
306
|
+
mock_write,
|
|
307
|
+
mock_check_file,
|
|
308
|
+
mock_file_classifier,
|
|
309
|
+
mock_artefact_type,
|
|
310
|
+
mock_artefact_class,
|
|
311
|
+
mock_classified_artefact_info,
|
|
312
|
+
):
|
|
313
|
+
"""Tests that an LLM fix is applied when the non-deterministic flag is True."""
|
|
314
|
+
mock_determine.return_value = (mock_artefact_type, mock_artefact_class)
|
|
315
|
+
mock_check_file.side_effect = [(False, "Pydantic validation error"), (True, "")]
|
|
316
|
+
mock_run_agent.return_value = mock_artefact_class
|
|
317
|
+
|
|
318
|
+
result = apply_autofix(
|
|
319
|
+
file_path="file.feature",
|
|
320
|
+
classifier="feature",
|
|
321
|
+
reason="Pydantic validation error",
|
|
322
|
+
deterministic=False,
|
|
323
|
+
non_deterministic=True,
|
|
324
|
+
classified_artefact_info=mock_classified_artefact_info,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
assert result is True
|
|
328
|
+
assert mock_check_file.call_count == 2
|
|
329
|
+
mock_read.assert_called_once_with("file.feature")
|
|
330
|
+
mock_run_agent.assert_called_once()
|
|
331
|
+
mock_write.assert_called_once_with("file.feature", "llm corrected content")
|
|
332
|
+
mock_file_classifier.assert_called_once()
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
@patch("ara_cli.artefact_autofix.write_corrected_artefact")
|
|
336
|
+
@patch("ara_cli.artefact_autofix.run_agent")
|
|
337
|
+
@patch("ara_cli.artefact_autofix.determine_artefact_type_and_class")
|
|
338
|
+
@patch("ara_cli.artefact_autofix.read_artefact", return_value="original text")
|
|
339
|
+
def test_apply_autofix_skips_llm_fix_without_non_deterministic_flag(
|
|
340
|
+
mock_read,
|
|
341
|
+
mock_determine,
|
|
342
|
+
mock_run_agent,
|
|
343
|
+
mock_write,
|
|
344
|
+
mock_artefact_type,
|
|
345
|
+
mock_artefact_class,
|
|
346
|
+
mock_classified_artefact_info,
|
|
347
|
+
):
|
|
348
|
+
"""Tests that an LLM fix is skipped when the non-deterministic flag is False."""
|
|
349
|
+
mock_determine.return_value = (mock_artefact_type, mock_artefact_class)
|
|
350
|
+
reason = "Pydantic validation error"
|
|
351
|
+
|
|
352
|
+
result = apply_autofix(
|
|
353
|
+
"file.feature",
|
|
354
|
+
"feature",
|
|
355
|
+
reason,
|
|
356
|
+
deterministic=True,
|
|
357
|
+
non_deterministic=False,
|
|
358
|
+
classified_artefact_info=mock_classified_artefact_info,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
assert result is False
|
|
362
|
+
mock_run_agent.assert_not_called()
|
|
363
|
+
mock_write.assert_not_called()
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
@patch("ara_cli.artefact_autofix.run_agent", side_effect=Exception("LLM failed"))
|
|
367
|
+
@patch("ara_cli.artefact_autofix.determine_artefact_type_and_class")
|
|
368
|
+
@patch("ara_cli.artefact_autofix.read_artefact", return_value="original text")
|
|
369
|
+
def test_apply_autofix_llm_exception(
|
|
370
|
+
mock_read,
|
|
371
|
+
mock_determine,
|
|
372
|
+
mock_run_agent,
|
|
373
|
+
capsys,
|
|
374
|
+
mock_artefact_type,
|
|
375
|
+
mock_artefact_class,
|
|
376
|
+
mock_classified_artefact_info,
|
|
377
|
+
):
|
|
378
|
+
"""Tests that an exception during an LLM fix is handled gracefully."""
|
|
379
|
+
mock_determine.return_value = (mock_artefact_type, mock_artefact_class)
|
|
380
|
+
reason = "Pydantic validation error"
|
|
381
|
+
|
|
382
|
+
result = apply_autofix(
|
|
383
|
+
"file.feature",
|
|
384
|
+
"feature",
|
|
385
|
+
reason,
|
|
386
|
+
deterministic=False,
|
|
387
|
+
non_deterministic=True,
|
|
388
|
+
classified_artefact_info=mock_classified_artefact_info,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
assert result is False
|
|
392
|
+
assert (
|
|
393
|
+
"LLM agent failed to fix artefact at file.feature: LLM failed"
|
|
394
|
+
in capsys.readouterr().out
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def test_fix_title_mismatch_success(mock_artefact_class):
|
|
399
|
+
artefact_text = "Feature: wrong title\nSome other content"
|
|
400
|
+
file_path = "path/to/correct_title.feature"
|
|
401
|
+
|
|
402
|
+
expected_text = "Feature: correct title\nSome other content"
|
|
403
|
+
|
|
404
|
+
result = fix_title_mismatch(file_path, artefact_text, mock_artefact_class)
|
|
405
|
+
|
|
406
|
+
assert result == expected_text
|
|
407
|
+
mock_artefact_class._title_prefix.assert_called_once()
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def test_fix_title_mismatch_prefix_not_found(capsys, mock_artefact_class):
|
|
411
|
+
artefact_text = "No title prefix here"
|
|
412
|
+
file_path = "path/to/correct_title.feature"
|
|
413
|
+
|
|
414
|
+
result = fix_title_mismatch(file_path, artefact_text, mock_artefact_class)
|
|
415
|
+
|
|
416
|
+
assert result == artefact_text # Should return original text
|
|
417
|
+
assert "Warning: Title prefix 'Feature:' not found" in capsys.readouterr().out
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
@patch("pydantic_ai.Agent")
|
|
421
|
+
def test_run_agent_exception_handling(mock_agent_class):
|
|
422
|
+
mock_agent_instance = mock_agent_class.return_value
|
|
423
|
+
mock_agent_instance.run_sync.side_effect = Exception("Agent error")
|
|
424
|
+
with pytest.raises(Exception, match="Agent error"):
|
|
425
|
+
run_agent("prompt", MagicMock())
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
@patch("builtins.input", side_effect=["1"])
|
|
429
|
+
def test_ask_for_contribution_choice_valid(mock_input):
|
|
430
|
+
"""Tests selecting a valid choice."""
|
|
431
|
+
choices = ["choice1", "choice2"]
|
|
432
|
+
# This simpler call now works without causing a TypeError
|
|
433
|
+
result = ask_for_contribution_choice(choices)
|
|
434
|
+
assert result == "choice1"
|
|
435
|
+
|
|
436
|
+
@patch("builtins.input", side_effect=["99"])
|
|
437
|
+
def test_ask_for_contribution_choice_out_of_range(mock_input, capsys):
|
|
438
|
+
"""Tests selecting a choice that is out of range."""
|
|
439
|
+
choices = ["choice1", "choice2"]
|
|
440
|
+
result = ask_for_contribution_choice(choices)
|
|
441
|
+
assert result is None
|
|
442
|
+
assert "Invalid choice" in capsys.readouterr().out
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
@patch("builtins.input", side_effect=["not a number"])
|
|
446
|
+
def test_ask_for_contribution_choice_invalid_input(mock_input, capsys):
|
|
447
|
+
"""Tests providing non-numeric input."""
|
|
448
|
+
choices = ["choice1", "choice2"]
|
|
449
|
+
result = ask_for_contribution_choice(choices)
|
|
450
|
+
assert result is None
|
|
451
|
+
assert "Invalid input" in capsys.readouterr().out
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
@patch("builtins.input", side_effect=["feature my_feature_name"])
|
|
455
|
+
def test_ask_for_correct_contribution_valid(mock_input):
|
|
456
|
+
"""Tests providing valid '<classifier> <name>' input."""
|
|
457
|
+
name, classifier = ask_for_correct_contribution(("old_name", "feature"))
|
|
458
|
+
assert name == "my_feature_name"
|
|
459
|
+
assert classifier == "feature"
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
@patch("builtins.input", side_effect=[""])
|
|
463
|
+
def test_ask_for_correct_contribution_empty_input(mock_input):
|
|
464
|
+
"""Tests providing empty input."""
|
|
465
|
+
name, classifier = ask_for_correct_contribution()
|
|
466
|
+
assert name is None
|
|
467
|
+
assert classifier is None
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
@patch("builtins.input", side_effect=["invalid-one-word-input"])
|
|
471
|
+
def test_ask_for_correct_contribution_invalid_format(mock_input, capsys):
|
|
472
|
+
"""Tests providing input with the wrong format."""
|
|
473
|
+
# Fix: Use input that results in a single part after split()
|
|
474
|
+
name, classifier = ask_for_correct_contribution()
|
|
475
|
+
assert name is None
|
|
476
|
+
assert classifier is None
|
|
477
|
+
assert "Invalid input format" in capsys.readouterr().out
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def test_has_valid_contribution_true(mock_artefact_with_contribution):
|
|
481
|
+
"""Tests with a valid contribution object."""
|
|
482
|
+
# Fix: Check for truthiness, not strict boolean equality
|
|
483
|
+
assert _has_valid_contribution(mock_artefact_with_contribution)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def test_has_valid_contribution_false_no_contribution():
|
|
487
|
+
"""Tests when the artefact's contribution is None."""
|
|
488
|
+
mock_artefact = MagicMock(spec=Artefact)
|
|
489
|
+
mock_artefact.contribution = None
|
|
490
|
+
# Fix: Check for falsiness, not strict boolean equality
|
|
491
|
+
assert not _has_valid_contribution(mock_artefact)
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
@patch("ara_cli.artefact_autofix.FileClassifier")
|
|
495
|
+
@patch("ara_cli.artefact_autofix.extract_artefact_names_of_classifier")
|
|
496
|
+
@patch("ara_cli.artefact_autofix.find_closest_name_matches")
|
|
497
|
+
def test_set_closest_contribution_no_change_needed(
|
|
498
|
+
mock_find, mock_extract, mock_classifier, mock_artefact_with_contribution
|
|
499
|
+
):
|
|
500
|
+
"""Tests the case where the contribution name is already the best match."""
|
|
501
|
+
mock_find.return_value = ["some_artefact"] # Exact match is found
|
|
502
|
+
artefact, changed = set_closest_contribution(mock_artefact_with_contribution)
|
|
503
|
+
assert changed is False
|
|
504
|
+
assert artefact == mock_artefact_with_contribution
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
@patch("ara_cli.artefact_autofix.FileClassifier")
|
|
508
|
+
@patch("ara_cli.artefact_autofix.extract_artefact_names_of_classifier")
|
|
509
|
+
@patch("ara_cli.artefact_autofix.find_closest_name_matches", return_value=[])
|
|
510
|
+
@patch(
|
|
511
|
+
"ara_cli.artefact_autofix.ask_for_correct_contribution",
|
|
512
|
+
return_value=("new_name", "new_classifier"),
|
|
513
|
+
)
|
|
514
|
+
def test_set_closest_contribution_no_matches_user_provides(
|
|
515
|
+
mock_ask, mock_find, mock_extract, mock_classifier, mock_artefact_with_contribution
|
|
516
|
+
):
|
|
517
|
+
"""Tests when no matches are found and the user provides a new contribution."""
|
|
518
|
+
artefact, changed = set_closest_contribution(mock_artefact_with_contribution)
|
|
519
|
+
assert changed is True
|
|
520
|
+
assert artefact.contribution.artefact_name == "new_name"
|
|
521
|
+
assert artefact.contribution.classifier == "new_classifier"
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
@patch("ara_cli.artefact_autofix.set_closest_contribution")
|
|
525
|
+
@patch("ara_cli.artefact_autofix.FileClassifier")
|
|
526
|
+
def test_fix_contribution(
|
|
527
|
+
mock_file_classifier, mock_set, mock_artefact_with_contribution
|
|
528
|
+
):
|
|
529
|
+
"""Tests the fix_contribution wrapper function."""
|
|
530
|
+
# Arrange
|
|
531
|
+
mock_artefact_class = MagicMock()
|
|
532
|
+
mock_artefact_class.deserialize.return_value = mock_artefact_with_contribution
|
|
533
|
+
mock_set.return_value = (mock_artefact_with_contribution, True)
|
|
534
|
+
|
|
535
|
+
# Act
|
|
536
|
+
result = fix_contribution(
|
|
537
|
+
file_path="dummy.path",
|
|
538
|
+
artefact_text="original text",
|
|
539
|
+
artefact_class=mock_artefact_class,
|
|
540
|
+
classified_artefact_info={},
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
# Assert
|
|
544
|
+
assert result == "serialized artefact text"
|
|
545
|
+
mock_artefact_class.deserialize.assert_called_once_with("original text")
|
|
546
|
+
mock_set.assert_called_once_with(mock_artefact_with_contribution)
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
@patch("ara_cli.artefact_autofix.FileClassifier")
|
|
550
|
+
@patch("ara_cli.artefact_autofix.check_file")
|
|
551
|
+
@patch("ara_cli.artefact_autofix.write_corrected_artefact")
|
|
552
|
+
@patch("ara_cli.artefact_autofix.fix_contribution", return_value="fixed text")
|
|
553
|
+
@patch("ara_cli.artefact_autofix.determine_artefact_type_and_class")
|
|
554
|
+
@patch("ara_cli.artefact_autofix.read_artefact", return_value="original text")
|
|
555
|
+
def test_apply_autofix_for_contribution_mismatch(
|
|
556
|
+
mock_read,
|
|
557
|
+
mock_determine,
|
|
558
|
+
mock_fix_contribution,
|
|
559
|
+
mock_write,
|
|
560
|
+
mock_check_file,
|
|
561
|
+
mock_classifier,
|
|
562
|
+
mock_artefact_type,
|
|
563
|
+
mock_artefact_class,
|
|
564
|
+
mock_classified_artefact_info,
|
|
565
|
+
):
|
|
566
|
+
"""Tests the deterministic fix for 'Invalid Contribution Reference'."""
|
|
567
|
+
mock_determine.return_value = (mock_artefact_type, mock_artefact_class)
|
|
568
|
+
mock_check_file.side_effect = [
|
|
569
|
+
(False, "Invalid Contribution Reference"),
|
|
570
|
+
(True, ""),
|
|
571
|
+
]
|
|
572
|
+
|
|
573
|
+
result = apply_autofix(
|
|
574
|
+
file_path="file.feature",
|
|
575
|
+
classifier="feature",
|
|
576
|
+
reason="Invalid Contribution Reference",
|
|
577
|
+
classified_artefact_info=mock_classified_artefact_info,
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
assert result is True
|
|
581
|
+
mock_fix_contribution.assert_called_once()
|
|
582
|
+
mock_write.assert_called_once_with("file.feature", "fixed text")
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
@patch("ara_cli.artefact_autofix.check_file")
|
|
586
|
+
@patch("ara_cli.artefact_autofix.write_corrected_artefact")
|
|
587
|
+
@patch("ara_cli.artefact_autofix.fix_title_mismatch", return_value="original text")
|
|
588
|
+
@patch("ara_cli.artefact_autofix.determine_artefact_type_and_class")
|
|
589
|
+
@patch("ara_cli.artefact_autofix.read_artefact", return_value="original text")
|
|
590
|
+
def test_apply_autofix_stops_if_no_alteration(
|
|
591
|
+
mock_read,
|
|
592
|
+
mock_determine,
|
|
593
|
+
mock_fix_title,
|
|
594
|
+
mock_write,
|
|
595
|
+
mock_check_file,
|
|
596
|
+
capsys,
|
|
597
|
+
mock_artefact_type,
|
|
598
|
+
mock_artefact_class,
|
|
599
|
+
mock_classified_artefact_info,
|
|
600
|
+
):
|
|
601
|
+
"""Tests that the loop stops if a fix attempt does not change the file content."""
|
|
602
|
+
mock_determine.return_value = (mock_artefact_type, mock_artefact_class)
|
|
603
|
+
mock_check_file.return_value = (False, "Filename-Title Mismatch")
|
|
604
|
+
|
|
605
|
+
result = apply_autofix(
|
|
606
|
+
file_path="file.feature",
|
|
607
|
+
classifier="feature",
|
|
608
|
+
reason="any",
|
|
609
|
+
classified_artefact_info=mock_classified_artefact_info,
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
assert result is False
|
|
613
|
+
mock_fix_title.assert_called_once()
|
|
614
|
+
mock_write.assert_not_called()
|
|
615
|
+
assert (
|
|
616
|
+
"Fixing attempt did not alter the file. Stopping to prevent infinite loop."
|
|
617
|
+
in capsys.readouterr().out
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
@patch("ara_cli.artefact_autofix.check_file")
|
|
622
|
+
@patch("ara_cli.artefact_autofix.determine_artefact_type_and_class")
|
|
623
|
+
def test_apply_autofix_single_pass(
|
|
624
|
+
mock_determine,
|
|
625
|
+
mock_check_file,
|
|
626
|
+
capsys,
|
|
627
|
+
mock_artefact_type,
|
|
628
|
+
mock_artefact_class,
|
|
629
|
+
mock_classified_artefact_info,
|
|
630
|
+
):
|
|
631
|
+
"""Tests that single_pass=True runs the loop only once."""
|
|
632
|
+
mock_determine.return_value = (mock_artefact_type, mock_artefact_class)
|
|
633
|
+
# Simulate a failure that won't be fixed to ensure the loop doesn't repeat
|
|
634
|
+
mock_check_file.return_value = (False, "Some unfixable error")
|
|
635
|
+
|
|
636
|
+
apply_autofix(
|
|
637
|
+
file_path="file.feature",
|
|
638
|
+
classifier="feature",
|
|
639
|
+
reason="any",
|
|
640
|
+
single_pass=True,
|
|
641
|
+
deterministic=False, # Disable fixes
|
|
642
|
+
non_deterministic=False,
|
|
643
|
+
classified_artefact_info=mock_classified_artefact_info,
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
output = capsys.readouterr().out
|
|
647
|
+
assert "Single-pass mode enabled" in output
|
|
648
|
+
assert "Attempt 1/1" in output
|
|
649
|
+
assert "Attempt 2/1" not in output
|
|
650
|
+
mock_check_file.assert_called_once()
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
@patch("ara_cli.artefact_autofix._update_rule")
|
|
654
|
+
@patch("ara_cli.artefact_autofix.populate_classified_artefact_info")
|
|
655
|
+
def test_fix_rule_with_rule(mock_populate, mock_update_rule, mock_artefact, mock_contribution, capsys):
|
|
656
|
+
# Contribution has a rule
|
|
657
|
+
artefact_class = MagicMock()
|
|
658
|
+
artefact_class.deserialize.return_value = mock_artefact
|
|
659
|
+
mock_populate.return_value = {"info": "dummy"}
|
|
660
|
+
|
|
661
|
+
result = fix_rule(
|
|
662
|
+
file_path="dummy.feature",
|
|
663
|
+
artefact_text="text",
|
|
664
|
+
artefact_class=artefact_class,
|
|
665
|
+
classified_artefact_info={},
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
# deserialize called
|
|
669
|
+
artefact_class.deserialize.assert_called_once_with("text")
|
|
670
|
+
# _update_rule called with correct args
|
|
671
|
+
mock_update_rule.assert_called_once_with(
|
|
672
|
+
artefact=mock_artefact,
|
|
673
|
+
name="parent_name",
|
|
674
|
+
classifier="feature",
|
|
675
|
+
classified_file_info={"info": "dummy"},
|
|
676
|
+
delete_if_not_found=True,
|
|
677
|
+
)
|
|
678
|
+
# Feedback message contains rule
|
|
679
|
+
assert "with rule" in capsys.readouterr().out
|
|
680
|
+
# Result is the serialized text
|
|
681
|
+
assert result == "serialized-text"
|
|
682
|
+
|
|
683
|
+
@patch("ara_cli.artefact_autofix._update_rule")
|
|
684
|
+
@patch("ara_cli.artefact_autofix.populate_classified_artefact_info")
|
|
685
|
+
def test_fix_rule_without_rule(mock_populate, mock_update_rule, mock_artefact, mock_contribution, capsys):
|
|
686
|
+
# Contribution rule becomes None after update
|
|
687
|
+
mock_contribution.rule = None
|
|
688
|
+
artefact_class = MagicMock()
|
|
689
|
+
artefact_class.deserialize.return_value = mock_artefact
|
|
690
|
+
mock_populate.return_value = {"info": "dummy"}
|
|
691
|
+
|
|
692
|
+
result = fix_rule(
|
|
693
|
+
file_path="dummy.feature",
|
|
694
|
+
artefact_text="text",
|
|
695
|
+
artefact_class=artefact_class,
|
|
696
|
+
classified_artefact_info={},
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
# Feedback message says "without a rule"
|
|
700
|
+
assert "without a rule" in capsys.readouterr().out
|
|
701
|
+
assert result == "serialized-text"
|
|
702
|
+
|
|
703
|
+
@patch("ara_cli.artefact_autofix.populate_classified_artefact_info")
|
|
704
|
+
def test_fix_rule_contribution_none_raises(mock_populate):
|
|
705
|
+
# artefact.contribution is None: should assert
|
|
706
|
+
artefact = MagicMock()
|
|
707
|
+
artefact.contribution = None
|
|
708
|
+
artefact_class = MagicMock()
|
|
709
|
+
artefact_class.deserialize.return_value = artefact
|
|
710
|
+
mock_populate.return_value = {}
|
|
711
|
+
|
|
712
|
+
with pytest.raises(AssertionError):
|
|
713
|
+
fix_rule(
|
|
714
|
+
file_path="dummy.feature",
|
|
715
|
+
artefact_text="stuff",
|
|
716
|
+
artefact_class=artefact_class,
|
|
717
|
+
classified_artefact_info={},
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
def test_populate_classified_artefact_info_force_true():
|
|
721
|
+
"""Test populate_classified_artefact_info with force=True"""
|
|
722
|
+
with patch('ara_cli.artefact_autofix.FileClassifier') as mock_classifier:
|
|
723
|
+
mock_instance = mock_classifier.return_value
|
|
724
|
+
mock_instance.classify_files.return_value = {"new": "data"}
|
|
725
|
+
|
|
726
|
+
result = populate_classified_artefact_info({"old": "data"}, force=True)
|
|
727
|
+
|
|
728
|
+
assert result == {"new": "data"}
|
|
729
|
+
mock_classifier.assert_called_once()
|
|
730
|
+
|
|
731
|
+
def test_populate_classified_artefact_info_none_input():
|
|
732
|
+
"""Test populate_classified_artefact_info with None input"""
|
|
733
|
+
with patch('ara_cli.artefact_autofix.FileClassifier') as mock_classifier:
|
|
734
|
+
mock_instance = mock_classifier.return_value
|
|
735
|
+
mock_instance.classify_files.return_value = {"classified": "data"}
|
|
736
|
+
|
|
737
|
+
result = populate_classified_artefact_info(None)
|
|
738
|
+
|
|
739
|
+
assert result == {"classified": "data"}
|
|
740
|
+
mock_classifier.assert_called_once()
|
|
741
|
+
|
|
742
|
+
def test_parse_report_empty_content():
|
|
743
|
+
"""Test parse_report with empty content"""
|
|
744
|
+
assert parse_report("") == {}
|
|
745
|
+
|
|
746
|
+
def test_parse_report_missing_reason():
|
|
747
|
+
"""Test parse_report with missing reason in issue line"""
|
|
748
|
+
content = "# Artefact Check Report\n\n## feature\n- `file.feature`\n"
|
|
749
|
+
expected = {"feature": [("file.feature", "")]}
|
|
750
|
+
assert parse_report(content) == expected
|
|
751
|
+
|
|
752
|
+
def test_parse_report_multiple_classifiers():
|
|
753
|
+
"""Test parse_report with multiple classifiers"""
|
|
754
|
+
content = (
|
|
755
|
+
"# Artefact Check Report\n\n"
|
|
756
|
+
"## feature\n- `file1.feature`: reason1\n\n"
|
|
757
|
+
"## task\n- `file2.task`: reason2\n"
|
|
758
|
+
)
|
|
759
|
+
expected = {
|
|
760
|
+
"feature": [("file1.feature", "reason1")],
|
|
761
|
+
"task": [("file2.task", "reason2")]
|
|
762
|
+
}
|
|
763
|
+
assert parse_report(content) == expected
|
|
764
|
+
|
|
765
|
+
def test_construct_prompt_non_task_artefact():
|
|
766
|
+
"""Test construct_prompt for non-task artefact types"""
|
|
767
|
+
prompt = construct_prompt(ArtefactType.feature, "some reason", "file.feature", "text")
|
|
768
|
+
assert "For task artefacts" not in prompt
|
|
769
|
+
assert "some reason" in prompt
|
|
770
|
+
assert "file.feature" in prompt
|
|
771
|
+
|
|
772
|
+
@patch("pydantic_ai.Agent")
|
|
773
|
+
def test_run_agent_success(mock_agent_class):
|
|
774
|
+
"""Test successful run_agent execution"""
|
|
775
|
+
mock_agent_instance = mock_agent_class.return_value
|
|
776
|
+
mock_result = MagicMock()
|
|
777
|
+
mock_result.output = "agent output"
|
|
778
|
+
mock_agent_instance.run_sync.return_value = mock_result
|
|
779
|
+
|
|
780
|
+
result = run_agent("test prompt", MagicMock())
|
|
781
|
+
|
|
782
|
+
assert result == "agent output"
|
|
783
|
+
mock_agent_class.assert_called_once()
|
|
784
|
+
|
|
785
|
+
def test_write_corrected_artefact_with_print(capsys):
|
|
786
|
+
"""Test write_corrected_artefact prints success message"""
|
|
787
|
+
with patch("builtins.open", mock_open()) as m:
|
|
788
|
+
write_corrected_artefact("file.feature", "corrected content")
|
|
789
|
+
|
|
790
|
+
captured = capsys.readouterr()
|
|
791
|
+
assert "Fixed artefact at file.feature" in captured.out
|
|
792
|
+
|
|
793
|
+
# Tests for the new scenario placeholder functions
|
|
794
|
+
def test_extract_scenario_block():
|
|
795
|
+
"""Test _extract_scenario_block function"""
|
|
796
|
+
lines = [
|
|
797
|
+
"Feature: Test",
|
|
798
|
+
"Scenario: Test scenario",
|
|
799
|
+
" Given something",
|
|
800
|
+
" When something",
|
|
801
|
+
"Scenario: Another scenario"
|
|
802
|
+
]
|
|
803
|
+
|
|
804
|
+
scenario_lines, next_index = _extract_scenario_block(lines, 1)
|
|
805
|
+
|
|
806
|
+
assert len(scenario_lines) == 3
|
|
807
|
+
assert scenario_lines[0] == "Scenario: Test scenario"
|
|
808
|
+
assert next_index == 4
|
|
809
|
+
|
|
810
|
+
def test_is_scenario_boundary():
|
|
811
|
+
"""Test _is_scenario_boundary function"""
|
|
812
|
+
assert _is_scenario_boundary("Scenario: test")
|
|
813
|
+
assert _is_scenario_boundary("Scenario Outline: test")
|
|
814
|
+
assert _is_scenario_boundary("Background:")
|
|
815
|
+
assert _is_scenario_boundary("Feature: test")
|
|
816
|
+
assert not _is_scenario_boundary("Given something")
|
|
817
|
+
|
|
818
|
+
def test_process_scenario_block_no_placeholders():
|
|
819
|
+
"""Test _process_scenario_block with no placeholders"""
|
|
820
|
+
scenario_lines = [
|
|
821
|
+
" Scenario: Test",
|
|
822
|
+
" Given something",
|
|
823
|
+
" When something"
|
|
824
|
+
]
|
|
825
|
+
|
|
826
|
+
result = _process_scenario_block(scenario_lines)
|
|
827
|
+
|
|
828
|
+
assert result == scenario_lines
|
|
829
|
+
|
|
830
|
+
def test_process_scenario_block_with_placeholders():
|
|
831
|
+
"""Test _process_scenario_block with placeholders"""
|
|
832
|
+
scenario_lines = [
|
|
833
|
+
" Scenario: Test",
|
|
834
|
+
" Given something with <placeholder>",
|
|
835
|
+
" When something with <another>"
|
|
836
|
+
]
|
|
837
|
+
|
|
838
|
+
result = _process_scenario_block(scenario_lines)
|
|
839
|
+
|
|
840
|
+
assert "Scenario Outline:" in result[0]
|
|
841
|
+
assert "Examples:" in result[-3]
|
|
842
|
+
|
|
843
|
+
def test_get_line_indentation():
|
|
844
|
+
"""Test _get_line_indentation function"""
|
|
845
|
+
assert _get_line_indentation(" indented line") == " "
|
|
846
|
+
assert _get_line_indentation("no indent") == ""
|
|
847
|
+
assert _get_line_indentation(" two spaces") == " "
|
|
848
|
+
|
|
849
|
+
def test_extract_placeholders_from_scenario():
|
|
850
|
+
"""Test _extract_placeholders_from_scenario function"""
|
|
851
|
+
step_lines = [
|
|
852
|
+
" Given something with <placeholder1>",
|
|
853
|
+
" When something with <placeholder2>",
|
|
854
|
+
" Then something normal"
|
|
855
|
+
]
|
|
856
|
+
|
|
857
|
+
placeholders = _extract_placeholders_from_scenario(step_lines)
|
|
858
|
+
|
|
859
|
+
assert placeholders == {"placeholder1", "placeholder2"}
|
|
860
|
+
|
|
861
|
+
def test_extract_placeholders_with_docstring():
|
|
862
|
+
"""Test _extract_placeholders_from_scenario ignoring docstrings"""
|
|
863
|
+
step_lines = [
|
|
864
|
+
" Given something with <placeholder1>",
|
|
865
|
+
' """',
|
|
866
|
+
" Some docstring with <not_a_placeholder>",
|
|
867
|
+
' """',
|
|
868
|
+
" When something with <placeholder2>"
|
|
869
|
+
]
|
|
870
|
+
|
|
871
|
+
placeholders = _extract_placeholders_from_scenario(step_lines)
|
|
872
|
+
|
|
873
|
+
assert placeholders == {"placeholder1", "placeholder2"}
|
|
874
|
+
|
|
875
|
+
def test_update_docstring_state():
|
|
876
|
+
"""Test _update_docstring_state function"""
|
|
877
|
+
assert _update_docstring_state('"""', False) == True
|
|
878
|
+
assert _update_docstring_state('"""', True) == False
|
|
879
|
+
assert _update_docstring_state('normal line', False) == False
|
|
880
|
+
assert _update_docstring_state('normal line', True) == True
|
|
881
|
+
|
|
882
|
+
def test_convert_to_scenario_outline():
|
|
883
|
+
"""Test _convert_to_scenario_outline function"""
|
|
884
|
+
scenario_lines = [
|
|
885
|
+
" Scenario: Test scenario",
|
|
886
|
+
" Given something",
|
|
887
|
+
" When something"
|
|
888
|
+
]
|
|
889
|
+
placeholders = {"placeholder1", "placeholder2"}
|
|
890
|
+
|
|
891
|
+
result = _convert_to_scenario_outline(scenario_lines, placeholders, " ")
|
|
892
|
+
|
|
893
|
+
assert "Scenario Outline: Test scenario" in result[0]
|
|
894
|
+
assert "Examples:" in result[-3]
|
|
895
|
+
|
|
896
|
+
def test_create_examples_table():
|
|
897
|
+
"""Test _create_examples_table function"""
|
|
898
|
+
placeholders = {"param1", "param2"}
|
|
899
|
+
|
|
900
|
+
result = _create_examples_table(placeholders, " ")
|
|
901
|
+
|
|
902
|
+
assert len(result) == 3
|
|
903
|
+
assert "Examples:" in result[0]
|
|
904
|
+
assert "| param1 | param2 |" in result[1]
|
|
905
|
+
assert "<param1_value>" in result[2]
|
|
906
|
+
|
|
907
|
+
def test_fix_scenario_placeholder_mismatch_no_scenarios():
|
|
908
|
+
"""Test fix_scenario_placeholder_mismatch with no scenarios"""
|
|
909
|
+
artefact_text = "Feature: Test\nBackground:\n Given something"
|
|
910
|
+
|
|
911
|
+
result = fix_scenario_placeholder_mismatch("file.feature", artefact_text, MagicMock())
|
|
912
|
+
|
|
913
|
+
assert result == artefact_text
|
|
914
|
+
|
|
915
|
+
def test_fix_scenario_placeholder_mismatch_with_placeholders():
|
|
916
|
+
"""Test fix_scenario_placeholder_mismatch converting to outline"""
|
|
917
|
+
artefact_text = """Feature: Test
|
|
918
|
+
Scenario: Test scenario
|
|
919
|
+
Given something with <placeholder>
|
|
920
|
+
When something happens"""
|
|
921
|
+
|
|
922
|
+
result = fix_scenario_placeholder_mismatch("file.feature", artefact_text, MagicMock())
|
|
923
|
+
|
|
924
|
+
assert "Scenario Outline:" in result
|
|
925
|
+
assert "Examples:" in result
|
|
926
|
+
assert "<placeholder>" in result
|
|
927
|
+
|
|
928
|
+
def test_should_skip_issue_non_deterministic_false():
|
|
929
|
+
"""Test should_skip_issue when non_deterministic is False"""
|
|
930
|
+
result = should_skip_issue(None, True, False, "file.txt")
|
|
931
|
+
assert result == True
|
|
932
|
+
|
|
933
|
+
def test_should_skip_issue_deterministic_false():
|
|
934
|
+
"""Test should_skip_issue when deterministic is False"""
|
|
935
|
+
result = should_skip_issue("some_issue", False, True, "file.txt")
|
|
936
|
+
assert result == True
|
|
937
|
+
|
|
938
|
+
def test_should_skip_issue_no_skip():
|
|
939
|
+
"""Test should_skip_issue when no skip is needed"""
|
|
940
|
+
result = should_skip_issue("some_issue", True, True, "file.txt")
|
|
941
|
+
assert result == False
|
|
942
|
+
|
|
943
|
+
def test_determine_attempt_count_single_pass():
|
|
944
|
+
"""Test determine_attempt_count with single_pass=True"""
|
|
945
|
+
result = determine_attempt_count(True, "file.txt")
|
|
946
|
+
assert result == 1
|
|
947
|
+
|
|
948
|
+
def test_determine_attempt_count_multiple_pass():
|
|
949
|
+
"""Test determine_attempt_count with single_pass=False"""
|
|
950
|
+
result = determine_attempt_count(False, "file.txt")
|
|
951
|
+
assert result == 3
|
|
952
|
+
|
|
953
|
+
def test_apply_deterministic_fix_with_issue():
|
|
954
|
+
"""Test apply_deterministic_fix when deterministic issue exists"""
|
|
955
|
+
mock_fix_function = MagicMock(return_value="fixed_text")
|
|
956
|
+
deterministic_markers = {"test_issue": mock_fix_function}
|
|
957
|
+
|
|
958
|
+
result = apply_deterministic_fix(
|
|
959
|
+
True, "test_issue", "file.txt", "original", MagicMock(),
|
|
960
|
+
{}, deterministic_markers, "corrected"
|
|
961
|
+
)
|
|
962
|
+
|
|
963
|
+
assert result == "fixed_text"
|
|
964
|
+
mock_fix_function.assert_called_once()
|
|
965
|
+
|
|
966
|
+
def test_apply_deterministic_fix_no_issue():
|
|
967
|
+
"""Test apply_deterministic_fix when no deterministic issue"""
|
|
968
|
+
result = apply_deterministic_fix(
|
|
969
|
+
True, None, "file.txt", "original", MagicMock(),
|
|
970
|
+
{}, {}, "corrected"
|
|
971
|
+
)
|
|
972
|
+
|
|
973
|
+
assert result == "corrected"
|
|
974
|
+
|
|
975
|
+
@patch('ara_cli.artefact_autofix.construct_prompt')
|
|
976
|
+
@patch('ara_cli.artefact_autofix.run_agent')
|
|
977
|
+
def test_apply_non_deterministic_fix_success(mock_run_agent, mock_construct_prompt):
|
|
978
|
+
"""Test apply_non_deterministic_fix successful execution"""
|
|
979
|
+
mock_construct_prompt.return_value = "test prompt"
|
|
980
|
+
mock_artefact = MagicMock()
|
|
981
|
+
mock_artefact.serialize.return_value = "fixed_text"
|
|
982
|
+
mock_run_agent.return_value = mock_artefact
|
|
983
|
+
|
|
984
|
+
result = apply_non_deterministic_fix(
|
|
985
|
+
True, None, "corrected", MagicMock(), "reason",
|
|
986
|
+
"file.txt", "original", MagicMock()
|
|
987
|
+
)
|
|
988
|
+
|
|
989
|
+
assert result == "fixed_text"
|
|
990
|
+
|
|
991
|
+
def test_apply_non_deterministic_fix_with_deterministic_issue():
|
|
992
|
+
"""Test apply_non_deterministic_fix when deterministic issue exists"""
|
|
993
|
+
result = apply_non_deterministic_fix(
|
|
994
|
+
True, "some_issue", "corrected", MagicMock(), "reason",
|
|
995
|
+
"file.txt", "original", MagicMock()
|
|
996
|
+
)
|
|
997
|
+
|
|
998
|
+
assert result == "corrected"
|
|
999
|
+
|
|
1000
|
+
@patch('ara_cli.artefact_autofix.construct_prompt')
|
|
1001
|
+
@patch('ara_cli.artefact_autofix.run_agent', side_effect=Exception("LLM Error"))
|
|
1002
|
+
def test_apply_non_deterministic_fix_exception(mock_run_agent, mock_construct_prompt, capsys):
|
|
1003
|
+
"""Test apply_non_deterministic_fix with exception"""
|
|
1004
|
+
mock_construct_prompt.return_value = "test prompt"
|
|
1005
|
+
|
|
1006
|
+
result = apply_non_deterministic_fix(
|
|
1007
|
+
True, None, "corrected", MagicMock(), "reason",
|
|
1008
|
+
"file.txt", "original", MagicMock()
|
|
1009
|
+
)
|
|
1010
|
+
|
|
1011
|
+
assert result is None
|
|
1012
|
+
assert "LLM agent failed" in capsys.readouterr().out
|
|
1013
|
+
|
|
1014
|
+
@patch('ara_cli.artefact_autofix.check_file')
|
|
1015
|
+
@patch('ara_cli.artefact_autofix.read_artefact')
|
|
1016
|
+
@patch('ara_cli.artefact_autofix.write_corrected_artefact')
|
|
1017
|
+
@patch('ara_cli.artefact_autofix.populate_classified_artefact_info')
|
|
1018
|
+
@patch('ara_cli.artefact_autofix.should_skip_issue', return_value=False)
|
|
1019
|
+
@patch('ara_cli.artefact_autofix.apply_deterministic_fix')
|
|
1020
|
+
@patch('ara_cli.artefact_autofix.apply_non_deterministic_fix')
|
|
1021
|
+
def test_attempt_autofix_loop_max_attempts_reached(
|
|
1022
|
+
mock_apply_non_det, mock_apply_det, mock_should_skip, mock_populate,
|
|
1023
|
+
mock_write, mock_read, mock_check_file, capsys
|
|
1024
|
+
):
|
|
1025
|
+
"""Test attempt_autofix_loop when max attempts are reached"""
|
|
1026
|
+
mock_check_file.return_value = (False, "persistent error")
|
|
1027
|
+
mock_read.return_value = "original text"
|
|
1028
|
+
mock_apply_det.return_value = "modified text" # Ensure text is modified
|
|
1029
|
+
mock_apply_non_det.return_value = "modified text" # Ensure text is modified
|
|
1030
|
+
|
|
1031
|
+
result = attempt_autofix_loop(
|
|
1032
|
+
"file.txt", MagicMock(), MagicMock(), {}, 2, True, True, {}
|
|
1033
|
+
)
|
|
1034
|
+
|
|
1035
|
+
assert result == False
|
|
1036
|
+
assert "Failed to fix file.txt after 2 attempts" in capsys.readouterr().out
|
|
1037
|
+
|
|
1038
|
+
@patch('ara_cli.artefact_autofix.check_file')
|
|
1039
|
+
def test_attempt_autofix_loop_already_valid(mock_check_file, capsys):
|
|
1040
|
+
"""Test attempt_autofix_loop when file is already valid"""
|
|
1041
|
+
mock_check_file.return_value = (True, "")
|
|
1042
|
+
|
|
1043
|
+
result = attempt_autofix_loop(
|
|
1044
|
+
"file.txt", MagicMock(), MagicMock(), {}, 3, True, True, {}
|
|
1045
|
+
)
|
|
1046
|
+
|
|
1047
|
+
assert result == True
|
|
1048
|
+
assert "is now valid" in capsys.readouterr().out
|
|
1049
|
+
|
|
1050
|
+
@patch('ara_cli.artefact_autofix.check_file')
|
|
1051
|
+
@patch('ara_cli.artefact_autofix.read_artefact', return_value=None)
|
|
1052
|
+
def test_attempt_autofix_loop_read_fails(mock_read, mock_check_file):
|
|
1053
|
+
"""Test attempt_autofix_loop when reading artefact fails"""
|
|
1054
|
+
mock_check_file.return_value = (False, "some error")
|
|
1055
|
+
|
|
1056
|
+
result = attempt_autofix_loop(
|
|
1057
|
+
"file.txt", MagicMock(), MagicMock(), {}, 3, True, True, {}
|
|
1058
|
+
)
|
|
1059
|
+
|
|
1060
|
+
assert result == False
|