ara-cli 0.1.9.73__py3-none-any.whl → 0.1.9.75__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ara-cli might be problematic. Click here for more details.

Files changed (39) hide show
  1. ara_cli/ara_command_action.py +15 -15
  2. ara_cli/ara_command_parser.py +2 -1
  3. ara_cli/ara_config.py +181 -73
  4. ara_cli/artefact_autofix.py +130 -68
  5. ara_cli/artefact_creator.py +1 -1
  6. ara_cli/artefact_models/artefact_model.py +26 -7
  7. ara_cli/artefact_models/artefact_templates.py +47 -31
  8. ara_cli/artefact_models/businessgoal_artefact_model.py +23 -25
  9. ara_cli/artefact_models/epic_artefact_model.py +23 -24
  10. ara_cli/artefact_models/feature_artefact_model.py +76 -46
  11. ara_cli/artefact_models/keyfeature_artefact_model.py +21 -24
  12. ara_cli/artefact_models/task_artefact_model.py +73 -13
  13. ara_cli/artefact_models/userstory_artefact_model.py +22 -24
  14. ara_cli/artefact_models/vision_artefact_model.py +23 -42
  15. ara_cli/artefact_scan.py +55 -17
  16. ara_cli/chat.py +23 -5
  17. ara_cli/prompt_handler.py +4 -4
  18. ara_cli/tag_extractor.py +43 -28
  19. ara_cli/template_manager.py +3 -8
  20. ara_cli/version.py +1 -1
  21. {ara_cli-0.1.9.73.dist-info → ara_cli-0.1.9.75.dist-info}/METADATA +1 -1
  22. {ara_cli-0.1.9.73.dist-info → ara_cli-0.1.9.75.dist-info}/RECORD +29 -39
  23. tests/test_ara_config.py +420 -36
  24. tests/test_artefact_autofix.py +289 -25
  25. tests/test_artefact_scan.py +296 -35
  26. tests/test_chat.py +35 -15
  27. ara_cli/templates/template.businessgoal +0 -10
  28. ara_cli/templates/template.capability +0 -10
  29. ara_cli/templates/template.epic +0 -15
  30. ara_cli/templates/template.example +0 -6
  31. ara_cli/templates/template.feature +0 -26
  32. ara_cli/templates/template.issue +0 -14
  33. ara_cli/templates/template.keyfeature +0 -15
  34. ara_cli/templates/template.task +0 -6
  35. ara_cli/templates/template.userstory +0 -17
  36. ara_cli/templates/template.vision +0 -14
  37. {ara_cli-0.1.9.73.dist-info → ara_cli-0.1.9.75.dist-info}/WHEEL +0 -0
  38. {ara_cli-0.1.9.73.dist-info → ara_cli-0.1.9.75.dist-info}/entry_points.txt +0 -0
  39. {ara_cli-0.1.9.73.dist-info → ara_cli-0.1.9.75.dist-info}/top_level.txt +0 -0
@@ -10,8 +10,13 @@ from ara_cli.artefact_autofix import (
10
10
  write_corrected_artefact,
11
11
  construct_prompt,
12
12
  fix_title_mismatch,
13
+ ask_for_correct_contribution,
14
+ ask_for_contribution_choice,
15
+ _has_valid_contribution,
16
+ set_closest_contribution,
17
+ fix_contribution,
13
18
  )
14
- from ara_cli.artefact_models.artefact_model import ArtefactType
19
+ from ara_cli.artefact_models.artefact_model import Artefact, ArtefactType, Contribution
15
20
 
16
21
 
17
22
  @pytest.fixture
@@ -38,6 +43,23 @@ def mock_classified_artefact_info():
38
43
  return MagicMock()
39
44
 
40
45
 
46
+ @pytest.fixture
47
+ def mock_artefact_with_contribution():
48
+ """Provides a mock Artefact with a mock Contribution."""
49
+ mock_contribution = MagicMock(spec=Contribution)
50
+ mock_contribution.artefact_name = "some_artefact"
51
+ mock_contribution.classifier = "feature"
52
+ mock_contribution.rule = "some rule"
53
+
54
+ mock_artefact = MagicMock(spec=Artefact)
55
+ mock_artefact.contribution = mock_contribution
56
+ mock_artefact.title = "my_test_artefact"
57
+ mock_artefact._artefact_type.return_value.value = "requirement"
58
+ mock_artefact.serialize.return_value = "serialized artefact text"
59
+
60
+ return mock_artefact
61
+
62
+
41
63
  def test_read_report_file_success():
42
64
  """Tests successful reading of the report file."""
43
65
  mock_content = "# Artefact Check Report\n- `file.feature`: reason"
@@ -96,11 +118,9 @@ def test_read_artefact_file_not_found(capsys):
96
118
  @patch("ara_cli.artefact_models.artefact_mapping.artefact_type_mapping")
97
119
  def test_determine_artefact_type_and_class_no_class_found(mock_mapping, capsys):
98
120
  mock_mapping.get.return_value = None
99
- # The function returns (None, None) if the class is not in the mapping.
100
121
  artefact_type, artefact_class = determine_artefact_type_and_class("feature")
101
122
  assert artefact_type is None
102
123
  assert artefact_class is None
103
- # The print statement inside the function is called before returning, so this check is valid.
104
124
  assert "No artefact class found for" in capsys.readouterr().out
105
125
 
106
126
 
@@ -124,7 +144,8 @@ def test_write_corrected_artefact():
124
144
  def test_construct_prompt_for_task():
125
145
  prompt = construct_prompt(ArtefactType.task, "some reason", "file.task", "text")
126
146
  assert (
127
- "For task artefacts, if the action items looks like template or empty" in prompt
147
+ "For task artefacts, if the action items looks like template or empty"
148
+ in prompt
128
149
  )
129
150
 
130
151
 
@@ -133,28 +154,30 @@ def test_construct_prompt_for_task():
133
154
  "ara_cli.artefact_autofix.determine_artefact_type_and_class",
134
155
  return_value=(None, None),
135
156
  )
136
- @patch("ara_cli.artefact_autofix.read_artefact", return_value="original text")
157
+ @patch("ara_cli.artefact_autofix.read_artefact")
137
158
  def test_apply_autofix_exits_when_classifier_is_invalid(
138
159
  mock_read, mock_determine, mock_run_agent, mock_classified_artefact_info
139
160
  ):
140
161
  """Tests that apply_autofix exits early if the classifier is invalid."""
141
162
  result = apply_autofix(
142
- "file.feature",
143
- "invalid",
144
- "reason",
163
+ file_path="file.feature",
164
+ classifier="invalid",
165
+ reason="reason",
145
166
  deterministic=True,
146
167
  non_deterministic=True,
147
168
  classified_artefact_info=mock_classified_artefact_info,
148
169
  )
149
170
  assert result is False
150
- mock_read.assert_called_once_with("file.feature")
151
171
  mock_determine.assert_called_once_with("invalid")
172
+ mock_read.assert_not_called()
152
173
  mock_run_agent.assert_not_called()
153
174
 
154
175
 
176
+ @patch("ara_cli.artefact_autofix.FileClassifier")
177
+ @patch("ara_cli.artefact_autofix.check_file")
155
178
  @patch("ara_cli.artefact_autofix.run_agent")
156
179
  @patch("ara_cli.artefact_autofix.write_corrected_artefact")
157
- @patch("ara_cli.artefact_autofix.fix_title_mismatch", return_value="fixed text")
180
+ @patch("ara_cli.artefact_autofix.fix_title_mismatch")
158
181
  @patch("ara_cli.artefact_autofix.determine_artefact_type_and_class")
159
182
  @patch("ara_cli.artefact_autofix.read_artefact", return_value="original text")
160
183
  def test_apply_autofix_for_title_mismatch_with_deterministic_flag(
@@ -163,24 +186,31 @@ def test_apply_autofix_for_title_mismatch_with_deterministic_flag(
163
186
  mock_fix_title,
164
187
  mock_write,
165
188
  mock_run_agent,
189
+ mock_check_file,
190
+ mock_file_classifier,
166
191
  mock_artefact_type,
167
192
  mock_artefact_class,
168
193
  mock_classified_artefact_info,
169
194
  ):
170
195
  """Tests that a deterministic fix is applied when the flag is True."""
171
196
  mock_determine.return_value = (mock_artefact_type, mock_artefact_class)
172
- reason = "Filename-Title Mismatch: some details"
197
+ mock_check_file.side_effect = [
198
+ (False, "Filename-Title Mismatch: some details"),
199
+ (True, ""),
200
+ ]
201
+ mock_fix_title.return_value = "fixed text"
173
202
 
174
203
  result = apply_autofix(
175
- "file.feature",
176
- "feature",
177
- reason,
204
+ file_path="file.feature",
205
+ classifier="feature",
206
+ reason="Filename-Title Mismatch: some details",
178
207
  deterministic=True,
179
208
  non_deterministic=False,
180
209
  classified_artefact_info=mock_classified_artefact_info,
181
210
  )
182
211
 
183
212
  assert result is True
213
+ assert mock_check_file.call_count == 2
184
214
  mock_fix_title.assert_called_once_with(
185
215
  file_path="file.feature",
186
216
  artefact_text="original text",
@@ -189,8 +219,10 @@ def test_apply_autofix_for_title_mismatch_with_deterministic_flag(
189
219
  )
190
220
  mock_write.assert_called_once_with("file.feature", "fixed text")
191
221
  mock_run_agent.assert_not_called()
222
+ mock_file_classifier.assert_called_once()
192
223
 
193
224
 
225
+ @patch("ara_cli.artefact_autofix.check_file")
194
226
  @patch("ara_cli.artefact_autofix.run_agent")
195
227
  @patch("ara_cli.artefact_autofix.write_corrected_artefact")
196
228
  @patch("ara_cli.artefact_autofix.fix_title_mismatch")
@@ -202,29 +234,34 @@ def test_apply_autofix_skips_title_mismatch_without_deterministic_flag(
202
234
  mock_fix_title,
203
235
  mock_write,
204
236
  mock_run_agent,
237
+ mock_check_file,
205
238
  mock_artefact_type,
206
239
  mock_artefact_class,
207
240
  mock_classified_artefact_info,
208
241
  ):
209
242
  """Tests that a deterministic fix is skipped when the flag is False."""
210
243
  mock_determine.return_value = (mock_artefact_type, mock_artefact_class)
211
- reason = "Filename-Title Mismatch: some details"
244
+ mock_check_file.return_value = (False, "Filename-Title Mismatch: some details")
212
245
 
213
246
  result = apply_autofix(
214
- "file.feature",
215
- "feature",
216
- reason,
247
+ file_path="file.feature",
248
+ classifier="feature",
249
+ reason="Filename-Title Mismatch: some details",
217
250
  deterministic=False,
218
251
  non_deterministic=True,
219
252
  classified_artefact_info=mock_classified_artefact_info,
220
253
  )
221
254
 
222
255
  assert result is False
256
+ mock_check_file.assert_called_once()
257
+ mock_read.assert_called_once_with("file.feature")
223
258
  mock_fix_title.assert_not_called()
224
259
  mock_write.assert_not_called()
225
260
  mock_run_agent.assert_not_called()
226
261
 
227
262
 
263
+ @patch("ara_cli.artefact_autofix.FileClassifier")
264
+ @patch("ara_cli.artefact_autofix.check_file")
228
265
  @patch("ara_cli.artefact_autofix.write_corrected_artefact")
229
266
  @patch("ara_cli.artefact_autofix.run_agent")
230
267
  @patch("ara_cli.artefact_autofix.determine_artefact_type_and_class")
@@ -234,27 +271,32 @@ def test_apply_autofix_for_llm_fix_with_non_deterministic_flag(
234
271
  mock_determine,
235
272
  mock_run_agent,
236
273
  mock_write,
274
+ mock_check_file,
275
+ mock_file_classifier,
237
276
  mock_artefact_type,
238
277
  mock_artefact_class,
239
278
  mock_classified_artefact_info,
240
279
  ):
241
280
  """Tests that an LLM fix is applied when the non-deterministic flag is True."""
242
281
  mock_determine.return_value = (mock_artefact_type, mock_artefact_class)
282
+ mock_check_file.side_effect = [(False, "Pydantic validation error"), (True, "")]
243
283
  mock_run_agent.return_value = mock_artefact_class
244
- reason = "Pydantic validation error"
245
284
 
246
285
  result = apply_autofix(
247
- "file.feature",
248
- "feature",
249
- reason,
286
+ file_path="file.feature",
287
+ classifier="feature",
288
+ reason="Pydantic validation error",
250
289
  deterministic=False,
251
290
  non_deterministic=True,
252
291
  classified_artefact_info=mock_classified_artefact_info,
253
292
  )
254
293
 
255
294
  assert result is True
295
+ assert mock_check_file.call_count == 2
296
+ mock_read.assert_called_once_with("file.feature")
256
297
  mock_run_agent.assert_called_once()
257
298
  mock_write.assert_called_once_with("file.feature", "llm corrected content")
299
+ mock_file_classifier.assert_called_once()
258
300
 
259
301
 
260
302
  @patch("ara_cli.artefact_autofix.write_corrected_artefact")
@@ -320,9 +362,6 @@ def test_apply_autofix_llm_exception(
320
362
  )
321
363
 
322
364
 
323
- # === Other Tests ===
324
-
325
-
326
365
  def test_fix_title_mismatch_success(mock_artefact_class):
327
366
  artefact_text = "Feature: wrong title\nSome other content"
328
367
  file_path = "path/to/correct_title.feature"
@@ -351,3 +390,228 @@ def test_run_agent_exception_handling(mock_agent_class):
351
390
  mock_agent_instance.run_sync.side_effect = Exception("Agent error")
352
391
  with pytest.raises(Exception, match="Agent error"):
353
392
  run_agent("prompt", MagicMock())
393
+
394
+
395
+ @patch("builtins.input", side_effect=["1"])
396
+ def test_ask_for_contribution_choice_valid(mock_input):
397
+ """Tests selecting a valid choice."""
398
+ choices = ["choice1", "choice2"]
399
+ # This simpler call now works without causing a TypeError
400
+ result = ask_for_contribution_choice(choices)
401
+ assert result == "choice1"
402
+
403
+ @patch("builtins.input", side_effect=["99"])
404
+ def test_ask_for_contribution_choice_out_of_range(mock_input, capsys):
405
+ """Tests selecting a choice that is out of range."""
406
+ choices = ["choice1", "choice2"]
407
+ result = ask_for_contribution_choice(choices)
408
+ assert result is None
409
+ assert "Invalid choice" in capsys.readouterr().out
410
+
411
+
412
+ @patch("builtins.input", side_effect=["not a number"])
413
+ def test_ask_for_contribution_choice_invalid_input(mock_input, capsys):
414
+ """Tests providing non-numeric input."""
415
+ choices = ["choice1", "choice2"]
416
+ result = ask_for_contribution_choice(choices)
417
+ assert result is None
418
+ assert "Invalid input" in capsys.readouterr().out
419
+
420
+
421
+ @patch("builtins.input", side_effect=["feature my_feature_name"])
422
+ def test_ask_for_correct_contribution_valid(mock_input):
423
+ """Tests providing valid '<classifier> <name>' input."""
424
+ name, classifier = ask_for_correct_contribution(("old_name", "feature"))
425
+ assert name == "my_feature_name"
426
+ assert classifier == "feature"
427
+
428
+
429
+ @patch("builtins.input", side_effect=[""])
430
+ def test_ask_for_correct_contribution_empty_input(mock_input):
431
+ """Tests providing empty input."""
432
+ name, classifier = ask_for_correct_contribution()
433
+ assert name is None
434
+ assert classifier is None
435
+
436
+
437
+ @patch("builtins.input", side_effect=["invalid-one-word-input"])
438
+ def test_ask_for_correct_contribution_invalid_format(mock_input, capsys):
439
+ """Tests providing input with the wrong format."""
440
+ # Fix: Use input that results in a single part after split()
441
+ name, classifier = ask_for_correct_contribution()
442
+ assert name is None
443
+ assert classifier is None
444
+ assert "Invalid input format" in capsys.readouterr().out
445
+
446
+
447
+ def test_has_valid_contribution_true(mock_artefact_with_contribution):
448
+ """Tests with a valid contribution object."""
449
+ # Fix: Check for truthiness, not strict boolean equality
450
+ assert _has_valid_contribution(mock_artefact_with_contribution)
451
+
452
+
453
+ def test_has_valid_contribution_false_no_contribution():
454
+ """Tests when the artefact's contribution is None."""
455
+ mock_artefact = MagicMock(spec=Artefact)
456
+ mock_artefact.contribution = None
457
+ # Fix: Check for falsiness, not strict boolean equality
458
+ assert not _has_valid_contribution(mock_artefact)
459
+
460
+
461
+ @patch("ara_cli.artefact_autofix.FileClassifier")
462
+ @patch("ara_cli.artefact_autofix.extract_artefact_names_of_classifier")
463
+ @patch("ara_cli.artefact_autofix.find_closest_name_matches")
464
+ def test_set_closest_contribution_no_change_needed(
465
+ mock_find, mock_extract, mock_classifier, mock_artefact_with_contribution
466
+ ):
467
+ """Tests the case where the contribution name is already the best match."""
468
+ mock_find.return_value = ["some_artefact"] # Exact match is found
469
+ artefact, changed = set_closest_contribution(mock_artefact_with_contribution)
470
+ assert changed is False
471
+ assert artefact == mock_artefact_with_contribution
472
+
473
+
474
+ @patch("ara_cli.artefact_autofix.FileClassifier")
475
+ @patch("ara_cli.artefact_autofix.extract_artefact_names_of_classifier")
476
+ @patch("ara_cli.artefact_autofix.find_closest_name_matches", return_value=[])
477
+ @patch(
478
+ "ara_cli.artefact_autofix.ask_for_correct_contribution",
479
+ return_value=("new_name", "new_classifier"),
480
+ )
481
+ def test_set_closest_contribution_no_matches_user_provides(
482
+ mock_ask, mock_find, mock_extract, mock_classifier, mock_artefact_with_contribution
483
+ ):
484
+ """Tests when no matches are found and the user provides a new contribution."""
485
+ artefact, changed = set_closest_contribution(mock_artefact_with_contribution)
486
+ assert changed is True
487
+ assert artefact.contribution.artefact_name == "new_name"
488
+ assert artefact.contribution.classifier == "new_classifier"
489
+
490
+
491
+ @patch("ara_cli.artefact_autofix.set_closest_contribution")
492
+ @patch("ara_cli.artefact_autofix.FileClassifier")
493
+ def test_fix_contribution(
494
+ mock_file_classifier, mock_set, mock_artefact_with_contribution
495
+ ):
496
+ """Tests the fix_contribution wrapper function."""
497
+ # Arrange
498
+ mock_artefact_class = MagicMock()
499
+ mock_artefact_class.deserialize.return_value = mock_artefact_with_contribution
500
+ mock_set.return_value = (mock_artefact_with_contribution, True)
501
+
502
+ # Act
503
+ result = fix_contribution(
504
+ file_path="dummy.path",
505
+ artefact_text="original text",
506
+ artefact_class=mock_artefact_class,
507
+ classified_artefact_info={},
508
+ )
509
+
510
+ # Assert
511
+ assert result == "serialized artefact text"
512
+ mock_artefact_class.deserialize.assert_called_once_with("original text")
513
+ mock_set.assert_called_once_with(mock_artefact_with_contribution)
514
+
515
+
516
+ @patch("ara_cli.artefact_autofix.FileClassifier")
517
+ @patch("ara_cli.artefact_autofix.check_file")
518
+ @patch("ara_cli.artefact_autofix.write_corrected_artefact")
519
+ @patch("ara_cli.artefact_autofix.fix_contribution", return_value="fixed text")
520
+ @patch("ara_cli.artefact_autofix.determine_artefact_type_and_class")
521
+ @patch("ara_cli.artefact_autofix.read_artefact", return_value="original text")
522
+ def test_apply_autofix_for_contribution_mismatch(
523
+ mock_read,
524
+ mock_determine,
525
+ mock_fix_contribution,
526
+ mock_write,
527
+ mock_check_file,
528
+ mock_classifier,
529
+ mock_artefact_type,
530
+ mock_artefact_class,
531
+ mock_classified_artefact_info,
532
+ ):
533
+ """Tests the deterministic fix for 'Invalid Contribution Reference'."""
534
+ mock_determine.return_value = (mock_artefact_type, mock_artefact_class)
535
+ mock_check_file.side_effect = [
536
+ (False, "Invalid Contribution Reference"),
537
+ (True, ""),
538
+ ]
539
+
540
+ result = apply_autofix(
541
+ file_path="file.feature",
542
+ classifier="feature",
543
+ reason="Invalid Contribution Reference",
544
+ classified_artefact_info=mock_classified_artefact_info,
545
+ )
546
+
547
+ assert result is True
548
+ mock_fix_contribution.assert_called_once()
549
+ mock_write.assert_called_once_with("file.feature", "fixed text")
550
+
551
+
552
+ @patch("ara_cli.artefact_autofix.check_file")
553
+ @patch("ara_cli.artefact_autofix.write_corrected_artefact")
554
+ @patch("ara_cli.artefact_autofix.fix_title_mismatch", return_value="original text")
555
+ @patch("ara_cli.artefact_autofix.determine_artefact_type_and_class")
556
+ @patch("ara_cli.artefact_autofix.read_artefact", return_value="original text")
557
+ def test_apply_autofix_stops_if_no_alteration(
558
+ mock_read,
559
+ mock_determine,
560
+ mock_fix_title,
561
+ mock_write,
562
+ mock_check_file,
563
+ capsys,
564
+ mock_artefact_type,
565
+ mock_artefact_class,
566
+ mock_classified_artefact_info,
567
+ ):
568
+ """Tests that the loop stops if a fix attempt does not change the file content."""
569
+ mock_determine.return_value = (mock_artefact_type, mock_artefact_class)
570
+ mock_check_file.return_value = (False, "Filename-Title Mismatch")
571
+
572
+ result = apply_autofix(
573
+ file_path="file.feature",
574
+ classifier="feature",
575
+ reason="any",
576
+ classified_artefact_info=mock_classified_artefact_info,
577
+ )
578
+
579
+ assert result is False
580
+ mock_fix_title.assert_called_once()
581
+ mock_write.assert_not_called()
582
+ assert (
583
+ "Fixing attempt did not alter the file. Stopping to prevent infinite loop."
584
+ in capsys.readouterr().out
585
+ )
586
+
587
+
588
+ @patch("ara_cli.artefact_autofix.check_file")
589
+ @patch("ara_cli.artefact_autofix.determine_artefact_type_and_class")
590
+ def test_apply_autofix_single_pass(
591
+ mock_determine,
592
+ mock_check_file,
593
+ capsys,
594
+ mock_artefact_type,
595
+ mock_artefact_class,
596
+ mock_classified_artefact_info,
597
+ ):
598
+ """Tests that single_pass=True runs the loop only once."""
599
+ mock_determine.return_value = (mock_artefact_type, mock_artefact_class)
600
+ # Simulate a failure that won't be fixed to ensure the loop doesn't repeat
601
+ mock_check_file.return_value = (False, "Some unfixable error")
602
+
603
+ apply_autofix(
604
+ file_path="file.feature",
605
+ classifier="feature",
606
+ reason="any",
607
+ single_pass=True,
608
+ deterministic=False, # Disable fixes
609
+ non_deterministic=False,
610
+ classified_artefact_info=mock_classified_artefact_info,
611
+ )
612
+
613
+ output = capsys.readouterr().out
614
+ assert "Single-pass mode enabled" in output
615
+ assert "Attempt 1/1" in output
616
+ assert "Attempt 2/1" not in output
617
+ mock_check_file.assert_called_once()