ara-cli 0.1.10.0__py3-none-any.whl → 0.1.13.3__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.
- ara_cli/__init__.py +51 -6
- ara_cli/__main__.py +270 -103
- ara_cli/ara_command_action.py +106 -63
- ara_cli/ara_config.py +187 -128
- 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/config.py +221 -0
- ara_cli/ara_subcommands/convert.py +43 -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.py +41 -0
- ara_cli/ara_subcommands/fetch_agents.py +22 -0
- ara_cli/ara_subcommands/fetch_scripts.py +19 -0
- ara_cli/ara_subcommands/fetch_templates.py +19 -0
- ara_cli/ara_subcommands/list.py +139 -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 +154 -63
- ara_cli/artefact_converter.py +256 -0
- ara_cli/artefact_models/artefact_model.py +106 -25
- ara_cli/artefact_models/artefact_templates.py +20 -10
- ara_cli/artefact_models/epic_artefact_model.py +11 -2
- ara_cli/artefact_models/feature_artefact_model.py +31 -1
- ara_cli/artefact_models/userstory_artefact_model.py +15 -3
- ara_cli/artefact_scan.py +2 -2
- ara_cli/chat.py +283 -80
- ara_cli/chat_agent/__init__.py +0 -0
- ara_cli/chat_agent/agent_process_manager.py +155 -0
- ara_cli/chat_script_runner/__init__.py +0 -0
- ara_cli/chat_script_runner/script_completer.py +23 -0
- ara_cli/chat_script_runner/script_finder.py +41 -0
- ara_cli/chat_script_runner/script_lister.py +36 -0
- ara_cli/chat_script_runner/script_runner.py +36 -0
- ara_cli/chat_web_search/__init__.py +0 -0
- ara_cli/chat_web_search/web_search.py +263 -0
- ara_cli/commands/agent_run_command.py +98 -0
- ara_cli/commands/fetch_agents_command.py +106 -0
- ara_cli/commands/fetch_scripts_command.py +43 -0
- ara_cli/commands/fetch_templates_command.py +39 -0
- ara_cli/commands/fetch_templates_commands.py +39 -0
- ara_cli/commands/list_agents_command.py +39 -0
- ara_cli/commands/read_command.py +17 -4
- ara_cli/completers.py +180 -0
- ara_cli/constants.py +2 -0
- ara_cli/directory_navigator.py +37 -4
- ara_cli/file_loaders/text_file_loader.py +2 -2
- ara_cli/global_file_lister.py +5 -15
- ara_cli/llm_utils.py +58 -0
- ara_cli/prompt_chat.py +20 -4
- ara_cli/prompt_extractor.py +199 -76
- ara_cli/prompt_handler.py +160 -59
- ara_cli/tag_extractor.py +38 -18
- ara_cli/template_loader.py +3 -2
- ara_cli/template_manager.py +52 -21
- ara_cli/templates/global-scripts/hello_global.py +1 -0
- ara_cli/templates/prompt-modules/commands/add_scenarios_for_new_behaviour.feature_creation_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/align_feature_with_implementation_changes.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/analyze_codebase_and_plan_tasks.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/choose_best_parent_artefact.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/create_tasks_from_artefact_content.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/create_tests_for_uncovered_modules.test_generation_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/derive_features_from_video_description.feature_creation_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/describe_agent_capabilities.agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/empty.commands.md +2 -12
- ara_cli/templates/prompt-modules/commands/execute_scoped_todos_in_task.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/explain_single_file_purpose.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/extract_file_information_bullets.interview_agent.commands.md +1 -0
- 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/fix_failing_behave_step_definitions.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/fix_failing_pytest_tests.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/general_instruction_policy.commands.md +47 -0
- ara_cli/templates/prompt-modules/commands/generate_and_fix_pytest_tests.test_generation_agent.commands.md +1 -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/templates/prompt-modules/commands/suggest_next_story_child_tasks.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/summarize_or_transcribe_media.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/update_feature_to_match_implementation.feature_creation_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/update_user_story_with_requirements.interview_agent.commands.md +1 -0
- ara_cli/version.py +1 -1
- {ara_cli-0.1.10.0.dist-info → ara_cli-0.1.13.3.dist-info}/METADATA +34 -1
- {ara_cli-0.1.10.0.dist-info → ara_cli-0.1.13.3.dist-info}/RECORD +123 -54
- tests/test_ara_command_action.py +31 -19
- tests/test_ara_config.py +177 -90
- tests/test_artefact_autofix.py +170 -97
- tests/test_artefact_autofix_integration.py +495 -0
- tests/test_artefact_converter.py +357 -0
- tests/test_artefact_extraction.py +564 -0
- tests/test_artefact_scan.py +1 -1
- tests/test_chat.py +162 -126
- tests/test_chat_givens_images.py +603 -0
- tests/test_chat_script_runner.py +454 -0
- tests/test_global_file_lister.py +1 -1
- tests/test_llm_utils.py +164 -0
- tests/test_prompt_chat.py +343 -0
- tests/test_prompt_extractor.py +683 -0
- tests/test_prompt_handler.py +12 -4
- tests/test_tag_extractor.py +19 -13
- tests/test_web_search.py +467 -0
- ara_cli/ara_command_parser.py +0 -605
- ara_cli/templates/prompt-modules/blueprints/complete_pytest_unittest.blueprint.md +0 -27
- ara_cli/templates/prompt-modules/blueprints/task_todo_list_implement_feature_BDD_way.blueprint.md +0 -30
- ara_cli/templates/prompt-modules/commands/artefact_classification.commands.md +0 -9
- ara_cli/templates/prompt-modules/commands/artefact_extension.commands.md +0 -17
- ara_cli/templates/prompt-modules/commands/artefact_formulation.commands.md +0 -14
- ara_cli/templates/prompt-modules/commands/behave_step_generation.commands.md +0 -102
- ara_cli/templates/prompt-modules/commands/code_generation_complex.commands.md +0 -20
- ara_cli/templates/prompt-modules/commands/code_generation_simple.commands.md +0 -13
- ara_cli/templates/prompt-modules/commands/error_fixing.commands.md +0 -20
- ara_cli/templates/prompt-modules/commands/feature_file_update.commands.md +0 -18
- ara_cli/templates/prompt-modules/commands/feature_formulation.commands.md +0 -43
- ara_cli/templates/prompt-modules/commands/js_code_generation_simple.commands.md +0 -13
- ara_cli/templates/prompt-modules/commands/refactoring.commands.md +0 -15
- ara_cli/templates/prompt-modules/commands/refactoring_analysis.commands.md +0 -9
- ara_cli/templates/prompt-modules/commands/reverse_engineer_feature_file.commands.md +0 -15
- ara_cli/templates/prompt-modules/commands/reverse_engineer_program_flow.commands.md +0 -19
- {ara_cli-0.1.10.0.dist-info → ara_cli-0.1.13.3.dist-info}/WHEEL +0 -0
- {ara_cli-0.1.10.0.dist-info → ara_cli-0.1.13.3.dist-info}/entry_points.txt +0 -0
- {ara_cli-0.1.10.0.dist-info → ara_cli-0.1.13.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for artefact_autofix.py - Integration scenarios
|
|
3
|
+
|
|
4
|
+
These tests extend the existing test_artefact_autofix.py to cover scenarios from:
|
|
5
|
+
- ara_autofix_command.feature
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
import os
|
|
10
|
+
import tempfile
|
|
11
|
+
from unittest.mock import patch, MagicMock, mock_open
|
|
12
|
+
from ara_cli.artefact_autofix import (
|
|
13
|
+
read_report_file,
|
|
14
|
+
parse_report,
|
|
15
|
+
apply_autofix,
|
|
16
|
+
read_artefact,
|
|
17
|
+
determine_artefact_type_and_class,
|
|
18
|
+
fix_title_mismatch,
|
|
19
|
+
fix_contribution,
|
|
20
|
+
fix_rule,
|
|
21
|
+
fix_scenario_placeholder_mismatch,
|
|
22
|
+
populate_classified_artefact_info,
|
|
23
|
+
should_skip_issue,
|
|
24
|
+
determine_attempt_count,
|
|
25
|
+
apply_deterministic_fix,
|
|
26
|
+
apply_non_deterministic_fix,
|
|
27
|
+
attempt_autofix_loop,
|
|
28
|
+
set_closest_contribution,
|
|
29
|
+
ask_for_contribution_choice,
|
|
30
|
+
ask_for_correct_contribution,
|
|
31
|
+
_extract_scenario_block,
|
|
32
|
+
_convert_to_scenario_outline,
|
|
33
|
+
_create_examples_table,
|
|
34
|
+
_extract_placeholders_from_scenario,
|
|
35
|
+
)
|
|
36
|
+
from ara_cli.artefact_models.artefact_model import Artefact, Contribution
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# =============================================================================
|
|
40
|
+
# Tests for single-pass mode (from ara_autofix_command.feature)
|
|
41
|
+
# =============================================================================
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TestSinglePassMode:
|
|
45
|
+
"""Tests for single-pass autofix mode."""
|
|
46
|
+
|
|
47
|
+
@patch("ara_cli.artefact_autofix.check_file")
|
|
48
|
+
@patch("ara_cli.artefact_autofix.determine_artefact_type_and_class")
|
|
49
|
+
def test_single_pass_runs_only_once(self, mock_determine, mock_check_file, capsys):
|
|
50
|
+
"""Single-pass mode runs the loop only once."""
|
|
51
|
+
mock_artefact_type = MagicMock()
|
|
52
|
+
mock_artefact_type.value = "feature"
|
|
53
|
+
mock_artefact_class = MagicMock()
|
|
54
|
+
mock_determine.return_value = (mock_artefact_type, mock_artefact_class)
|
|
55
|
+
mock_check_file.return_value = (False, "Some unfixable error")
|
|
56
|
+
|
|
57
|
+
apply_autofix(
|
|
58
|
+
file_path="file.feature",
|
|
59
|
+
classifier="feature",
|
|
60
|
+
reason="any",
|
|
61
|
+
single_pass=True,
|
|
62
|
+
deterministic=False,
|
|
63
|
+
non_deterministic=False,
|
|
64
|
+
classified_artefact_info={},
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
output = capsys.readouterr().out
|
|
68
|
+
assert "Single-pass mode enabled" in output
|
|
69
|
+
assert "1/1" in output
|
|
70
|
+
mock_check_file.assert_called_once()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# =============================================================================
|
|
74
|
+
# Tests for deterministic vs non-deterministic flags
|
|
75
|
+
# =============================================================================
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class TestDeterministicFlags:
|
|
79
|
+
"""Tests for deterministic and non-deterministic flag behavior."""
|
|
80
|
+
|
|
81
|
+
@patch("ara_cli.artefact_autofix.run_agent")
|
|
82
|
+
@patch("ara_cli.artefact_autofix.fix_title_mismatch", return_value="fixed")
|
|
83
|
+
@patch("ara_cli.artefact_autofix.check_file")
|
|
84
|
+
@patch("ara_cli.artefact_autofix.write_corrected_artefact")
|
|
85
|
+
@patch("ara_cli.artefact_autofix.determine_artefact_type_and_class")
|
|
86
|
+
@patch("ara_cli.artefact_autofix.read_artefact", return_value="original")
|
|
87
|
+
@patch("ara_cli.artefact_autofix.FileClassifier")
|
|
88
|
+
def test_deterministic_only_skips_llm(
|
|
89
|
+
self,
|
|
90
|
+
mock_fc,
|
|
91
|
+
mock_read,
|
|
92
|
+
mock_determine,
|
|
93
|
+
mock_write,
|
|
94
|
+
mock_check,
|
|
95
|
+
mock_fix,
|
|
96
|
+
mock_agent,
|
|
97
|
+
):
|
|
98
|
+
"""Deterministic-only mode skips LLM fixes."""
|
|
99
|
+
mock_type = MagicMock()
|
|
100
|
+
mock_type.value = "feature"
|
|
101
|
+
mock_class = MagicMock()
|
|
102
|
+
mock_class._title_prefix.return_value = "Feature:"
|
|
103
|
+
mock_determine.return_value = (mock_type, mock_class)
|
|
104
|
+
mock_check.side_effect = [(False, "Filename-Title Mismatch"), (True, "")]
|
|
105
|
+
|
|
106
|
+
apply_autofix(
|
|
107
|
+
file_path="file.feature",
|
|
108
|
+
classifier="feature",
|
|
109
|
+
reason="Filename-Title Mismatch",
|
|
110
|
+
deterministic=True,
|
|
111
|
+
non_deterministic=False,
|
|
112
|
+
classified_artefact_info={},
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
mock_fix.assert_called_once()
|
|
116
|
+
mock_agent.assert_not_called()
|
|
117
|
+
|
|
118
|
+
@patch("ara_cli.artefact_autofix.run_agent")
|
|
119
|
+
@patch("ara_cli.artefact_autofix.fix_title_mismatch")
|
|
120
|
+
@patch("ara_cli.artefact_autofix.check_file")
|
|
121
|
+
@patch("ara_cli.artefact_autofix.determine_artefact_type_and_class")
|
|
122
|
+
@patch("ara_cli.artefact_autofix.read_artefact", return_value="original")
|
|
123
|
+
def test_non_deterministic_only_skips_deterministic_fixes(
|
|
124
|
+
self, mock_read, mock_determine, mock_check, mock_fix_title, mock_agent, capsys
|
|
125
|
+
):
|
|
126
|
+
"""Non-deterministic-only mode skips deterministic fixes."""
|
|
127
|
+
mock_type = MagicMock()
|
|
128
|
+
mock_type.value = "feature"
|
|
129
|
+
mock_class = MagicMock()
|
|
130
|
+
mock_determine.return_value = (mock_type, mock_class)
|
|
131
|
+
mock_check.return_value = (False, "Filename-Title Mismatch")
|
|
132
|
+
|
|
133
|
+
apply_autofix(
|
|
134
|
+
file_path="file.feature",
|
|
135
|
+
classifier="feature",
|
|
136
|
+
reason="Filename-Title Mismatch",
|
|
137
|
+
deterministic=False,
|
|
138
|
+
non_deterministic=True,
|
|
139
|
+
classified_artefact_info={},
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
output = capsys.readouterr().out
|
|
143
|
+
assert "Skipping" in output or mock_fix_title.call_count == 0
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# =============================================================================
|
|
147
|
+
# Tests for contribution fixes (from ara_autofix_command.feature)
|
|
148
|
+
# =============================================================================
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class TestContributionFixes:
|
|
152
|
+
"""Tests for contribution mismatch fixes."""
|
|
153
|
+
|
|
154
|
+
@patch("ara_cli.artefact_autofix.FileClassifier")
|
|
155
|
+
@patch("ara_cli.artefact_autofix.extract_artefact_names_of_classifier")
|
|
156
|
+
@patch("ara_cli.artefact_autofix.find_closest_name_matches")
|
|
157
|
+
def test_set_closest_contribution_single_match(
|
|
158
|
+
self, mock_find, mock_extract, mock_fc
|
|
159
|
+
):
|
|
160
|
+
"""Sets contribution when single close match is found."""
|
|
161
|
+
mock_artefact = MagicMock()
|
|
162
|
+
mock_contribution = MagicMock()
|
|
163
|
+
mock_contribution.artefact_name = "test_epic"
|
|
164
|
+
mock_contribution.classifier = "epic"
|
|
165
|
+
mock_contribution.rule = None
|
|
166
|
+
mock_artefact.contribution = mock_contribution
|
|
167
|
+
mock_artefact.title = "test_userstory"
|
|
168
|
+
mock_artefact._artefact_type.return_value.value = "userstory"
|
|
169
|
+
|
|
170
|
+
mock_find.return_value = ["test_epic"]
|
|
171
|
+
|
|
172
|
+
artefact, changed = set_closest_contribution(mock_artefact)
|
|
173
|
+
|
|
174
|
+
assert changed is False # Already correct
|
|
175
|
+
|
|
176
|
+
@patch("ara_cli.artefact_autofix.FileClassifier")
|
|
177
|
+
@patch("ara_cli.artefact_autofix.extract_artefact_names_of_classifier")
|
|
178
|
+
@patch("ara_cli.artefact_autofix.find_closest_name_matches")
|
|
179
|
+
@patch(
|
|
180
|
+
"ara_cli.artefact_autofix.ask_for_contribution_choice",
|
|
181
|
+
return_value="first_match",
|
|
182
|
+
)
|
|
183
|
+
def test_set_closest_contribution_multiple_matches(
|
|
184
|
+
self, mock_ask, mock_find, mock_extract, mock_fc, capsys
|
|
185
|
+
):
|
|
186
|
+
"""Prompts user when multiple close matches found."""
|
|
187
|
+
mock_artefact = MagicMock()
|
|
188
|
+
mock_contribution = MagicMock()
|
|
189
|
+
mock_contribution.artefact_name = "test_Epic" # Slightly different
|
|
190
|
+
mock_contribution.classifier = "epic"
|
|
191
|
+
mock_contribution.rule = None
|
|
192
|
+
mock_artefact.contribution = mock_contribution
|
|
193
|
+
mock_artefact.title = "test_userstory"
|
|
194
|
+
mock_artefact._artefact_type.return_value.value = "userstory"
|
|
195
|
+
|
|
196
|
+
mock_find.return_value = ["test_epic", "Test_Epic"]
|
|
197
|
+
|
|
198
|
+
artefact, changed = set_closest_contribution(mock_artefact)
|
|
199
|
+
|
|
200
|
+
mock_ask.assert_called_once()
|
|
201
|
+
|
|
202
|
+
@patch("ara_cli.artefact_autofix.FileClassifier")
|
|
203
|
+
@patch("ara_cli.artefact_autofix.extract_artefact_names_of_classifier")
|
|
204
|
+
@patch("ara_cli.artefact_autofix.find_closest_name_matches", return_value=[])
|
|
205
|
+
@patch(
|
|
206
|
+
"ara_cli.artefact_autofix.ask_for_correct_contribution",
|
|
207
|
+
return_value=("new_epic", "epic"),
|
|
208
|
+
)
|
|
209
|
+
def test_set_closest_contribution_no_match_user_provides(
|
|
210
|
+
self, mock_ask, mock_find, mock_extract, mock_fc
|
|
211
|
+
):
|
|
212
|
+
"""Prompts user to provide contribution when no match found."""
|
|
213
|
+
mock_artefact = MagicMock()
|
|
214
|
+
mock_contribution = MagicMock()
|
|
215
|
+
mock_contribution.artefact_name = "nonexistent"
|
|
216
|
+
mock_contribution.classifier = "epic"
|
|
217
|
+
mock_contribution.rule = None
|
|
218
|
+
mock_artefact.contribution = mock_contribution
|
|
219
|
+
mock_artefact.title = "test_userstory"
|
|
220
|
+
mock_artefact._artefact_type.return_value.value = "userstory"
|
|
221
|
+
|
|
222
|
+
artefact, changed = set_closest_contribution(mock_artefact)
|
|
223
|
+
|
|
224
|
+
assert changed is True
|
|
225
|
+
mock_ask.assert_called_once()
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# =============================================================================
|
|
229
|
+
# Tests for user input handling
|
|
230
|
+
# =============================================================================
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class TestUserInputHandling:
|
|
234
|
+
"""Tests for user input during autofix."""
|
|
235
|
+
|
|
236
|
+
@patch("builtins.input", side_effect=["1"])
|
|
237
|
+
def test_ask_for_contribution_choice_selects_first(self, mock_input):
|
|
238
|
+
"""User selects first option."""
|
|
239
|
+
choices = ["option1", "option2", "option3"]
|
|
240
|
+
result = ask_for_contribution_choice(choices)
|
|
241
|
+
assert result == "option1"
|
|
242
|
+
|
|
243
|
+
@patch("builtins.input", side_effect=["2"])
|
|
244
|
+
def test_ask_for_contribution_choice_selects_second(self, mock_input):
|
|
245
|
+
"""User selects second option."""
|
|
246
|
+
choices = ["option1", "option2", "option3"]
|
|
247
|
+
result = ask_for_contribution_choice(choices)
|
|
248
|
+
assert result == "option2"
|
|
249
|
+
|
|
250
|
+
@patch("builtins.input", side_effect=["0"])
|
|
251
|
+
def test_ask_for_contribution_choice_invalid_zero(self, mock_input, capsys):
|
|
252
|
+
"""Zero is invalid choice."""
|
|
253
|
+
choices = ["option1", "option2"]
|
|
254
|
+
result = ask_for_contribution_choice(choices)
|
|
255
|
+
assert result is None
|
|
256
|
+
|
|
257
|
+
@patch("builtins.input", side_effect=["epic my_epic_name"])
|
|
258
|
+
def test_ask_for_correct_contribution_parses_input(self, mock_input):
|
|
259
|
+
"""Parses classifier and name from user input."""
|
|
260
|
+
name, classifier = ask_for_correct_contribution()
|
|
261
|
+
assert name == "my_epic_name"
|
|
262
|
+
assert classifier == "epic"
|
|
263
|
+
|
|
264
|
+
@patch("builtins.input", side_effect=[""])
|
|
265
|
+
def test_ask_for_correct_contribution_empty_clears(self, mock_input):
|
|
266
|
+
"""Empty input results in None values."""
|
|
267
|
+
name, classifier = ask_for_correct_contribution()
|
|
268
|
+
assert name is None
|
|
269
|
+
assert classifier is None
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# =============================================================================
|
|
273
|
+
# Tests for rule mismatch fixes
|
|
274
|
+
# =============================================================================
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class TestRuleFixes:
|
|
278
|
+
"""Tests for rule mismatch fixes."""
|
|
279
|
+
|
|
280
|
+
@patch("ara_cli.artefact_autofix._update_rule")
|
|
281
|
+
@patch("ara_cli.artefact_autofix.populate_classified_artefact_info")
|
|
282
|
+
def test_fix_rule_updates_rule(self, mock_populate, mock_update):
|
|
283
|
+
"""Updates rule when mismatch detected."""
|
|
284
|
+
mock_artefact = MagicMock()
|
|
285
|
+
mock_contribution = MagicMock()
|
|
286
|
+
mock_contribution.artefact_name = "parent"
|
|
287
|
+
mock_contribution.classifier = "epic"
|
|
288
|
+
mock_contribution.rule = "wrong rule"
|
|
289
|
+
mock_artefact.contribution = mock_contribution
|
|
290
|
+
mock_artefact.title = "my_artefact"
|
|
291
|
+
mock_artefact.serialize.return_value = "serialized"
|
|
292
|
+
mock_artefact._artefact_type.return_value.value = "userstory"
|
|
293
|
+
|
|
294
|
+
mock_artefact_class = MagicMock()
|
|
295
|
+
mock_artefact_class.deserialize.return_value = mock_artefact
|
|
296
|
+
mock_populate.return_value = {"info": "data"}
|
|
297
|
+
|
|
298
|
+
result = fix_rule(
|
|
299
|
+
file_path="file.userstory",
|
|
300
|
+
artefact_text="text",
|
|
301
|
+
artefact_class=mock_artefact_class,
|
|
302
|
+
classified_artefact_info={},
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
mock_update.assert_called_once()
|
|
306
|
+
assert result == "serialized"
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# =============================================================================
|
|
310
|
+
# Tests for scenario placeholder to outline conversion
|
|
311
|
+
# =============================================================================
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
class TestScenarioPlaceholderConversion:
|
|
315
|
+
"""Tests for converting Scenario with placeholders to Scenario Outline."""
|
|
316
|
+
|
|
317
|
+
def test_extract_placeholders_from_scenario(self):
|
|
318
|
+
"""Extracts placeholder variables from scenario."""
|
|
319
|
+
scenario_lines = [
|
|
320
|
+
"Given the system is running with <frequency> Hz",
|
|
321
|
+
"When the <role> performs an action",
|
|
322
|
+
"Then the result should be <expected_result>",
|
|
323
|
+
]
|
|
324
|
+
|
|
325
|
+
result = _extract_placeholders_from_scenario(scenario_lines)
|
|
326
|
+
|
|
327
|
+
assert "frequency" in result
|
|
328
|
+
assert "role" in result
|
|
329
|
+
assert "expected_result" in result
|
|
330
|
+
|
|
331
|
+
def test_create_examples_table(self):
|
|
332
|
+
"""Creates Examples table from placeholders."""
|
|
333
|
+
placeholders = {"role", "frequency", "result"}
|
|
334
|
+
indentation = " "
|
|
335
|
+
|
|
336
|
+
result = _create_examples_table(placeholders, indentation)
|
|
337
|
+
|
|
338
|
+
assert any("Examples:" in line for line in result)
|
|
339
|
+
# Check header contains placeholders
|
|
340
|
+
header_line = result[1] if len(result) > 1 else ""
|
|
341
|
+
assert (
|
|
342
|
+
"role" in header_line
|
|
343
|
+
or "frequency" in header_line
|
|
344
|
+
or "result" in header_line
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
def test_convert_to_scenario_outline(self):
|
|
348
|
+
"""Converts Scenario to Scenario Outline."""
|
|
349
|
+
scenario_lines = ["Scenario: Test scenario", " Given a step"]
|
|
350
|
+
placeholders = {"value"}
|
|
351
|
+
indentation = ""
|
|
352
|
+
|
|
353
|
+
result = _convert_to_scenario_outline(scenario_lines, placeholders, indentation)
|
|
354
|
+
|
|
355
|
+
assert any("Scenario Outline:" in line for line in result)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
# =============================================================================
|
|
359
|
+
# Tests for should_skip_issue
|
|
360
|
+
# =============================================================================
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
class TestShouldSkipIssue:
|
|
364
|
+
"""Tests for should_skip_issue function."""
|
|
365
|
+
|
|
366
|
+
def test_skips_deterministic_issue_when_flag_false(self):
|
|
367
|
+
"""Skips deterministic issues when deterministic=False."""
|
|
368
|
+
# deterministic_issue is not None means it's a deterministic issue
|
|
369
|
+
result = should_skip_issue(
|
|
370
|
+
deterministic_issue="Filename-Title Mismatch",
|
|
371
|
+
deterministic=False,
|
|
372
|
+
non_deterministic=True,
|
|
373
|
+
file_path="test.feature",
|
|
374
|
+
)
|
|
375
|
+
assert result is True
|
|
376
|
+
|
|
377
|
+
def test_skips_non_deterministic_when_flag_false(self):
|
|
378
|
+
"""Skips non-deterministic issues when non_deterministic=False."""
|
|
379
|
+
# deterministic_issue=None means it's a non-deterministic issue
|
|
380
|
+
result = should_skip_issue(
|
|
381
|
+
deterministic_issue=None,
|
|
382
|
+
deterministic=True,
|
|
383
|
+
non_deterministic=False,
|
|
384
|
+
file_path="test.feature",
|
|
385
|
+
)
|
|
386
|
+
assert result is True
|
|
387
|
+
|
|
388
|
+
def test_does_not_skip_when_both_flags_true(self):
|
|
389
|
+
"""Does not skip when both flags are True."""
|
|
390
|
+
result = should_skip_issue(
|
|
391
|
+
deterministic_issue="Filename-Title Mismatch",
|
|
392
|
+
deterministic=True,
|
|
393
|
+
non_deterministic=True,
|
|
394
|
+
file_path="test.feature",
|
|
395
|
+
)
|
|
396
|
+
assert result is False
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
# =============================================================================
|
|
400
|
+
# Tests for determine_attempt_count
|
|
401
|
+
# =============================================================================
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
class TestDetermineAttemptCount:
|
|
405
|
+
"""Tests for determine_attempt_count function."""
|
|
406
|
+
|
|
407
|
+
def test_single_pass_returns_one(self):
|
|
408
|
+
"""Single-pass mode returns 1 attempt."""
|
|
409
|
+
result = determine_attempt_count(single_pass=True, file_path="test.feature")
|
|
410
|
+
assert result == 1
|
|
411
|
+
|
|
412
|
+
def test_default_returns_three(self):
|
|
413
|
+
"""Default returns 3 attempts."""
|
|
414
|
+
result = determine_attempt_count(single_pass=False, file_path="test.feature")
|
|
415
|
+
assert result == 3
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
# =============================================================================
|
|
419
|
+
# Tests for parse_report edge cases
|
|
420
|
+
# =============================================================================
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
class TestParseReportEdgeCases:
|
|
424
|
+
"""Tests for parse_report edge cases."""
|
|
425
|
+
|
|
426
|
+
def test_handles_special_characters_in_reason(self):
|
|
427
|
+
"""Handles special characters in reason field."""
|
|
428
|
+
content = "# Artefact Check Report\n\n## feature\n- `file.feature`: Contains <placeholders> and 'quotes'\n"
|
|
429
|
+
result = parse_report(content)
|
|
430
|
+
|
|
431
|
+
assert "feature" in result
|
|
432
|
+
assert len(result["feature"]) == 1
|
|
433
|
+
assert "<placeholders>" in result["feature"][0][1]
|
|
434
|
+
|
|
435
|
+
def test_handles_multiple_issues_per_classifier(self):
|
|
436
|
+
"""Handles multiple issues for same classifier."""
|
|
437
|
+
content = """# Artefact Check Report
|
|
438
|
+
|
|
439
|
+
## feature
|
|
440
|
+
- `file1.feature`: Issue 1
|
|
441
|
+
- `file2.feature`: Issue 2
|
|
442
|
+
- `file3.feature`: Issue 3
|
|
443
|
+
"""
|
|
444
|
+
result = parse_report(content)
|
|
445
|
+
|
|
446
|
+
assert len(result["feature"]) == 3
|
|
447
|
+
|
|
448
|
+
def test_handles_empty_reason(self):
|
|
449
|
+
"""Handles empty reason field."""
|
|
450
|
+
content = "# Artefact Check Report\n\n## task\n- `file.task`\n"
|
|
451
|
+
result = parse_report(content)
|
|
452
|
+
|
|
453
|
+
assert "task" in result
|
|
454
|
+
assert result["task"][0][1] == ""
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
# =============================================================================
|
|
458
|
+
# Tests for populate_classified_artefact_info
|
|
459
|
+
# =============================================================================
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
class TestPopulateClassifiedArtefactInfo:
|
|
463
|
+
"""Tests for populate_classified_artefact_info function."""
|
|
464
|
+
|
|
465
|
+
@patch("ara_cli.artefact_autofix.FileClassifier")
|
|
466
|
+
def test_returns_existing_info_when_not_force(self, mock_fc):
|
|
467
|
+
"""Returns existing info when force=False and info exists."""
|
|
468
|
+
existing_info = {"existing": "data"}
|
|
469
|
+
result = populate_classified_artefact_info(existing_info, force=False)
|
|
470
|
+
|
|
471
|
+
assert result == existing_info
|
|
472
|
+
mock_fc.assert_not_called()
|
|
473
|
+
|
|
474
|
+
@patch("ara_cli.artefact_autofix.FileClassifier")
|
|
475
|
+
def test_creates_new_info_when_none(self, mock_fc):
|
|
476
|
+
"""Creates new info when None provided."""
|
|
477
|
+
mock_instance = mock_fc.return_value
|
|
478
|
+
mock_instance.classify_files.return_value = {"new": "data"}
|
|
479
|
+
|
|
480
|
+
result = populate_classified_artefact_info(None, force=False)
|
|
481
|
+
|
|
482
|
+
assert result == {"new": "data"}
|
|
483
|
+
mock_fc.assert_called_once()
|
|
484
|
+
|
|
485
|
+
@patch("ara_cli.artefact_autofix.FileClassifier")
|
|
486
|
+
def test_creates_new_info_when_force(self, mock_fc):
|
|
487
|
+
"""Creates new info when force=True even if info exists."""
|
|
488
|
+
mock_instance = mock_fc.return_value
|
|
489
|
+
mock_instance.classify_files.return_value = {"new": "data"}
|
|
490
|
+
existing_info = {"old": "data"}
|
|
491
|
+
|
|
492
|
+
result = populate_classified_artefact_info(existing_info, force=True)
|
|
493
|
+
|
|
494
|
+
assert result == {"new": "data"}
|
|
495
|
+
mock_fc.assert_called_once()
|