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,603 @@
1
+ """
2
+ Unit tests for chat givens loading and image handling functionality.
3
+
4
+ These tests cover the functionality tested by:
5
+ - ara_chat_command_load_file_content.feature (givens and image loading scenarios)
6
+ """
7
+
8
+ import pytest
9
+ import os
10
+ import tempfile
11
+ import base64
12
+ from unittest.mock import patch, MagicMock, mock_open
13
+ from types import SimpleNamespace
14
+
15
+
16
+ def get_default_config():
17
+ """Default config for test fixtures."""
18
+ return SimpleNamespace(
19
+ ext_code_dirs=[
20
+ {"source_dir": "./src"},
21
+ {"source_dir": "./tests"},
22
+ ],
23
+ glossary_dir="./glossary",
24
+ doc_dir="./docs",
25
+ local_prompt_templates_dir="./ara/.araconfig",
26
+ local_ara_templates_dir="./ara/.araconfig/templates/",
27
+ ara_prompt_given_list_includes=[
28
+ "*.businessgoal",
29
+ "*.vision",
30
+ "*.capability",
31
+ "*.keyfeature",
32
+ "*.epic",
33
+ "*.userstory",
34
+ "*.example",
35
+ "*.feature",
36
+ "*.task",
37
+ "*.py",
38
+ "*.md",
39
+ "*.png",
40
+ "*.jpg",
41
+ "*.jpeg",
42
+ ],
43
+ llm_config=[
44
+ {"provider": "openai", "model": "openai/gpt-4o", "temperature": 1.0},
45
+ ],
46
+ global_dirs=[],
47
+ )
48
+
49
+
50
+ # =============================================================================
51
+ # Tests for _find_givens_files (givens file discovery)
52
+ # =============================================================================
53
+
54
+
55
+ class TestFindGivensFiles:
56
+ """Tests for the _find_givens_files method in Chat class."""
57
+
58
+ @pytest.fixture
59
+ def chat_instance(self):
60
+ """Creates a chat instance with mocked config."""
61
+ from ara_cli.chat import Chat
62
+
63
+ with tempfile.TemporaryDirectory() as tmpdir:
64
+ chat_file = os.path.join(tmpdir, "test_chat.md")
65
+ with open(chat_file, "w") as f:
66
+ f.write("# ara prompt:\n")
67
+
68
+ mock_config = get_default_config()
69
+ with patch(
70
+ "ara_cli.prompt_handler.ConfigManager.get_config",
71
+ return_value=mock_config,
72
+ ):
73
+ chat = Chat(chat_file, reset=False)
74
+ yield chat, tmpdir
75
+
76
+ def test_find_givens_files_explicit_file(self, chat_instance):
77
+ """Finds explicitly specified givens file."""
78
+ chat, tmpdir = chat_instance
79
+
80
+ # Create a givens file
81
+ givens_file = os.path.join(tmpdir, "custom_givens.md")
82
+ with open(givens_file, "w") as f:
83
+ f.write("[x] some_file.py\n")
84
+
85
+ result = chat._find_givens_files("custom_givens.md")
86
+ assert len(result) == 1
87
+ assert result[0] == givens_file
88
+
89
+ def test_find_givens_files_default_location(self, chat_instance):
90
+ """Finds default givens config file."""
91
+ chat, tmpdir = chat_instance
92
+
93
+ # Create prompt.data directory with default givens file
94
+ prompt_data_dir = os.path.join(tmpdir, "prompt.data")
95
+ os.makedirs(prompt_data_dir, exist_ok=True)
96
+
97
+ givens_file = os.path.join(prompt_data_dir, "config.prompt_givens.md")
98
+ with open(givens_file, "w") as f:
99
+ f.write("[x] file1.py\n")
100
+
101
+ result = chat._find_givens_files("")
102
+ assert len(result) == 1
103
+ assert result[0] == givens_file
104
+
105
+ def test_find_givens_files_multiple_defaults(self, chat_instance):
106
+ """Finds both local and global givens files."""
107
+ chat, tmpdir = chat_instance
108
+
109
+ # Create prompt.data directory with both files
110
+ prompt_data_dir = os.path.join(tmpdir, "prompt.data")
111
+ os.makedirs(prompt_data_dir, exist_ok=True)
112
+
113
+ local_givens = os.path.join(prompt_data_dir, "config.prompt_givens.md")
114
+ global_givens = os.path.join(prompt_data_dir, "config.prompt_global_givens.md")
115
+
116
+ with open(local_givens, "w") as f:
117
+ f.write("[x] local_file.py\n")
118
+ with open(global_givens, "w") as f:
119
+ f.write("[x] global_file.py\n")
120
+
121
+ result = chat._find_givens_files("")
122
+ assert len(result) == 2
123
+
124
+ def test_find_givens_files_not_found(self, chat_instance):
125
+ """Returns empty list when file not found."""
126
+ chat, tmpdir = chat_instance
127
+
128
+ with patch("sys.stdin.readline", return_value="\n"):
129
+ result = chat._find_givens_files("")
130
+
131
+ assert result == []
132
+
133
+
134
+ # =============================================================================
135
+ # Tests for LOAD_GIVENS command
136
+ # =============================================================================
137
+
138
+
139
+ class TestLoadGivensCommand:
140
+ """Tests for the do_LOAD_GIVENS command."""
141
+
142
+ @pytest.fixture
143
+ def temp_chat_setup(self):
144
+ """Creates a temporary chat setup for testing."""
145
+ from ara_cli.chat import Chat
146
+
147
+ with tempfile.TemporaryDirectory() as tmpdir:
148
+ chat_file = os.path.join(tmpdir, "test_chat.md")
149
+ with open(chat_file, "w") as f:
150
+ f.write("# ara prompt:\n")
151
+
152
+ # Create prompt.data directory
153
+ prompt_data_dir = os.path.join(tmpdir, "prompt.data")
154
+ os.makedirs(prompt_data_dir, exist_ok=True)
155
+
156
+ mock_config = get_default_config()
157
+ with patch(
158
+ "ara_cli.prompt_handler.ConfigManager.get_config",
159
+ return_value=mock_config,
160
+ ):
161
+ chat = Chat(chat_file, reset=False)
162
+
163
+ yield {
164
+ "chat": chat,
165
+ "tmpdir": tmpdir,
166
+ "chat_file": chat_file,
167
+ "prompt_data_dir": prompt_data_dir,
168
+ }
169
+
170
+ def test_load_givens_appends_content(self, temp_chat_setup):
171
+ """LOAD_GIVENS appends file content to chat."""
172
+ setup = temp_chat_setup
173
+
174
+ # Create givens config with marked entry
175
+ givens_file = os.path.join(setup["prompt_data_dir"], "config.prompt_givens.md")
176
+
177
+ # Create a file to load
178
+ target_file = os.path.join(setup["tmpdir"], "to_load.py")
179
+ with open(target_file, "w") as f:
180
+ f.write("print('loaded content')")
181
+
182
+ # The givens file should reference the target file
183
+ with open(givens_file, "w") as f:
184
+ f.write(f"[x] {target_file}\n")
185
+
186
+ with patch(
187
+ "ara_cli.prompt_handler.load_givens",
188
+ return_value=("loaded givens content\n", []),
189
+ ):
190
+ setup["chat"].do_LOAD_GIVENS("")
191
+
192
+ with open(setup["chat_file"], "r") as f:
193
+ content = f.read()
194
+
195
+ assert "loaded givens content" in content
196
+
197
+
198
+ # =============================================================================
199
+ # Tests for image loading (LOAD_IMAGE command)
200
+ # =============================================================================
201
+
202
+
203
+ class TestLoadImageCommand:
204
+ """Tests for the do_LOAD_IMAGE command."""
205
+
206
+ @pytest.fixture
207
+ def chat_with_image(self):
208
+ """Creates a chat instance with a test image file."""
209
+ from ara_cli.chat import Chat
210
+
211
+ with tempfile.TemporaryDirectory() as tmpdir:
212
+ chat_file = os.path.join(tmpdir, "test_chat.md")
213
+ with open(chat_file, "w") as f:
214
+ f.write("# ara prompt:\n")
215
+
216
+ # Create a fake image file
217
+ image_file = os.path.join(tmpdir, "test_image.png")
218
+ with open(image_file, "wb") as f:
219
+ f.write(b"\x89PNG\r\n\x1a\n" + b"fake_image_data")
220
+
221
+ mock_config = get_default_config()
222
+ with patch(
223
+ "ara_cli.prompt_handler.ConfigManager.get_config",
224
+ return_value=mock_config,
225
+ ):
226
+ chat = Chat(chat_file, reset=False)
227
+
228
+ yield {
229
+ "chat": chat,
230
+ "tmpdir": tmpdir,
231
+ "chat_file": chat_file,
232
+ "image_file": image_file,
233
+ }
234
+
235
+ def test_load_image_recognizes_png(self, chat_with_image):
236
+ """Recognizes PNG as valid image format."""
237
+ setup = chat_with_image
238
+
239
+ with patch.object(
240
+ setup["chat"],
241
+ "find_matching_files_to_load",
242
+ return_value=[setup["image_file"]],
243
+ ):
244
+ with patch(
245
+ "ara_cli.commands.load_image_command.LoadImageCommand"
246
+ ) as MockCmd:
247
+ mock_instance = MagicMock()
248
+ MockCmd.return_value = mock_instance
249
+
250
+ setup["chat"].do_LOAD_IMAGE("test_image.png")
251
+
252
+ MockCmd.assert_called_once()
253
+ mock_instance.execute.assert_called_once()
254
+
255
+ def test_load_image_rejects_unsupported_format(self, chat_with_image):
256
+ """Rejects unsupported file formats."""
257
+ setup = chat_with_image
258
+
259
+ # Create a non-image file
260
+ text_file = os.path.join(setup["tmpdir"], "file.txt")
261
+ with open(text_file, "w") as f:
262
+ f.write("not an image")
263
+
264
+ with patch.object(
265
+ setup["chat"], "find_matching_files_to_load", return_value=[text_file]
266
+ ):
267
+ # Should report error for unsupported format
268
+ with patch("ara_cli.error_handler.report_error") as mock_error:
269
+ setup["chat"].do_LOAD_IMAGE("file.txt")
270
+ mock_error.assert_called()
271
+
272
+
273
+ class TestLoadImageFileContent:
274
+ """Tests for image file content loading into chat."""
275
+
276
+ @pytest.fixture
277
+ def temp_chat_file(self):
278
+ """Creates a temporary chat file."""
279
+ from ara_cli.chat import Chat
280
+
281
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False) as f:
282
+ f.write("# ara prompt:\n")
283
+ temp_path = f.name
284
+
285
+ mock_config = get_default_config()
286
+ with patch(
287
+ "ara_cli.prompt_handler.ConfigManager.get_config", return_value=mock_config
288
+ ):
289
+ chat = Chat(temp_path, reset=False)
290
+
291
+ yield chat, temp_path
292
+
293
+ # Cleanup
294
+ if os.path.exists(temp_path):
295
+ os.unlink(temp_path)
296
+
297
+ def test_load_image_returns_true(self, temp_chat_file):
298
+ """load_image returns True for valid image."""
299
+ chat, temp_path = temp_chat_file
300
+
301
+ with patch.object(chat, "load_binary_file", return_value=True) as mock_load:
302
+ result = chat.load_image("test.png", prefix="PREFIX:", suffix=":SUFFIX")
303
+
304
+ mock_load.assert_called_once_with(
305
+ file_path="test.png",
306
+ mime_type="image/png",
307
+ prefix="PREFIX:",
308
+ suffix=":SUFFIX",
309
+ )
310
+ assert result is True
311
+
312
+ def test_load_image_handles_jpg(self, temp_chat_file):
313
+ """load_image handles JPG extension."""
314
+ chat, temp_path = temp_chat_file
315
+
316
+ with patch.object(chat, "load_binary_file", return_value=True) as mock_load:
317
+ chat.load_image("photo.jpg")
318
+
319
+ mock_load.assert_called_once()
320
+ call_args = mock_load.call_args
321
+ assert call_args.kwargs["mime_type"] == "image/jpeg"
322
+
323
+ def test_load_image_handles_jpeg(self, temp_chat_file):
324
+ """load_image handles JPEG extension."""
325
+ chat, temp_path = temp_chat_file
326
+
327
+ with patch.object(chat, "load_binary_file", return_value=True) as mock_load:
328
+ chat.load_image("photo.jpeg")
329
+
330
+ mock_load.assert_called_once()
331
+ call_args = mock_load.call_args
332
+ assert call_args.kwargs["mime_type"] == "image/jpeg"
333
+
334
+ def test_load_image_error_for_unsupported_webp(self, temp_chat_file):
335
+ """WebP is not supported - verifies error handling."""
336
+ chat, temp_path = temp_chat_file
337
+
338
+ with patch('ara_cli.error_handler.report_error') as mock_error:
339
+ result = chat.load_image("image.webp")
340
+ # Should report error for unsupported format
341
+ mock_error.assert_called_once()
342
+
343
+ def test_load_image_unsupported_extension(self, temp_chat_file):
344
+ """load_image reports error for unsupported format."""
345
+ chat, temp_path = temp_chat_file
346
+
347
+ with patch("ara_cli.error_handler.report_error") as mock_error:
348
+ result = chat.load_image("document.xyz")
349
+
350
+ mock_error.assert_called_once()
351
+
352
+
353
+ # =============================================================================
354
+ # Tests for find_matching_files_to_load (glob patterns)
355
+ # =============================================================================
356
+
357
+
358
+ class TestFindMatchingFilesToLoad:
359
+ """Tests for file matching with glob patterns."""
360
+
361
+ @pytest.fixture
362
+ def chat_with_files(self):
363
+ """Creates a chat with multiple files in directory."""
364
+ from ara_cli.chat import Chat
365
+
366
+ with tempfile.TemporaryDirectory() as tmpdir:
367
+ chat_file = os.path.join(tmpdir, "test_chat.md")
368
+ with open(chat_file, "w") as f:
369
+ f.write("# ara prompt:\n")
370
+
371
+ # Create multiple test files
372
+ for name in ["file1.py", "file2.py", "readme.md", "image.png"]:
373
+ with open(os.path.join(tmpdir, name), "w") as f:
374
+ f.write(f"content of {name}")
375
+
376
+ mock_config = get_default_config()
377
+ with patch(
378
+ "ara_cli.prompt_handler.ConfigManager.get_config",
379
+ return_value=mock_config,
380
+ ):
381
+ chat = Chat(chat_file, reset=False)
382
+
383
+ yield chat, tmpdir
384
+
385
+ def test_find_matching_files_single(self, chat_with_files):
386
+ """Finds single file by name."""
387
+ chat, tmpdir = chat_with_files
388
+
389
+ result = chat.find_matching_files_to_load("file1.py")
390
+
391
+ assert len(result) == 1
392
+ assert result[0].endswith("file1.py")
393
+
394
+ def test_find_matching_files_glob_pattern(self, chat_with_files):
395
+ """Finds files using glob pattern."""
396
+ chat, tmpdir = chat_with_files
397
+
398
+ result = chat.find_matching_files_to_load("*.py")
399
+
400
+ assert len(result) == 2
401
+ assert any("file1.py" in f for f in result)
402
+ assert any("file2.py" in f for f in result)
403
+
404
+ def test_find_matching_files_no_match(self, chat_with_files):
405
+ """Returns None when no files match."""
406
+ chat, tmpdir = chat_with_files
407
+
408
+ with patch("ara_cli.error_handler.report_error"):
409
+ result = chat.find_matching_files_to_load("nonexistent.txt")
410
+
411
+ assert result is None
412
+
413
+
414
+ # =============================================================================
415
+ # Tests for global directory loading
416
+ # =============================================================================
417
+
418
+
419
+ class TestGlobalDirectoryLoading:
420
+ """Tests for loading files from global directories."""
421
+
422
+ @pytest.fixture
423
+ def chat_with_global_dir(self):
424
+ """Creates a chat with global directory configuration."""
425
+ from ara_cli.chat import Chat
426
+
427
+ with tempfile.TemporaryDirectory() as tmpdir:
428
+ chat_file = os.path.join(tmpdir, "test_chat.md")
429
+ with open(chat_file, "w") as f:
430
+ f.write("# ara prompt:\n")
431
+
432
+ # Create global directory with files
433
+ global_dir = os.path.join(tmpdir, "global_files")
434
+ os.makedirs(global_dir, exist_ok=True)
435
+
436
+ global_file = os.path.join(global_dir, "shared_code.py")
437
+ with open(global_file, "w") as f:
438
+ f.write("# Shared code\ndef helper(): pass")
439
+
440
+ mock_config = get_default_config()
441
+ mock_config.global_dirs = [{"source_dir": global_dir}]
442
+
443
+ with patch(
444
+ "ara_cli.prompt_handler.ConfigManager.get_config",
445
+ return_value=mock_config,
446
+ ):
447
+ chat = Chat(chat_file, reset=False)
448
+
449
+ yield {
450
+ "chat": chat,
451
+ "tmpdir": tmpdir,
452
+ "global_dir": global_dir,
453
+ "global_file": global_file,
454
+ }
455
+
456
+ def test_global_dir_in_config(self, chat_with_global_dir):
457
+ """Global directory is accessible in config."""
458
+ setup = chat_with_global_dir
459
+
460
+ assert len(setup["chat"].config.global_dirs) == 1
461
+
462
+
463
+ # =============================================================================
464
+ # Tests for LOAD command with --load-images flag
465
+ # =============================================================================
466
+
467
+
468
+ class TestLoadWithImages:
469
+ """Tests for LOAD command with image extraction."""
470
+
471
+ def test_load_command_class_accepts_extract_images(self):
472
+ """LoadCommand accepts extract_images parameter."""
473
+ from ara_cli.commands.load_command import LoadCommand
474
+ import inspect
475
+
476
+ # Check that LoadCommand.__init__ accepts extract_images parameter
477
+ sig = inspect.signature(LoadCommand.__init__)
478
+ params = list(sig.parameters.keys())
479
+ assert 'extract_images' in params
480
+
481
+ def test_load_command_instantiation_with_extract_images(self):
482
+ """LoadCommand can be instantiated with extract_images=True."""
483
+ from ara_cli.commands.load_command import LoadCommand
484
+
485
+ # Should not raise an error
486
+ cmd = LoadCommand(
487
+ chat_instance=MagicMock(),
488
+ file_path="test.md",
489
+ prefix="",
490
+ block_delimiter="```",
491
+ extract_images=True,
492
+ output=print
493
+ )
494
+ assert cmd is not None
495
+
496
+
497
+ # =============================================================================
498
+ # Tests for binary content appending (base64 encoding)
499
+ # =============================================================================
500
+
501
+
502
+ class TestBinaryContentAppending:
503
+ """Tests for binary file content being appended as base64."""
504
+
505
+ @pytest.fixture
506
+ def chat_instance(self):
507
+ """Creates a chat instance for testing."""
508
+ from ara_cli.chat import Chat
509
+
510
+ with tempfile.TemporaryDirectory() as tmpdir:
511
+ chat_file = os.path.join(tmpdir, "test_chat.md")
512
+ with open(chat_file, "w") as f:
513
+ f.write("# ara prompt:\n")
514
+
515
+ mock_config = get_default_config()
516
+ with patch(
517
+ "ara_cli.prompt_handler.ConfigManager.get_config",
518
+ return_value=mock_config,
519
+ ):
520
+ chat = Chat(chat_file, reset=False)
521
+
522
+ yield chat, tmpdir, chat_file
523
+
524
+ def test_binary_file_content_base64_encoded(self, chat_instance):
525
+ """Binary file content is base64 encoded when appended."""
526
+ chat, tmpdir, chat_file = chat_instance
527
+
528
+ # Create a binary file
529
+ binary_file = os.path.join(tmpdir, "image.png")
530
+ binary_content = b"\x89PNG\r\n\x1a\ntest_binary_data"
531
+ with open(binary_file, "wb") as f:
532
+ f.write(binary_content)
533
+
534
+ expected_base64 = base64.b64encode(binary_content).decode("utf-8")
535
+
536
+ # Mock the open calls to capture what gets written
537
+ with patch(
538
+ "ara_cli.file_loaders.binary_file_loader.open",
539
+ mock_open(read_data=binary_content),
540
+ ) as mock_file:
541
+ chat.load_binary_file(binary_file, mime_type="image/png")
542
+
543
+ # Check that the file was written with base64 content
544
+ write_calls = mock_file().write.call_args_list
545
+ if write_calls:
546
+ written_content = "".join(str(call[0][0]) for call in write_calls)
547
+ assert expected_base64 in written_content or "base64" in written_content
548
+
549
+
550
+ # =============================================================================
551
+ # Tests for choose_file_to_load (user file selection)
552
+ # =============================================================================
553
+
554
+
555
+ class TestChooseFileToLoad:
556
+ """Tests for interactive file selection."""
557
+
558
+ @pytest.fixture
559
+ def chat_instance(self):
560
+ """Creates a chat instance for testing."""
561
+ from ara_cli.chat import Chat
562
+
563
+ with tempfile.TemporaryDirectory() as tmpdir:
564
+ chat_file = os.path.join(tmpdir, "test_chat.md")
565
+ with open(chat_file, "w") as f:
566
+ f.write("# ara prompt:\n")
567
+
568
+ mock_config = get_default_config()
569
+ with patch(
570
+ "ara_cli.prompt_handler.ConfigManager.get_config",
571
+ return_value=mock_config,
572
+ ):
573
+ chat = Chat(chat_file, reset=False)
574
+
575
+ yield chat
576
+
577
+ def test_choose_file_to_load_single_file(self, chat_instance):
578
+ """Single file is returned without prompting."""
579
+ files = ["/path/to/file.py"]
580
+
581
+ result = chat_instance.choose_file_to_load(files, "*.py")
582
+
583
+ assert result == "/path/to/file.py"
584
+
585
+ def test_choose_file_to_load_multiple_files_user_selects(self, chat_instance):
586
+ """Multiple files prompts user for selection."""
587
+ files = ["/path/to/file1.py", "/path/to/file2.py"]
588
+
589
+ with patch("sys.stdin.readline", return_value="1\n"):
590
+ result = chat_instance.choose_file_to_load(files, "*.py")
591
+
592
+ # User selects option 1 (0-indexed would be file1.py)
593
+ assert result == "/path/to/file1.py"
594
+
595
+ def test_choose_file_to_load_invalid_choice(self, chat_instance):
596
+ """Invalid choice returns None."""
597
+ files = ["/path/to/file1.py", "/path/to/file2.py"]
598
+
599
+ with patch("sys.stdin.readline", return_value="99\n"):
600
+ with patch("ara_cli.error_handler.report_error"):
601
+ result = chat_instance.choose_file_to_load(files, "*.py")
602
+
603
+ assert result is None