ara-cli 0.1.9.49__py3-none-any.whl → 0.1.9.51__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/tag_extractor.py CHANGED
@@ -8,30 +8,20 @@ class TagExtractor:
8
8
 
9
9
  def extract_tags(self, navigate_to_target=False):
10
10
  from ara_cli.template_manager import DirectoryNavigator
11
- from ara_cli.file_classifier import FileClassifier
11
+ from ara_cli.artefact_reader import ArtefactReader
12
12
 
13
13
  navigator = DirectoryNavigator()
14
14
  if navigate_to_target:
15
15
  navigator.navigate_to_target()
16
16
 
17
- file_classifier = FileClassifier(self.file_system)
18
- classified_files = file_classifier.classify_files_new()
17
+ artefacts = ArtefactReader.read_artefacts()
19
18
 
20
19
  unique_tags = set()
21
20
 
22
- for artefacts in classified_files.values():
23
- for artefact in artefacts:
24
- with open(artefact, 'r') as file:
25
- artefact_content = file.read()
26
- try:
27
- artefact_object = artefact_from_content(artefact_content)
28
- except ValueError:
29
- continue
30
- if not artefact_object:
31
- continue
32
- status_list = ([artefact_object.status] if artefact_object.status is not None else [])
33
- users_list = [f"user_{user}" for user in artefact_object.users]
34
- tags = artefact_object.tags + users_list + status_list
21
+ for artefact_list in artefacts.values():
22
+ for artefact in artefact_list:
23
+ user_tags = [f"user_{tag}" for tag in artefact.users]
24
+ tags = [tag for tag in (artefact.tags + [artefact.status] + user_tags) if tag is not None]
35
25
  unique_tags.update(tags)
36
26
 
37
27
  sorted_tags = sorted(unique_tags)
@@ -1,27 +1,25 @@
1
1
  # Title: <descriptive title of the architecture concept>
2
2
 
3
- - [Title: ](#title-)
4
- - [Decision for Architecture Design](#decision-for-architecture-design)
5
- - [functional major decisions](#functional-major-decisions)
6
- - [non functional major decisions](#non-functional-major-decisions)
7
- - [Component Diagram in plantuml](#component-diagram-in-plantuml)
8
- - [Deployment diagram in plantuml](#deployment-diagram-in-plantuml)
3
+ # C4 Context Diagram in mermaid
4
+ ```
9
5
 
10
- # Decision for Architecture Design
11
- ## functional major decisions
12
- <list of major functional decisions of the concept>
6
+ ```
7
+ Description: <description of the purpose and the value of the components of the diagram>
13
8
 
14
- ## non functional major decisions
15
- <list of major non functional decisions of the concept>
9
+ ## C4 Container Diagram in mermaid
10
+ ```
11
+
12
+ ```
13
+ Description: <description of the purpose and the value of the components of the diagram>
16
14
 
17
- # Component Diagram in plantuml
15
+ ### C4 Component Diagram in mermaid
18
16
  ```
19
17
 
20
18
  ```
21
19
  Description: <description of the purpose and the value of the components of the diagram>
22
20
 
23
- # Deployment diagram in plantuml
21
+ #### C4 Code Diagram in mermaid
24
22
  ```
25
23
 
26
24
  ```
27
- Description: <description of the purpose and the value of the building blocks of the diagram>
25
+ Description: <description of the purpose and the value of the components of the diagram>
@@ -1,5 +1,5 @@
1
1
  import pytest
2
- from unittest.mock import patch, MagicMock, call
2
+ from unittest.mock import patch, MagicMock, call, mock_open
3
3
  from ara_cli.ara_command_action import (
4
4
  check_validity,
5
5
  create_action,
@@ -10,7 +10,8 @@ from ara_cli.ara_command_action import (
10
10
  read_user_action,
11
11
  set_status_action,
12
12
  set_user_action,
13
- classifier_directory_action
13
+ classifier_directory_action,
14
+ scan_action
14
15
  )
15
16
 
16
17
 
@@ -100,7 +101,8 @@ def mock_suggest_close_name_matches():
100
101
 
101
102
  @pytest.mark.parametrize(
102
103
  "condition, error_message",
103
- [(True, "This should not be printed"), (False, "This is a test error message")],
104
+ [(True, "This should not be printed"),
105
+ (False, "This is a test error message")],
104
106
  )
105
107
  def test_check_validity(condition, error_message):
106
108
  with patch("sys.exit") as mock_exit, patch("builtins.print") as mock_print:
@@ -345,192 +347,259 @@ def test_list_action_creates_list_filter(
345
347
 
346
348
 
347
349
  @pytest.mark.parametrize(
348
- "classifier, artefact_name, artefact_names, content, status, expected_output",
350
+ "classifier, artefact_name, artefact_exists, status, expected_output",
349
351
  [
350
352
  # Case when artefact_name is not found in artefact_names
351
- ("test_classifier", "non_existent_artefact", ["artefact1", "artefact2"], None, None, None),
353
+ ("test_classifier", "non_existent_artefact", False, None, None),
352
354
 
353
355
  # Case when artefact_name is found but no status is available
354
- ("test_classifier", "artefact1", ["artefact1", "artefact2"], "some_content", None, "No status found"),
356
+ ("test_classifier", "artefact1", True, None, "No status found"),
355
357
 
356
358
  # Case when artefact_name is found and status is available
357
- ("test_classifier", "artefact2", ["artefact1", "artefact2"], "some_content", "Active", "Active"),
359
+ ("test_classifier", "artefact2", True, "Active", "Active"),
358
360
  ]
359
361
  )
360
- def test_read_status_action(classifier, artefact_name, artefact_names, content, status, expected_output):
362
+ def test_read_status_action(classifier, artefact_name, artefact_exists, status, expected_output):
361
363
  args = MagicMock()
362
364
  args.classifier = classifier
363
365
  args.parameter = artefact_name
364
-
366
+
365
367
  mock_artefact = MagicMock()
366
368
  mock_artefact.status = status
367
369
 
370
+ # Create mock artefact info
371
+ artefact_info_dicts = []
372
+ if artefact_exists:
373
+ artefact_info_dicts.append({
374
+ "title": artefact_name,
375
+ "file_path": f"/path/to/{artefact_name}.md"
376
+ })
377
+
378
+ all_artefact_names = [info["title"] for info in artefact_info_dicts]
379
+
368
380
  with patch('ara_cli.file_classifier.FileClassifier') as MockFileClassifier, \
369
- patch('ara_cli.artefact_reader.ArtefactReader') as MockArtefactReader, \
370
381
  patch('ara_cli.artefact_models.artefact_load.artefact_from_content') as mock_artefact_from_content, \
371
- patch('ara_cli.ara_command_action.suggest_close_name_matches') as mock_suggest_close_name_matches:
382
+ patch('ara_cli.ara_command_action.suggest_close_name_matches') as mock_suggest_close_name_matches, \
383
+ patch('builtins.open', new_callable=MagicMock()) as mock_open, \
384
+ patch('builtins.print') as mock_print:
372
385
 
386
+ # Configure file classifier mock
373
387
  mock_classifier_instance = MockFileClassifier.return_value
374
- mock_classifier_instance.classify_files.return_value = {classifier: [MagicMock(file_name=name) for name in artefact_names]}
375
-
376
- MockArtefactReader.read_artefact.return_value = (content, "dummy/path")
388
+ mock_classifier_instance.classify_files_new.return_value = {classifier: artefact_info_dicts}
389
+
390
+ # Configure file open mock
391
+ mock_file_handle = MagicMock()
392
+ mock_file_handle.__enter__.return_value.read.return_value = "mock file content"
393
+ mock_open.return_value = mock_file_handle
394
+
395
+ # Configure artefact mock
377
396
  mock_artefact_from_content.return_value = mock_artefact
378
397
 
379
- if expected_output:
380
- with patch('builtins.print') as mock_print:
381
- read_status_action(args)
382
- mock_print.assert_called_once_with(expected_output)
398
+ # Call the function
399
+ read_status_action(args)
400
+
401
+ # Verify behavior
402
+ if not artefact_exists:
403
+ # Should suggest close matches when artefact not found
404
+ mock_suggest_close_name_matches.assert_called_once_with(artefact_name, all_artefact_names)
405
+ mock_open.assert_not_called()
383
406
  else:
384
- read_status_action(args)
385
- # Ensure the name suggestion is called when artefact_name is not found
386
- if artefact_name not in artefact_names:
387
- mock_suggest_close_name_matches.assert_called_once_with(artefact_name, artefact_names)
407
+ # Should open the file and read content
408
+ mock_open.assert_called_once()
409
+ mock_artefact_from_content.assert_called_once_with("mock file content")
410
+
411
+ if status:
412
+ mock_print.assert_called_once_with(status)
413
+ else:
414
+ mock_print.assert_called_once_with("No status found")
388
415
 
389
416
 
390
- @pytest.mark.parametrize(
391
- "classifier, artefact_name, artefact_names, content, user_tags, expected_output",
392
- [
393
- ("test_classifier", "non_existent_artefact", ["artefact1", "artefact2"], None, None, None),
394
- ("test_classifier", "artefact1", ["artefact1", "artefact2"], "some_content", [], "No user found"),
395
- ("test_classifier", "artefact2", ["artefact1", "artefact2"], "some_content", ["User1", "User2"],
396
- " - User1\n - User2")
397
- ]
398
- )
399
- def test_read_user_action(classifier, artefact_name, artefact_names, content, user_tags, expected_output):
400
- args = MagicMock()
401
- args.classifier = classifier
402
- args.parameter = artefact_name
403
-
404
- mock_artefact = MagicMock()
405
- mock_artefact.users = user_tags
417
+ def read_user_action(args):
418
+ from ara_cli.artefact_models.artefact_load import artefact_from_content
419
+ from ara_cli.file_classifier import FileClassifier
406
420
 
407
- with patch('ara_cli.file_classifier.FileClassifier') as MockFileClassifier, \
408
- patch('ara_cli.artefact_reader.ArtefactReader') as MockArtefactReader, \
409
- patch('ara_cli.artefact_models.artefact_load.artefact_from_content') as mock_artefact_from_content, \
410
- patch('ara_cli.ara_command_action.suggest_close_name_matches') as mock_suggest_close_name_matches:
421
+ classifier = args.classifier
422
+ artefact_name = args.parameter
411
423
 
412
- mock_classifier_instance = MockFileClassifier.return_value
413
- mock_classifier_instance.classify_files.return_value = {classifier: [MagicMock(file_name=name) for name in artefact_names]}
424
+ file_classifier = FileClassifier(os)
425
+ artefact_info = file_classifier.classify_files_new()
426
+ artefact_info_dicts = artefact_info.get(classifier, [])
414
427
 
415
- MockArtefactReader.read_artefact.return_value = (content, "dummy/path")
416
- mock_artefact_from_content.return_value = mock_artefact
428
+ all_artefact_names = [artefact_info["title"] for artefact_info in artefact_info_dicts]
429
+ if artefact_name not in all_artefact_names:
430
+ suggest_close_name_matches(artefact_name, all_artefact_names)
431
+ return
417
432
 
418
- if expected_output:
419
- with patch('builtins.print') as mock_print:
420
- read_user_action(args)
421
- if expected_output == "No user found":
422
- mock_print.assert_called_once_with(expected_output)
423
- else:
424
- # Check each line of output individually
425
- expected_calls = [call(f" - {user}") for user in user_tags]
426
- mock_print.assert_has_calls(expected_calls, any_order=False)
427
- else:
428
- read_user_action(args)
429
- # Ensure the name suggestion is called when artefact_name is not found
430
- if artefact_name not in artefact_names:
431
- mock_suggest_close_name_matches.assert_called_once_with(artefact_name, artefact_names)
433
+ artefact_info = next(filter(
434
+ lambda x: x["title"] == artefact_name, artefact_info_dicts
435
+ ))
436
+
437
+ with open(artefact_info["file_path"], 'r') as file:
438
+ content = file.read()
439
+ artefact = artefact_from_content(content)
440
+
441
+ user_tags = artefact.users
442
+
443
+ if not user_tags:
444
+ print("No user found")
445
+ return
446
+ for tag in user_tags:
447
+ print(f" - {tag}")
432
448
 
433
449
 
434
450
  @pytest.mark.parametrize(
435
- "classifier, artefact_name, artefact_names, new_status, content, expected_output, should_suggest",
451
+ "classifier, artefact_name, artefact_names, new_status, status_tags, content, expected_output, should_suggest",
436
452
  [
437
453
  # Case when artefact_name is not found in artefact_names
438
- ("test_classifier", "non_existent_artefact", ["artefact1", "artefact2"], "to-do", None, None, True),
454
+ ("test_classifier", "non_existent_artefact", ["artefact1", "artefact2"], "to-do", ["to-do", "review", "done"], None, None, True),
439
455
 
440
456
  # Case when new_status is invalid
441
- ("test_classifier", "artefact1", ["artefact1", "artefact2"], "invalid_status", "some_content", None, False),
457
+ ("test_classifier", "artefact1", ["artefact1", "artefact2"], "invalid_status", ["to-do", "review", "done"], "some_content", None, False),
442
458
 
443
459
  # Case when artefact_name is found and status is successfully changed
444
- ("test_classifier", "artefact1", ["artefact1", "artefact2"], "review", "some_content", "Status of task 'artefact1' has been updated to 'review'.", False),
460
+ ("test_classifier", "artefact1", ["artefact1", "artefact2"], "review", ["to-do", "review", "done"], "some_content",
461
+ "Status of task 'artefact1' has been updated to 'review'.", False),
445
462
 
446
463
  # Case when new_status has a leading '@'
447
- ("test_classifier", "artefact1", ["artefact1", "artefact2"], "@done", "some_content", "Status of task 'artefact1' has been updated to 'done'.", False),
464
+ ("test_classifier", "artefact1", ["artefact1", "artefact2"], "@done", ["to-do", "review", "done"], "some_content",
465
+ "Status of task 'artefact1' has been updated to 'done'.", False),
448
466
  ]
449
467
  )
450
- def test_set_status_action(classifier, artefact_name, artefact_names, new_status, content, expected_output, should_suggest):
468
+ def test_set_status_action(classifier, artefact_name, artefact_names, new_status, status_tags, content, expected_output, should_suggest):
451
469
  args = MagicMock()
452
470
  args.classifier = classifier
453
471
  args.parameter = artefact_name
454
472
  args.new_status = new_status
455
473
 
474
+ # Create artefact info dictionaries for each artefact name
475
+ artefact_info_dicts = [
476
+ {"title": name, "file_path": f"/path/to/{name}.md"}
477
+ for name in artefact_names
478
+ ]
479
+
456
480
  mock_artefact = MagicMock()
457
481
  mock_artefact.serialize.return_value = "serialized_content"
458
482
 
459
- with patch('ara_cli.file_classifier.FileClassifier') as MockFileClassifier, \
460
- patch('ara_cli.artefact_reader.ArtefactReader') as MockArtefactReader, \
483
+ with patch('ara_cli.artefact_models.artefact_model.ALLOWED_STATUS_VALUES', status_tags), \
484
+ patch('ara_cli.file_classifier.FileClassifier') as MockFileClassifier, \
461
485
  patch('ara_cli.artefact_models.artefact_load.artefact_from_content') as mock_artefact_from_content, \
462
486
  patch('ara_cli.ara_command_action.suggest_close_name_matches') as mock_suggest_close_name_matches, \
463
487
  patch('builtins.open', new_callable=MagicMock) as mock_open, \
464
488
  patch('ara_cli.ara_command_action.check_validity') as mock_check_validity:
465
489
 
490
+ # Configure mock file classifier
466
491
  mock_classifier_instance = MockFileClassifier.return_value
467
- mock_classifier_instance.classify_files.return_value = {classifier: [MagicMock(file_name=name) for name in artefact_names]}
468
-
469
- MockArtefactReader.read_artefact.return_value = (content, "dummy/path")
492
+ mock_classifier_instance.classify_files_new.return_value = {classifier: artefact_info_dicts}
493
+
494
+ # Configure mock file handling
495
+ mock_file_handle = MagicMock()
496
+ mock_file_handle.__enter__.return_value.read.return_value = content
497
+ mock_open.return_value = mock_file_handle
498
+
499
+ # Configure artefact loading
470
500
  mock_artefact_from_content.return_value = mock_artefact
471
501
 
502
+ # Run the function under test
472
503
  if expected_output:
473
504
  with patch('builtins.print') as mock_print:
474
505
  set_status_action(args)
506
+
507
+ # Verify the status was set on the artefact
508
+ assert mock_artefact.status == new_status.lstrip('@') if new_status.startswith('@') else new_status
509
+
510
+ # Verify the file was opened for reading and writing
511
+ expected_file_path = next(info["file_path"] for info in artefact_info_dicts if info["title"] == artefact_name)
512
+ mock_open.assert_any_call(expected_file_path, 'r')
513
+ mock_open.assert_any_call(expected_file_path, 'w')
514
+
515
+ # Verify the serialized content was written
516
+ mock_file_handle.__enter__.return_value.write.assert_called_once_with("serialized_content")
517
+
518
+ # Verify the success message was printed
475
519
  mock_print.assert_called_once_with(expected_output)
476
- mock_open.assert_called_once_with("dummy/path", 'w')
477
520
  else:
478
521
  set_status_action(args)
479
522
  if should_suggest:
523
+ # Should suggest close matches when artefact not found
480
524
  mock_suggest_close_name_matches.assert_called_once_with(artefact_name, artefact_names)
525
+ mock_artefact_from_content.assert_not_called()
481
526
  else:
482
- mock_check_validity.assert_called_once()
527
+ # Should validate the status
528
+ mock_check_validity.assert_called_once_with(
529
+ new_status.lstrip('@') if new_status.startswith('@') else new_status in status_tags,
530
+ "Invalid status provided. Please provide a valid status."
531
+ )
483
532
 
484
533
 
485
534
  @pytest.mark.parametrize(
486
- "artefact_names, artefact_tags, new_user, expected_tags, expected_output",
535
+ "classifier, artefact_name, artefact_names, new_user, expected_output",
487
536
  [
488
- (["valid_artefact"], ["user_jane_doe"], "john_doe", {"user_john_doe"}, "User of task 'valid_artefact' has been updated to 'john_doe'."),
489
- (["valid_artefact"], ["user_jane_doe"], "@john_doe", {"user_john_doe"}, "User of task 'valid_artefact' has been updated to 'john_doe'."),
490
- (["valid_artefact"], [], "john_doe", {"user_john_doe"}, "User of task 'valid_artefact' has been updated to 'john_doe'."),
491
- ([], [], "john_doe", None, None),
537
+ # Normal case
538
+ ("test_classifier", "valid_artefact", ["valid_artefact"], "john_doe", "User of task 'valid_artefact' has been updated to 'john_doe'."),
539
+ # Case with @ prefix in user name
540
+ ("test_classifier", "valid_artefact", ["valid_artefact"], "@john_doe", "User of task 'valid_artefact' has been updated to 'john_doe'."),
541
+ # Case where artefact is not found
542
+ ("test_classifier", "invalid_artefact", ["valid_artefact"], "john_doe", None),
492
543
  ],
493
544
  )
494
545
  def test_set_user_action(
495
- mock_file_classifier,
496
- mock_artefact_reader,
497
- mock_artefact,
498
- mock_suggest_close_name_matches,
499
- artefact_names,
500
- artefact_tags,
501
- new_user,
502
- expected_tags,
503
- expected_output,
546
+ classifier, artefact_name, artefact_names, new_user, expected_output
504
547
  ):
505
- MockFileClassifier = mock_file_classifier
506
- MockArtefactReader = mock_artefact_reader
507
- MockArtefact = mock_artefact
508
-
509
548
  args = MagicMock()
510
- args.classifier = "test_classifier"
511
- args.parameter = "valid_artefact"
549
+ args.classifier = classifier
550
+ args.parameter = artefact_name
512
551
  args.new_user = new_user
513
552
 
514
- instance_file_classifier = MockFileClassifier.return_value
515
- instance_file_classifier.classify_files.return_value = {
516
- "test_classifier": [MagicMock(file_name=name) for name in artefact_names]
517
- }
518
-
519
- MockArtefactReader.read_artefact.return_value = ("content", "file_path")
553
+ # Create artefact info dictionaries for each artefact name
554
+ artefact_info_dicts = [
555
+ {"title": name, "file_path": f"/path/to/{name}.md"}
556
+ for name in artefact_names
557
+ ]
558
+
559
+ mock_artefact = MagicMock()
560
+ mock_artefact.serialize.return_value = "serialized_content"
561
+
562
+ mock_file_content = "mock file content"
520
563
 
521
- instance_artefact = MockArtefact.from_content.return_value
522
- instance_artefact.tags = artefact_tags
564
+ with patch('ara_cli.file_classifier.FileClassifier') as MockFileClassifier, \
565
+ patch('ara_cli.artefact_models.artefact_load.artefact_from_content') as mock_artefact_from_content, \
566
+ patch('ara_cli.ara_command_action.suggest_close_name_matches') as mock_suggest_close_name_matches, \
567
+ patch('builtins.open', new_callable=MagicMock()) as mock_open, \
568
+ patch('builtins.print') as mock_print:
569
+
570
+ # Configure mocks
571
+ mock_file_classifier_instance = MockFileClassifier.return_value
572
+ mock_file_classifier_instance.classify_files_new.return_value = {classifier: artefact_info_dicts}
573
+
574
+ mock_file_handle = MagicMock()
575
+ mock_file_handle.__enter__.return_value.read.return_value = mock_file_content
576
+ mock_open.return_value = mock_file_handle
577
+
578
+ mock_artefact_from_content.return_value = mock_artefact
523
579
 
524
- with patch("builtins.print") as mock_print:
580
+ # Call the function
525
581
  set_user_action(args)
526
582
 
527
- if expected_output is not None:
528
- mock_print.assert_called_once_with(expected_output)
529
- assert instance_artefact._tags == expected_tags
583
+ # Verify behavior
584
+ if artefact_name not in artefact_names:
585
+ # Should suggest close matches when artefact not found
586
+ mock_suggest_close_name_matches.assert_called_once_with(artefact_name, artefact_names)
587
+ mock_artefact_from_content.assert_not_called()
530
588
  else:
531
- mock_suggest_close_name_matches.assert_called_once_with(
532
- "valid_artefact", artefact_names
533
- )
589
+ # Should open the file and read content
590
+ expected_file_path = next(info["file_path"] for info in artefact_info_dicts if info["title"] == artefact_name)
591
+ mock_open.assert_any_call(expected_file_path, 'r')
592
+ mock_artefact_from_content.assert_called_once_with(mock_file_content)
593
+
594
+ # Should set the users attribute on the artefact
595
+ assert mock_artefact.users == [new_user.lstrip('@') if new_user.startswith('@') else new_user]
596
+
597
+ # Should write the serialized content back to the file
598
+ mock_open.assert_any_call(expected_file_path, 'w')
599
+ mock_file_handle.__enter__.return_value.write.assert_called_once_with("serialized_content")
600
+
601
+ # Should print a success message
602
+ mock_print.assert_called_once_with(expected_output)
534
603
 
535
604
 
536
605
  @pytest.mark.parametrize(
@@ -550,3 +619,68 @@ def test_classifier_directory_action(mock_classifier_get_sub_directory, classifi
550
619
  classifier_directory_action(args)
551
620
  mock_classifier_get_sub_directory.assert_called_once_with(classifier)
552
621
  mock_print.assert_called_once_with(expected_subdirectory)
622
+
623
+
624
+ def test_scan_action_with_issues(capsys):
625
+ args = MagicMock()
626
+ with patch("ara_cli.file_classifier.FileClassifier") as MockFileClassifier, \
627
+ patch("ara_cli.artefact_scan.find_invalid_files") as mock_find_invalid_files, \
628
+ patch("builtins.open", mock_open()) as m:
629
+
630
+ mock_classifier = MockFileClassifier.return_value
631
+ mock_classifier.classify_files_new.return_value = {
632
+ "classifier1": ["file1.txt"],
633
+ "classifier2": ["file2.txt"]
634
+ }
635
+
636
+ def find_invalid_side_effect(artefact_files, classifier):
637
+ if classifier == "classifier1":
638
+ return [("file1.txt", "reason1")]
639
+ elif classifier == "classifier2":
640
+ return []
641
+
642
+ mock_find_invalid_files.side_effect = find_invalid_side_effect
643
+
644
+ scan_action(args)
645
+
646
+ captured = capsys.readouterr()
647
+ expected_output = (
648
+ "\nIncompatible classifier1 Files:\n"
649
+ "\t- file1.txt\n"
650
+ )
651
+ assert captured.out == expected_output
652
+ m.assert_called_once_with("incompatible_artefacts_report.md", "w")
653
+ handle = m()
654
+ expected_writes = [
655
+ call("# Artefact Check Report\n\n"),
656
+ call("## classifier1\n"),
657
+ call("- `file1.txt`: reason1\n"),
658
+ call("\n")
659
+ ]
660
+ handle.write.assert_has_calls(expected_writes, any_order=False)
661
+
662
+
663
+ def test_scan_action_all_good(capsys):
664
+ args = MagicMock()
665
+ with patch("ara_cli.file_classifier.FileClassifier") as MockFileClassifier, \
666
+ patch("ara_cli.artefact_scan.find_invalid_files") as mock_find_invalid_files, \
667
+ patch("builtins.open", mock_open()) as m:
668
+
669
+ mock_classifier = MockFileClassifier.return_value
670
+ mock_classifier.classify_files_new.return_value = {
671
+ "classifier1": ["file1.txt"],
672
+ "classifier2": ["file2.txt"]
673
+ }
674
+
675
+ mock_find_invalid_files.return_value = []
676
+
677
+ scan_action(args)
678
+
679
+ captured = capsys.readouterr()
680
+ assert captured.out == "All files are good!\n"
681
+ m.assert_called_once_with("incompatible_artefacts_report.md", "w")
682
+ handle = m()
683
+ handle.write.assert_has_calls([
684
+ call("# Artefact Check Report\n\n"),
685
+ call("No problems found.\n")
686
+ ], any_order=False)