ara-cli 0.1.9.75__py3-none-any.whl → 0.1.9.77__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/chat.py CHANGED
@@ -34,6 +34,8 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
34
34
  ".jpg": "image/jpeg",
35
35
  ".jpeg": "image/jpeg",
36
36
  }
37
+
38
+ DOCUMENT_TYPE_EXTENSIONS = [".docx", ".doc", ".odt", ".pdf"]
37
39
 
38
40
  def __init__(
39
41
  self,
@@ -371,9 +373,52 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
371
373
  with open(self.chat_name, 'a', encoding='utf-8') as chat_file:
372
374
  chat_file.write(write_content)
373
375
  return True
376
+
377
+ def read_docx(self, file_path):
378
+ import docx
379
+ doc = docx.Document(file_path)
380
+ return '\n'.join(para.text for para in doc.paragraphs)
381
+
382
+ def read_pdf(self, file_path):
383
+ import pymupdf4llm
384
+ return pymupdf4llm.to_markdown(file_path, write_images=False)
385
+
386
+ def read_odt(self, file_path):
387
+ import pymupdf4llm
388
+ return pymupdf4llm.to_markdown(file_path, write_images=False)
389
+
390
+ @file_exists_check
391
+ def load_document_file(self, file_path: str, prefix: str = "", suffix: str = "", block_delimiter: str = "```"):
392
+ import os
393
+
394
+ _, ext = os.path.splitext(file_path)
395
+ ext = ext.lower()
396
+
397
+ text_content = ""
398
+ match ext:
399
+ case ".docx":
400
+ text_content = self.read_docx(file_path)
401
+ case ".pdf":
402
+ text_content = self.read_pdf(file_path)
403
+ case ".odt":
404
+ text_content = self.read_odt(file_path)
405
+ # Add more cases if needed.
406
+ case _:
407
+ print("Unsupported document type.")
408
+ return False
409
+
410
+ if block_delimiter:
411
+ text_content = f"{block_delimiter}\n{text_content}\n{block_delimiter}"
412
+
413
+ write_content = f"{prefix}{text_content}{suffix}\n"
414
+
415
+ with open(self.chat_name, 'a', encoding='utf-8') as chat_file:
416
+ chat_file.write(write_content)
417
+ return True
374
418
 
375
419
  def load_file(self, file_name: str, prefix: str = "", suffix: str = "", block_delimiter: str = ""):
376
420
  binary_type_mapping = Chat.BINARY_TYPE_MAPPING
421
+ document_type_extensions = Chat.DOCUMENT_TYPE_EXTENSIONS
377
422
 
378
423
  file_type = None
379
424
  file_name_lower = file_name.lower()
@@ -382,7 +427,16 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
382
427
  file_type = mime_type
383
428
  break
384
429
 
385
- if file_type:
430
+ is_file_document = any(file_name_lower.endswith(ext) for ext in document_type_extensions)
431
+
432
+ if is_file_document:
433
+ return self.load_document_file(
434
+ file_name=file_name,
435
+ prefix=prefix,
436
+ suffix=suffix,
437
+ block_delimiter=block_delimiter
438
+ )
439
+ elif file_type:
386
440
  return self.load_binary_file(
387
441
  file_name=file_name,
388
442
  mime_type=file_type,
@@ -503,6 +557,19 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
503
557
  return False
504
558
  return True
505
559
 
560
+ @cmd2.with_category(CATEGORY_CHAT_CONTROL)
561
+ def do_LOAD_DOCUMENT(self, file_name):
562
+ """Load a document file (PDF, DOCX, DOC, ODT) and append its text content to chat file. Can be given the file name in-line. Will attempt to find the file relative to chat file first, then treat the given path as absolute"""
563
+ matching_files = self.find_matching_files_to_load(file_name)
564
+ if not matching_files:
565
+ return
566
+
567
+ for file_path in matching_files:
568
+ prefix = f"\nFile: {file_path}\n"
569
+ self.add_prompt_tag_if_needed(self.chat_name)
570
+ if not os.path.isdir(file_path) and self.load_document_file(file_path, prefix=prefix):
571
+ print(f"Loaded document file {file_path}")
572
+
506
573
  @cmd2.with_category(CATEGORY_CHAT_CONTROL)
507
574
  def do_LOAD_IMAGE(self, file_name):
508
575
  """Load an image file and append it to chat file. Can be given the file name in-line. Will attempt to find the file relative to chat file first, then treat the given path as absolute"""
ara_cli/version.py CHANGED
@@ -1,2 +1,2 @@
1
1
  # version.py
2
- __version__ = "0.1.9.75" # fith parameter like .0 for local install test purposes only. official numbers should be 4 digit numbers
2
+ __version__ = "0.1.9.77" # fith parameter like .0 for local install test purposes only. official numbers should be 4 digit numbers
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ara_cli
3
- Version: 0.1.9.75
3
+ Version: 0.1.9.77
4
4
  Requires-Dist: litellm
5
5
  Requires-Dist: llama-index
6
6
  Requires-Dist: llama-index-llms-openai
@@ -13,4 +13,6 @@ Requires-Dist: argcomplete
13
13
  Requires-Dist: cmd2>=2.5
14
14
  Requires-Dist: pydantic
15
15
  Requires-Dist: pydantic_ai
16
+ Requires-Dist: python-docx
17
+ Requires-Dist: pymupdf4llm
16
18
  Dynamic: requires-dist
@@ -1,18 +1,18 @@
1
1
  ara_cli/__init__.py,sha256=0zl7IegxTid26EBGLav_fXZ4CCIV3H5TfAoFQiOHjvg,148
2
- ara_cli/__main__.py,sha256=Z6XYWRLceIoZPvfC-X9EXouSZdtFOOe84kKVWJGA4r4,1861
2
+ ara_cli/__main__.py,sha256=ppfq0FIi4x6ONRzP67784A4BPo2labh8Bd_EExuXo4U,2011
3
3
  ara_cli/ara_command_action.py,sha256=J613DUTjRxrPG8Jm-fJcIM0QlZTeULmq9Q7DKkDxJHg,22039
4
- ara_cli/ara_command_parser.py,sha256=HHuLxGeLjsd3R-JcoWJ5MUgaxXqkECHoqE95amlaVXY,18115
4
+ ara_cli/ara_command_parser.py,sha256=vyxLELnyAZFC2C3v0hH4-r9QBmJ8oIs0hCZukkMFXfc,20136
5
5
  ara_cli/ara_config.py,sha256=SgZfQVpqj5JJN4SB0n2IvAH0sKIdS3k1K1Zht2wDywA,8814
6
- ara_cli/artefact_autofix.py,sha256=XT-OGiznPCX7b6wwxFigHaVt5KzC2pFm6IT8HuwiSVs,17519
6
+ ara_cli/artefact_autofix.py,sha256=WVTiIR-jo4YKmmz4eS3qTFvl45W1YKwAk1XSuz9QX10,20015
7
7
  ara_cli/artefact_creator.py,sha256=0Ory6cB-Ahkw-BDNb8QHnTbp_OHGABdkb9bhwcEdcIc,6063
8
8
  ara_cli/artefact_deleter.py,sha256=Co4wwCH3yW8H9NrOq7_2p5571EeHr0TsfE-H8KqoOfY,1900
9
9
  ara_cli/artefact_fuzzy_search.py,sha256=iBlDqjZf-_D3VUjFf7ZwkiQbpQDcwRndIU7aG_sRTgE,2668
10
10
  ara_cli/artefact_link_updater.py,sha256=nKdxTpDKqWTOAMD8viKmUaklSFGWzJZ8S8E8xW_ADuM,3775
11
11
  ara_cli/artefact_lister.py,sha256=jhk4n4eqp7hDIq07q43QzS7-36BM3OfZ4EABxCeOGcw,4764
12
- ara_cli/artefact_reader.py,sha256=E6DMBvbOYf1OoLf-OyLaiB6K2-gd7iHmjoQZU9Rsy6g,6965
12
+ ara_cli/artefact_reader.py,sha256=Pho0_Eqm7kD9CNbVMhKb6mkNM0I3iJiCJXbXmVp1DJU,7827
13
13
  ara_cli/artefact_renamer.py,sha256=Hnz_3zD9xxnBa1FHyUE6mIktLk_9ttP2rFRvQIkmz-o,4061
14
14
  ara_cli/artefact_scan.py,sha256=msPCm-vPWOAZ_e_z5GylXxq1MtNlmJ4zvKrsdOFCWF4,4813
15
- ara_cli/chat.py,sha256=CR30wBfKaS8DVCeavEEYFMeCFp3IyRtybQnk-GrCj7U,29341
15
+ ara_cli/chat.py,sha256=Plje33XcOedSx-nmLCkuFIXSqHPIvMcy5I71xYWuYmU,31956
16
16
  ara_cli/classifier.py,sha256=zWskj7rBYdqYBGjksBm46iTgVU5IIf2PZsJr4qeiwVU,1878
17
17
  ara_cli/codefusionretriever.py,sha256=fCHgXdIBRzkVAnapX-KI2NQ44XbrrF4tEQmn5J6clUI,1980
18
18
  ara_cli/codehierachieretriever.py,sha256=Xd3EgEWWhkSf1TmTWtf8X5_YvyE_4B66nRrqarwSiTU,1182
@@ -31,9 +31,9 @@ ara_cli/run_file_lister.py,sha256=XbrrDTJXp1LFGx9Lv91SNsEHZPP-PyEMBF_P4btjbDA,23
31
31
  ara_cli/tag_extractor.py,sha256=TGdaQOVnjy25R0zDsAifB67C5oom0Fwo24s0_fr5A_I,3151
32
32
  ara_cli/template_manager.py,sha256=YwrN6AYPpl6ZrW8BVQpVXx8yTRf-oNpJUIKeg4NAggs,6606
33
33
  ara_cli/update_config_prompt.py,sha256=Oy9vNTw6UhDohyTEfSKkqE5ifEMPlmWNYkKHgUrK_pY,4607
34
- ara_cli/version.py,sha256=pKiqeeWiFsEsFOcA9URWYucMsyrAfEmlaiQ8MVZ8ApI,146
34
+ ara_cli/version.py,sha256=YZaVX1MbXXFpamZLvZnUrGSRq0Szw8TohbROLVLDRkA,146
35
35
  ara_cli/artefact_models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
- ara_cli/artefact_models/artefact_load.py,sha256=dNcwZDW2Dk0bts9YnPZ0ESmWD2NbsLIvl4Z-qQeGmTQ,401
36
+ ara_cli/artefact_models/artefact_load.py,sha256=IXzWxP-Q_j_oDGMno0m-OuXCQ7Vd5c_NctshGr4ROBw,621
37
37
  ara_cli/artefact_models/artefact_mapping.py,sha256=8aD0spBjkJ8toMAmFawc6UTUxB6-tEEViZXv2I-r88Q,1874
38
38
  ara_cli/artefact_models/artefact_model.py,sha256=qSbcrmFWAYgBqcNl9QARI1_uLQJm-TPVgP5q2AEFnjE,15983
39
39
  ara_cli/artefact_models/artefact_templates.py,sha256=8HNM-TsNvKgTpruOBs751yRDXJypTiJhc1tkWCiYG7s,9830
@@ -45,7 +45,7 @@ ara_cli/artefact_models/feature_artefact_model.py,sha256=FrR7_xydOmMySAz0QpWgrNF
45
45
  ara_cli/artefact_models/issue_artefact_model.py,sha256=v6CpKnkqiUh6Wch2kkEmyyW49c8ysdy1qz8l1Ft9uJA,2552
46
46
  ara_cli/artefact_models/keyfeature_artefact_model.py,sha256=J9oXLsCAo22AW31D5Z104y02ss0S0O4tPCcd09zYCD0,4066
47
47
  ara_cli/artefact_models/serialize_helper.py,sha256=Wks30wy-UrwJURetydKykLgJkdGRgXFHkDT24vHe5tU,595
48
- ara_cli/artefact_models/task_artefact_model.py,sha256=OrG7Z0u5QDGoS1a0PlWNxC2n47j9RtNZQo0rz1kNs84,5841
48
+ ara_cli/artefact_models/task_artefact_model.py,sha256=1BSMbz9D-RXvdpdd0RlAr9hUx84Rcuysk2YfQC8Qy14,6046
49
49
  ara_cli/artefact_models/userstory_artefact_model.py,sha256=2awH31ROtm7j4T44Bv4cylQDYLQtnfgXZMhDu_pgw-k,6435
50
50
  ara_cli/artefact_models/vision_artefact_model.py,sha256=frjaUJj-mmIlVHEhzAQztCGs-CtvNu_odSborgztfzo,5251
51
51
  ara_cli/templates/agile.artefacts,sha256=nTA8dp98HWKAD-0qhmNpVYIfkVGoJshZqMJGnphiOsE,7932
@@ -124,14 +124,14 @@ ara_cli/templates/specification_breakdown_files/template.technology.md,sha256=by
124
124
  tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
125
125
  tests/test_ara_command_action.py,sha256=JTLqXM9BSMlU33OQgrk_sZnoowFJZKZAx8q-st-wa34,25821
126
126
  tests/test_ara_config.py,sha256=pvkdPLTzgLkOijil0HaN0mhLC2Rdu4Fu5RfXEyOlRfs,16672
127
- tests/test_artefact_autofix.py,sha256=K5h-IcPHZHdbLFgQHoto3K4fgX8cNW-ddHgQwCAIlFc,22145
127
+ tests/test_artefact_autofix.py,sha256=pApZ-N0dW8Ujt-cNLbgvd4bhiIIK8oXb-saLf6QlA-8,25022
128
128
  tests/test_artefact_fuzzy_search.py,sha256=5Sh3_l9QK8-WHn6JpGPU1b6h4QEnl2JoMq1Tdp2cj1U,1261
129
129
  tests/test_artefact_link_updater.py,sha256=biqbEp2jCOz8giv72hu2P2hDfeJfJ9OrVGdAv5d9cK4,2191
130
130
  tests/test_artefact_lister.py,sha256=VCEOCgDgnAOeUUgIoGAbWgz60hf9UT-tdHg18LGfB34,22656
131
131
  tests/test_artefact_reader.py,sha256=660K-d8ed-j8hulsUB_7baPD2-hhbg9TffUR5yVc4Uo,927
132
132
  tests/test_artefact_renamer.py,sha256=lSnKCCfoFGgKhTdDZrEaeBq1xJAak1QoqH5aSeOe9Ro,3494
133
133
  tests/test_artefact_scan.py,sha256=uNWgrt7ieZ4ogKACsPqzAsh59JF2BhTKSag31hpVrTQ,16887
134
- tests/test_chat.py,sha256=BcVCGJjdBHEIXR9l-9Q83fQXP7Hly6yHewY2qEuZ1DE,49161
134
+ tests/test_chat.py,sha256=fUGqpsyilLjwIFNlCAC69pYGEhwRuU6pplywwGJk-K8,54907
135
135
  tests/test_classifier.py,sha256=grYGPksydNdPsaEBQxYHZTuTdcJWz7VQtikCKA6BNaQ,1920
136
136
  tests/test_directory_navigator.py,sha256=7G0MVrBbtBvbrFUpL0zb_9EkEWi1dulWuHsrQxMJxDY,140
137
137
  tests/test_file_classifier.py,sha256=kLWPiePu3F5mkVuI_lK_2QlLh2kXD_Mt2K8KZZ1fAnA,10940
@@ -141,8 +141,8 @@ tests/test_list_filter.py,sha256=fJA3d_SdaOAUkE7jn68MOVS0THXGghy1fye_64Zvo1U,796
141
141
  tests/test_tag_extractor.py,sha256=nSiAYlTKZ7TLAOtcJpwK5zTWHhFYU0tI5xKnivLc1dU,2712
142
142
  tests/test_template_manager.py,sha256=q-LMHRG4rHkD6ON6YW4cpZxUx9hul6Or8wVVRC2kb-8,4099
143
143
  tests/test_update_config_prompt.py,sha256=xsqj1WTn4BsG5Q2t-sNPfu7EoMURFcS-hfb5VSXUnJc,6765
144
- ara_cli-0.1.9.75.dist-info/METADATA,sha256=gWhv0kRyRzVik_sN-4cY1ILDTcqrWyE8p5y9d50ODpo,415
145
- ara_cli-0.1.9.75.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
146
- ara_cli-0.1.9.75.dist-info/entry_points.txt,sha256=v4h7MzysTgSIDYfEo3oj4Kz_8lzsRa3hq-KJHEcLVX8,45
147
- ara_cli-0.1.9.75.dist-info/top_level.txt,sha256=WM4cLHT5DYUaWzLtRj-gu3yVNFpGQ6lLRI3FMmC-38I,14
148
- ara_cli-0.1.9.75.dist-info/RECORD,,
144
+ ara_cli-0.1.9.77.dist-info/METADATA,sha256=HYjYn2M6mOc2aYugIuai2mhaV8uaDi2xWtT_x-p3aq0,469
145
+ ara_cli-0.1.9.77.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
146
+ ara_cli-0.1.9.77.dist-info/entry_points.txt,sha256=v4h7MzysTgSIDYfEo3oj4Kz_8lzsRa3hq-KJHEcLVX8,45
147
+ ara_cli-0.1.9.77.dist-info/top_level.txt,sha256=WM4cLHT5DYUaWzLtRj-gu3yVNFpGQ6lLRI3FMmC-38I,14
148
+ ara_cli-0.1.9.77.dist-info/RECORD,,
@@ -15,6 +15,7 @@ from ara_cli.artefact_autofix import (
15
15
  _has_valid_contribution,
16
16
  set_closest_contribution,
17
17
  fix_contribution,
18
+ fix_rule
18
19
  )
19
20
  from ara_cli.artefact_models.artefact_model import Artefact, ArtefactType, Contribution
20
21
 
@@ -60,6 +61,24 @@ def mock_artefact_with_contribution():
60
61
  return mock_artefact
61
62
 
62
63
 
64
+ @pytest.fixture
65
+ def mock_contribution():
66
+ m = MagicMock()
67
+ m.artefact_name = "parent_name"
68
+ m.classifier = "feature"
69
+ m.rule = "my_rule"
70
+ return m
71
+
72
+ @pytest.fixture
73
+ def mock_artefact(mock_contribution):
74
+ m = MagicMock()
75
+ m.contribution = mock_contribution
76
+ m._artefact_type.return_value.value = "requirement"
77
+ m.title = "my_title"
78
+ m.serialize.return_value = "serialized-text"
79
+ return m
80
+
81
+
63
82
  def test_read_report_file_success():
64
83
  """Tests successful reading of the report file."""
65
84
  mock_content = "# Artefact Check Report\n- `file.feature`: reason"
@@ -614,4 +633,72 @@ def test_apply_autofix_single_pass(
614
633
  assert "Single-pass mode enabled" in output
615
634
  assert "Attempt 1/1" in output
616
635
  assert "Attempt 2/1" not in output
617
- mock_check_file.assert_called_once()
636
+ mock_check_file.assert_called_once()
637
+
638
+
639
+ @patch("ara_cli.artefact_autofix._update_rule")
640
+ @patch("ara_cli.artefact_autofix.populate_classified_artefact_info")
641
+ def test_fix_rule_with_rule(mock_populate, mock_update_rule, mock_artefact, mock_contribution, capsys):
642
+ # Contribution has a rule
643
+ artefact_class = MagicMock()
644
+ artefact_class.deserialize.return_value = mock_artefact
645
+ mock_populate.return_value = {"info": "dummy"}
646
+
647
+ result = fix_rule(
648
+ file_path="dummy.feature",
649
+ artefact_text="text",
650
+ artefact_class=artefact_class,
651
+ classified_artefact_info={},
652
+ )
653
+
654
+ # deserialize called
655
+ artefact_class.deserialize.assert_called_once_with("text")
656
+ # _update_rule called with correct args
657
+ mock_update_rule.assert_called_once_with(
658
+ artefact=mock_artefact,
659
+ name="parent_name",
660
+ classifier="feature",
661
+ classified_file_info={"info": "dummy"},
662
+ delete_if_not_found=True,
663
+ )
664
+ # Feedback message contains rule
665
+ assert "with rule" in capsys.readouterr().out
666
+ # Result is the serialized text
667
+ assert result == "serialized-text"
668
+
669
+ @patch("ara_cli.artefact_autofix._update_rule")
670
+ @patch("ara_cli.artefact_autofix.populate_classified_artefact_info")
671
+ def test_fix_rule_without_rule(mock_populate, mock_update_rule, mock_artefact, mock_contribution, capsys):
672
+ # Contribution rule becomes None after update
673
+ mock_contribution.rule = None
674
+ artefact_class = MagicMock()
675
+ artefact_class.deserialize.return_value = mock_artefact
676
+ mock_populate.return_value = {"info": "dummy"}
677
+
678
+ result = fix_rule(
679
+ file_path="dummy.feature",
680
+ artefact_text="text",
681
+ artefact_class=artefact_class,
682
+ classified_artefact_info={},
683
+ )
684
+
685
+ # Feedback message says "without a rule"
686
+ assert "without a rule" in capsys.readouterr().out
687
+ assert result == "serialized-text"
688
+
689
+ @patch("ara_cli.artefact_autofix.populate_classified_artefact_info")
690
+ def test_fix_rule_contribution_none_raises(mock_populate):
691
+ # artefact.contribution is None: should assert
692
+ artefact = MagicMock()
693
+ artefact.contribution = None
694
+ artefact_class = MagicMock()
695
+ artefact_class.deserialize.return_value = artefact
696
+ mock_populate.return_value = {}
697
+
698
+ with pytest.raises(AssertionError):
699
+ fix_rule(
700
+ file_path="dummy.feature",
701
+ artefact_text="stuff",
702
+ artefact_class=artefact_class,
703
+ classified_artefact_info={},
704
+ )
tests/test_chat.py CHANGED
@@ -185,7 +185,7 @@ def test_disable_commands(temp_chat_file):
185
185
  (["This is a line.", "Another line here.", "Yet another line."], None),
186
186
  (["This is a line.", "# ara prompt:", "Another line here."], "# ara prompt:"),
187
187
  (["This is a line.", "# ara prompt:", "Another line here.", "# ara response:"], "# ara response:"),
188
- (["This is a line.", " # ara prompt: ", "Another line here.", " # ara response: "], "# ara response:"),
188
+ (["This is a line.", " # ara prompt: ", "Another line here.", " # ara response: "], "# ara response:"),
189
189
  (["# ara prompt:", "# ara response:"], "# ara response:"),
190
190
  (["# ara response:", "# ara prompt:", "# ara prompt:", "# ara response:"], "# ara response:"),
191
191
  ([], None)
@@ -233,8 +233,8 @@ def test_start(temp_chat_file):
233
233
  (["This is a line.\n", "# ara prompt:\n", "Another line here.\n", "# ara response:\n"],
234
234
  ["This is a line.\n", "# ara prompt:\n", "Another line here.\n", "# ara response:\n", "\n", "# ara prompt:"]),
235
235
 
236
- (["This is a line.\n", " # ara prompt: \n", "Another line here.\n", " # ara response: \n"],
237
- ["This is a line.\n", " # ara prompt: \n", "Another line here.\n", " # ara response: \n", "\n", "# ara prompt:"]),
236
+ (["This is a line.\n", " # ara prompt: \n", "Another line here.\n", " # ara response: \n"],
237
+ ["This is a line.\n", " # ara prompt: \n", "Another line here.\n", " # ara response: \n", "\n", "# ara prompt:"]),
238
238
 
239
239
  (["# ara prompt:\n", "# ara response:\n"],
240
240
  ["# ara prompt:\n", "# ara response:\n", "\n", "# ara prompt:"]),
@@ -257,7 +257,7 @@ def test_add_prompt_tag_if_needed(temp_chat_file, initial_content, expected_cont
257
257
 
258
258
 
259
259
  @pytest.mark.parametrize("lines, expected", [
260
- (["\n", " ", "# ara prompt:", "Another line here.", " \n"], "Another line here."),
260
+ (["\n", " ", "# ara prompt:", "Another line here.", " \n"], "Another line here."),
261
261
  (["This is a line.", "Another line here.", " \n", "\n"], "Another line here."),
262
262
  (["\n", " \n", " \n"], ""),
263
263
  (["This is a line.", "Another line here.", "# ara response:", " \n"], "# ara response:"),
@@ -270,7 +270,7 @@ def test_get_last_non_empty_line(lines, expected, temp_chat_file):
270
270
  assert Chat.get_last_non_empty_line(Chat, file) == expected
271
271
 
272
272
  @pytest.mark.parametrize("lines, expected", [
273
- (["\n", " ", "# ara prompt:", "Another line here.", " \n"], ""),
273
+ (["\n", " ", "# ara prompt:", "Another line here.", " \n"], ""),
274
274
  (["This is a line.", "Another line here."], "Another line here."),
275
275
  (["\n", " \n", " \n"], ""),
276
276
  (["This is a line.", "Another line here.", "# ara response:", " \n"], ""),
@@ -290,7 +290,7 @@ def test_get_last_line(lines, expected, temp_chat_file):
290
290
  (["Text with image", "(data:image/png;base64,abc123)"],
291
291
  "Text with image",
292
292
  [{"type": "image_url", "image_url": {"url": "data:image/png;base64,abc123"}}]),
293
- (["Just text", "Another (data:image/png;base64,xyz789) image"],
293
+ (["Just text", "Another (data:image/png;base64,xyz789) image"],
294
294
  "Just text",
295
295
  [{"type": "image_url", "image_url": {"url": "data:image/png;base64,xyz789"}}]),
296
296
  (["No images here at all"], "No images here at all", []),
@@ -392,7 +392,7 @@ def test_save_message(temp_chat_file, role, message, initial_content, expected_c
392
392
  def test_resend_message(temp_chat_file, initial_content, expected_content):
393
393
  temp_chat_file.writelines(initial_content)
394
394
  temp_chat_file.flush()
395
-
395
+
396
396
  mock_config = get_default_config()
397
397
  with patch('ara_cli.prompt_handler.ConfigManager.get_config', return_value=mock_config):
398
398
  chat = Chat(temp_chat_file.name, reset=False)
@@ -456,7 +456,7 @@ def test_determine_file_path(temp_chat_file):
456
456
  ]
457
457
 
458
458
  with patch('os.path.exists') as mock_exists, \
459
- patch('os.path.dirname', return_value="current_directory") as mock_dirname:
459
+ patch('os.path.dirname', return_value="current_directory") as mock_dirname:
460
460
 
461
461
  for file_name, exists_in_current, exists_elsewhere, expected_path in test_cases:
462
462
  mock_exists.side_effect = [exists_in_current, exists_elsewhere]
@@ -501,9 +501,9 @@ def test_load_text_file_file_not_found(temp_chat_file):
501
501
  with patch("builtins.open", mock_open()) as mock_file:
502
502
  result = chat.load_text_file("nonexistent.txt")
503
503
 
504
- assert result is False
504
+ assert result is False
505
505
 
506
- mock_file.assert_not_called()
506
+ mock_file.assert_not_called()
507
507
 
508
508
 
509
509
  @pytest.mark.parametrize("file_name, mime_type, file_content, expected, path_exists", [
@@ -516,7 +516,6 @@ def test_load_binary_file(temp_chat_file, file_name, mime_type, file_content, ex
516
516
  with patch('ara_cli.prompt_handler.ConfigManager.get_config', return_value=mock_config):
517
517
  chat = Chat(temp_chat_file.name, reset=False)
518
518
 
519
- # Mock open to handle both read and write operations
520
519
  mock_file = mock_open(read_data=file_content)
521
520
 
522
521
  with patch('builtins.open', mock_file) as mocked_open, \
@@ -535,36 +534,112 @@ def test_load_binary_file(temp_chat_file, file_name, mime_type, file_content, ex
535
534
  assert result is False
536
535
 
537
536
 
538
- @pytest.mark.parametrize("file_name, is_binary", [
539
- ("image.png", True), # Binary file
540
- ("document.txt", False) # Text file
537
+ @pytest.mark.parametrize("file_name, loader_path, mock_setup, expected_content", [
538
+ (
539
+ "test.docx",
540
+ "docx.Document",
541
+ lambda mock: setattr(mock.return_value, 'paragraphs', [MagicMock(text="Docx content")]),
542
+ "Docx content"
543
+ ),
544
+ pytest.param(
545
+ "test.pdf",
546
+ "pymupdf4llm.to_markdown",
547
+ lambda mock: setattr(mock, 'return_value', "PDF content"),
548
+ "PDF content",
549
+ marks=pytest.mark.filterwarnings("ignore::DeprecationWarning")
550
+ ),
551
+ pytest.param(
552
+ "test.odt",
553
+ "pymupdf4llm.to_markdown",
554
+ lambda mock: setattr(mock, 'return_value', "ODT content"),
555
+ "ODT content",
556
+ marks=pytest.mark.filterwarnings("ignore::DeprecationWarning")
557
+ ),
541
558
  ])
542
- def test_load_file(temp_chat_file, file_name, is_binary):
559
+ def test_load_document_file(temp_chat_file, file_name, loader_path, mock_setup, expected_content):
560
+ mock_config = get_default_config()
561
+ with patch('ara_cli.prompt_handler.ConfigManager.get_config', return_value=mock_config):
562
+ chat = Chat(temp_chat_file.name, reset=False)
563
+
564
+ with patch(loader_path, create=True) as mock_loader, \
565
+ patch("builtins.open", mock_open()) as mock_chat_open:
566
+
567
+ mock_setup(mock_loader)
568
+
569
+ with patch.object(chat, 'determine_file_path', return_value=file_name):
570
+ result = chat.load_document_file(file_name, prefix="Prefix-", suffix="-Suffix", block_delimiter="```")
571
+
572
+ assert result is True
573
+
574
+ if loader_path == "pymupdf4llm.to_markdown":
575
+ mock_loader.assert_called_once_with(file_name, write_images=False)
576
+ else:
577
+ mock_loader.assert_called_once_with(file_name)
578
+
579
+ expected_write = f"Prefix-```\n{expected_content}\n```-Suffix\n"
580
+ mock_chat_open.assert_called_with(chat.chat_name, 'a', encoding='utf-8')
581
+ mock_chat_open().write.assert_called_once_with(expected_write)
582
+
583
+
584
+ def test_load_document_file_unsupported(temp_chat_file, capsys):
585
+ mock_config = get_default_config()
586
+ with patch('ara_cli.prompt_handler.ConfigManager.get_config', return_value=mock_config):
587
+ chat = Chat(temp_chat_file.name, reset=False)
588
+
589
+ unsupported_file = "test.txt"
590
+ with patch.object(chat, 'determine_file_path', return_value=unsupported_file):
591
+ result = chat.load_document_file(unsupported_file)
592
+
593
+ assert result is False
594
+ captured = capsys.readouterr()
595
+ assert "Unsupported document type." in captured.out
596
+
597
+
598
+ @pytest.mark.parametrize("file_name, file_type, mime_type", [
599
+ ("image.png", "binary", "image/png"),
600
+ ("document.txt", "text", None),
601
+ ("document.docx", "document", None),
602
+ ("document.pdf", "document", None),
603
+ ("archive.zip", "text", None),
604
+ ])
605
+ def test_load_file(temp_chat_file, file_name, file_type, mime_type):
543
606
  mock_config = get_default_config()
544
607
  with patch('ara_cli.prompt_handler.ConfigManager.get_config', return_value=mock_config):
545
608
  chat = Chat(temp_chat_file.name, reset=False)
546
609
 
547
610
  with patch.object(chat, 'load_binary_file', return_value=True) as mock_load_binary, \
548
- patch.object(chat, 'load_text_file', return_value=True) as mock_load_text:
611
+ patch.object(chat, 'load_text_file', return_value=True) as mock_load_text, \
612
+ patch.object(chat, 'load_document_file', return_value=True) as mock_load_document:
549
613
 
550
- chat.load_file(file_name=file_name)
614
+ chat.load_file(file_name=file_name, prefix="p-", suffix="-s", block_delimiter="b")
551
615
 
552
- if is_binary:
616
+ if file_type == "binary":
553
617
  mock_load_binary.assert_called_once_with(
554
618
  file_name=file_name,
555
- mime_type='image/png',
556
- prefix="",
557
- suffix=""
619
+ mime_type=mime_type,
620
+ prefix="p-",
621
+ suffix="-s"
558
622
  )
559
623
  mock_load_text.assert_not_called()
624
+ mock_load_document.assert_not_called()
625
+ elif file_type == "document":
626
+ mock_load_binary.assert_not_called()
627
+ mock_load_text.assert_not_called()
628
+ mock_load_document.assert_called_once_with(
629
+ file_name=file_name,
630
+ prefix="p-",
631
+ suffix="-s",
632
+ block_delimiter="b"
633
+ )
560
634
  else:
635
+ mock_load_binary.assert_not_called()
561
636
  mock_load_text.assert_called_once_with(
562
637
  file_name=file_name,
563
- prefix="",
564
- suffix="",
565
- block_delimiter=""
638
+ prefix="p-",
639
+ suffix="-s",
640
+ block_delimiter="b"
566
641
  )
567
- mock_load_binary.assert_not_called()
642
+ mock_load_document.assert_not_called()
568
643
 
569
644
 
570
645
  @pytest.mark.parametrize("files, pattern, user_input, expected_output, expected_file", [
@@ -788,6 +863,71 @@ def test_complete_LOAD(monkeypatch, temp_chat_file, text, line, begidx, endidx,
788
863
  assert completions == matching_files
789
864
 
790
865
 
866
+ @pytest.mark.parametrize("file_name, is_image, expected_mime", [
867
+ ("test.png", True, "image/png"),
868
+ ("test.jpg", True, "image/jpeg"),
869
+ ("test.jpeg", True, "image/jpeg"),
870
+ ("test.txt", False, None)
871
+ ])
872
+ def test_load_image(capsys, temp_chat_file, file_name, is_image, expected_mime):
873
+ mock_config = get_default_config()
874
+ with patch('ara_cli.prompt_handler.ConfigManager.get_config', return_value=mock_config):
875
+ chat = Chat(temp_chat_file.name, reset=False)
876
+
877
+ with patch.object(chat, 'load_binary_file', return_value=True) as mock_load_binary:
878
+ chat.load_image(file_name=file_name, prefix="p-", suffix="-s")
879
+
880
+ if is_image:
881
+ mock_load_binary.assert_called_once_with(
882
+ file_name=file_name,
883
+ mime_type=expected_mime,
884
+ prefix="p-",
885
+ suffix="-s"
886
+ )
887
+ else:
888
+ mock_load_binary.assert_not_called()
889
+ captured = capsys.readouterr()
890
+ assert f"File {file_name} not recognized as image, could not load" in captured.out
891
+
892
+
893
+ def test_do_LOAD_DOCUMENT(capsys, temp_chat_file):
894
+ mock_config = get_default_config()
895
+ with patch('ara_cli.prompt_handler.ConfigManager.get_config', return_value=mock_config):
896
+ chat = Chat(temp_chat_file.name, reset=False)
897
+
898
+ doc_file = "test.docx"
899
+ with patch.object(chat, 'find_matching_files_to_load', return_value=[doc_file]) as mock_find, \
900
+ patch.object(chat, 'load_document_file', return_value=True) as mock_load, \
901
+ patch.object(chat, 'add_prompt_tag_if_needed') as mock_add_tag:
902
+
903
+ chat.do_LOAD_DOCUMENT(doc_file)
904
+
905
+ mock_find.assert_called_once_with(doc_file)
906
+ mock_add_tag.assert_called_once_with(chat.chat_name)
907
+ mock_load.assert_called_once_with(doc_file, prefix=f"\nFile: {doc_file}\n")
908
+ captured = capsys.readouterr()
909
+ assert f"Loaded document file {doc_file}" in captured.out
910
+
911
+
912
+ def test_do_LOAD_IMAGE(capsys, temp_chat_file):
913
+ mock_config = get_default_config()
914
+ with patch('ara_cli.prompt_handler.ConfigManager.get_config', return_value=mock_config):
915
+ chat = Chat(temp_chat_file.name, reset=False)
916
+
917
+ image_file = "test.png"
918
+ with patch.object(chat, 'find_matching_files_to_load', return_value=[image_file]) as mock_find, \
919
+ patch.object(chat, 'load_image', return_value=True) as mock_load, \
920
+ patch.object(chat, 'add_prompt_tag_if_needed') as mock_add_tag:
921
+
922
+ chat.do_LOAD_IMAGE(image_file)
923
+
924
+ mock_find.assert_called_once_with(image_file)
925
+ mock_add_tag.assert_called_once_with(chat.chat_name)
926
+ mock_load.assert_called_once_with(image_file, prefix=f"\nFile: {image_file}\n")
927
+ captured = capsys.readouterr()
928
+ assert f"Loaded image file {image_file}" in captured.out
929
+
930
+
791
931
  @pytest.mark.parametrize("input_chat_name, expected_chat_name", [
792
932
  ("", "What should be the new chat name? "),
793
933
  ("new_chat", "new_chat_chat.md"),
@@ -796,13 +936,13 @@ def test_complete_LOAD(monkeypatch, temp_chat_file, text, line, begidx, endidx,
796
936
  def test_do_new(monkeypatch, temp_chat_file, input_chat_name, expected_chat_name):
797
937
  def mock_input(prompt):
798
938
  return "input_chat_name"
799
-
939
+
800
940
  monkeypatch.setattr('builtins.input', mock_input)
801
941
 
802
942
  mock_config = get_default_config()
803
943
  with patch('ara_cli.prompt_handler.ConfigManager.get_config', return_value=mock_config):
804
944
  chat = Chat(temp_chat_file.name, reset=False)
805
-
945
+
806
946
  with patch.object(Chat, '__init__', return_value=None) as mock_init:
807
947
  chat.do_NEW(input_chat_name)
808
948
  if input_chat_name == "":
@@ -1057,10 +1197,9 @@ def test_do_SEND(temp_chat_file):
1057
1197
  ("AnotherTemplate", MagicMock(serialize=MagicMock(return_value="other_content")), "other_content", "Loaded AnotherTemplate artefact template\n"),
1058
1198
  ])
1059
1199
  def test_do_LOAD_TEMPLATE_success(temp_chat_file, template_name, artefact_obj, expected_write, expected_print, capsys):
1060
- mock_config = MagicMock() # or use get_default_config() if needed
1200
+ mock_config = MagicMock()
1061
1201
  with patch('ara_cli.prompt_handler.ConfigManager.get_config', return_value=mock_config):
1062
1202
  chat = Chat(temp_chat_file.name, reset=False)
1063
- # Patch the artefact loader to return artefact_obj
1064
1203
  with patch('ara_cli.artefact_models.artefact_templates.template_artefact_of_type', return_value=artefact_obj) as mock_template_loader, \
1065
1204
  patch.object(chat, 'add_prompt_tag_if_needed') as mock_add_prompt_tag, \
1066
1205
  patch("builtins.open", mock_open()) as mock_file:
@@ -1087,4 +1226,4 @@ def test_do_LOAD_TEMPLATE_missing_artefact(temp_chat_file, template_name):
1087
1226
  chat.do_LOAD_TEMPLATE(template_name)
1088
1227
  mock_template_loader.assert_called_once_with(template_name)
1089
1228
  mock_add_prompt_tag.assert_not_called()
1090
- mock_file.assert_not_called()
1229
+ mock_file.assert_not_called()