ara-cli 0.1.10.5__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.
Files changed (106) hide show
  1. ara_cli/__init__.py +51 -6
  2. ara_cli/__main__.py +87 -75
  3. ara_cli/ara_command_action.py +95 -57
  4. ara_cli/ara_config.py +187 -128
  5. ara_cli/ara_subcommands/common.py +2 -2
  6. ara_cli/ara_subcommands/config.py +221 -0
  7. ara_cli/ara_subcommands/convert.py +43 -0
  8. ara_cli/ara_subcommands/fetch.py +41 -0
  9. ara_cli/ara_subcommands/fetch_agents.py +22 -0
  10. ara_cli/ara_subcommands/fetch_scripts.py +19 -0
  11. ara_cli/ara_subcommands/fetch_templates.py +15 -10
  12. ara_cli/ara_subcommands/list.py +97 -23
  13. ara_cli/artefact_autofix.py +115 -62
  14. ara_cli/artefact_converter.py +256 -0
  15. ara_cli/chat.py +283 -62
  16. ara_cli/chat_agent/__init__.py +0 -0
  17. ara_cli/chat_agent/agent_process_manager.py +155 -0
  18. ara_cli/chat_script_runner/__init__.py +0 -0
  19. ara_cli/chat_script_runner/script_completer.py +23 -0
  20. ara_cli/chat_script_runner/script_finder.py +41 -0
  21. ara_cli/chat_script_runner/script_lister.py +36 -0
  22. ara_cli/chat_script_runner/script_runner.py +36 -0
  23. ara_cli/chat_web_search/__init__.py +0 -0
  24. ara_cli/chat_web_search/web_search.py +263 -0
  25. ara_cli/commands/agent_run_command.py +98 -0
  26. ara_cli/commands/fetch_agents_command.py +106 -0
  27. ara_cli/commands/fetch_scripts_command.py +43 -0
  28. ara_cli/commands/fetch_templates_command.py +39 -0
  29. ara_cli/commands/fetch_templates_commands.py +39 -0
  30. ara_cli/commands/list_agents_command.py +39 -0
  31. ara_cli/completers.py +71 -35
  32. ara_cli/constants.py +2 -0
  33. ara_cli/directory_navigator.py +37 -4
  34. ara_cli/llm_utils.py +58 -0
  35. ara_cli/prompt_chat.py +20 -4
  36. ara_cli/prompt_extractor.py +47 -32
  37. ara_cli/template_loader.py +2 -1
  38. ara_cli/template_manager.py +52 -21
  39. ara_cli/templates/global-scripts/hello_global.py +1 -0
  40. ara_cli/templates/prompt-modules/commands/add_scenarios_for_new_behaviour.feature_creation_agent.commands.md +1 -0
  41. ara_cli/templates/prompt-modules/commands/align_feature_with_implementation_changes.interview_agent.commands.md +1 -0
  42. ara_cli/templates/prompt-modules/commands/analyze_codebase_and_plan_tasks.interview_agent.commands.md +1 -0
  43. ara_cli/templates/prompt-modules/commands/choose_best_parent_artefact.interview_agent.commands.md +1 -0
  44. ara_cli/templates/prompt-modules/commands/create_tasks_from_artefact_content.interview_agent.commands.md +1 -0
  45. ara_cli/templates/prompt-modules/commands/create_tests_for_uncovered_modules.test_generation_agent.commands.md +1 -0
  46. ara_cli/templates/prompt-modules/commands/derive_features_from_video_description.feature_creation_agent.commands.md +1 -0
  47. ara_cli/templates/prompt-modules/commands/describe_agent_capabilities.agent.commands.md +1 -0
  48. ara_cli/templates/prompt-modules/commands/empty.commands.md +2 -12
  49. ara_cli/templates/prompt-modules/commands/execute_scoped_todos_in_task.interview_agent.commands.md +1 -0
  50. ara_cli/templates/prompt-modules/commands/explain_single_file_purpose.interview_agent.commands.md +1 -0
  51. ara_cli/templates/prompt-modules/commands/extract_file_information_bullets.interview_agent.commands.md +1 -0
  52. ara_cli/templates/prompt-modules/commands/extract_general.commands.md +12 -0
  53. ara_cli/templates/prompt-modules/commands/extract_markdown.commands.md +11 -0
  54. ara_cli/templates/prompt-modules/commands/extract_python.commands.md +13 -0
  55. ara_cli/templates/prompt-modules/commands/feature_add_or_modifiy_specified_behavior.commands.md +36 -0
  56. ara_cli/templates/prompt-modules/commands/feature_generate_initial_specified_bevahior.commands.md +53 -0
  57. ara_cli/templates/prompt-modules/commands/fix_failing_behave_step_definitions.interview_agent.commands.md +1 -0
  58. ara_cli/templates/prompt-modules/commands/fix_failing_pytest_tests.interview_agent.commands.md +1 -0
  59. ara_cli/templates/prompt-modules/commands/general_instruction_policy.commands.md +47 -0
  60. ara_cli/templates/prompt-modules/commands/generate_and_fix_pytest_tests.test_generation_agent.commands.md +1 -0
  61. ara_cli/templates/prompt-modules/commands/prompt_template_tech_stack_transformer.commands.md +95 -0
  62. ara_cli/templates/prompt-modules/commands/python_bug_fixing_code.commands.md +34 -0
  63. ara_cli/templates/prompt-modules/commands/python_generate_code.commands.md +27 -0
  64. ara_cli/templates/prompt-modules/commands/python_refactoring_code.commands.md +39 -0
  65. ara_cli/templates/prompt-modules/commands/python_step_definitions_generation_and_fixing.commands.md +40 -0
  66. ara_cli/templates/prompt-modules/commands/python_unittest_generation_and_fixing.commands.md +48 -0
  67. ara_cli/templates/prompt-modules/commands/suggest_next_story_child_tasks.interview_agent.commands.md +1 -0
  68. ara_cli/templates/prompt-modules/commands/summarize_or_transcribe_media.interview_agent.commands.md +1 -0
  69. ara_cli/templates/prompt-modules/commands/update_feature_to_match_implementation.feature_creation_agent.commands.md +1 -0
  70. ara_cli/templates/prompt-modules/commands/update_user_story_with_requirements.interview_agent.commands.md +1 -0
  71. ara_cli/version.py +1 -1
  72. {ara_cli-0.1.10.5.dist-info → ara_cli-0.1.13.3.dist-info}/METADATA +33 -1
  73. {ara_cli-0.1.10.5.dist-info → ara_cli-0.1.13.3.dist-info}/RECORD +89 -43
  74. tests/test_ara_command_action.py +31 -19
  75. tests/test_ara_config.py +177 -90
  76. tests/test_artefact_autofix.py +170 -97
  77. tests/test_artefact_autofix_integration.py +495 -0
  78. tests/test_artefact_converter.py +357 -0
  79. tests/test_artefact_extraction.py +564 -0
  80. tests/test_chat.py +162 -126
  81. tests/test_chat_givens_images.py +603 -0
  82. tests/test_chat_script_runner.py +454 -0
  83. tests/test_llm_utils.py +164 -0
  84. tests/test_prompt_chat.py +343 -0
  85. tests/test_prompt_extractor.py +683 -0
  86. tests/test_web_search.py +467 -0
  87. ara_cli/templates/prompt-modules/blueprints/complete_pytest_unittest.blueprint.md +0 -27
  88. ara_cli/templates/prompt-modules/blueprints/pytest_unittest_prompt.blueprint.md +0 -32
  89. ara_cli/templates/prompt-modules/blueprints/task_todo_list_implement_feature_BDD_way.blueprint.md +0 -30
  90. ara_cli/templates/prompt-modules/commands/artefact_classification.commands.md +0 -9
  91. ara_cli/templates/prompt-modules/commands/artefact_extension.commands.md +0 -17
  92. ara_cli/templates/prompt-modules/commands/artefact_formulation.commands.md +0 -14
  93. ara_cli/templates/prompt-modules/commands/behave_step_generation.commands.md +0 -102
  94. ara_cli/templates/prompt-modules/commands/code_generation_complex.commands.md +0 -20
  95. ara_cli/templates/prompt-modules/commands/code_generation_simple.commands.md +0 -13
  96. ara_cli/templates/prompt-modules/commands/error_fixing.commands.md +0 -20
  97. ara_cli/templates/prompt-modules/commands/feature_file_update.commands.md +0 -18
  98. ara_cli/templates/prompt-modules/commands/feature_formulation.commands.md +0 -43
  99. ara_cli/templates/prompt-modules/commands/js_code_generation_simple.commands.md +0 -13
  100. ara_cli/templates/prompt-modules/commands/refactoring.commands.md +0 -15
  101. ara_cli/templates/prompt-modules/commands/refactoring_analysis.commands.md +0 -9
  102. ara_cli/templates/prompt-modules/commands/reverse_engineer_feature_file.commands.md +0 -15
  103. ara_cli/templates/prompt-modules/commands/reverse_engineer_program_flow.commands.md +0 -19
  104. {ara_cli-0.1.10.5.dist-info → ara_cli-0.1.13.3.dist-info}/WHEEL +0 -0
  105. {ara_cli-0.1.10.5.dist-info → ara_cli-0.1.13.3.dist-info}/entry_points.txt +0 -0
  106. {ara_cli-0.1.10.5.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()