ara-cli 0.1.9.96__py3-none-any.whl → 0.1.10.0__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.
@@ -10,6 +10,30 @@ from ara_cli import prompt_handler
10
10
  from ara_cli.ara_config import ARAconfig, LLMConfigItem, ConfigManager
11
11
  from ara_cli.classifier import Classifier
12
12
 
13
+ from langfuse.api.resources.commons.errors import NotFoundError
14
+
15
+
16
+ @pytest.fixture(autouse=True)
17
+ def mock_langfuse():
18
+ """Mock Langfuse client to prevent network calls during tests."""
19
+ with patch.object(prompt_handler.LLMSingleton, 'langfuse', None):
20
+ mock_langfuse_instance = MagicMock()
21
+
22
+ # Mock the get_prompt method to raise NotFoundError (simulating prompt not found)
23
+ mock_langfuse_instance.get_prompt.side_effect = NotFoundError(
24
+ # status_code=404,
25
+ body={'message': "Prompt not found", 'error': 'LangfuseNotFoundError'}
26
+ )
27
+
28
+ # Mock the span context manager
29
+ mock_span = MagicMock()
30
+ mock_span.__enter__ = MagicMock(return_value=mock_span)
31
+ mock_span.__exit__ = MagicMock(return_value=None)
32
+ mock_langfuse_instance.start_as_current_span.return_value = mock_span
33
+
34
+ with patch.object(prompt_handler.LLMSingleton, 'langfuse', mock_langfuse_instance):
35
+ yield mock_langfuse_instance
36
+
13
37
 
14
38
  @pytest.fixture
15
39
  def mock_config():
@@ -30,7 +54,7 @@ def mock_config():
30
54
  return config
31
55
 
32
56
 
33
- @pytest.fixture
57
+ @pytest.fixture(autouse=True)
34
58
  def mock_config_manager(mock_config):
35
59
  """Patches ConfigManager to ensure it always returns the mock_config."""
36
60
  with patch.object(ConfigManager, 'get_config') as mock_get_config:
@@ -243,13 +267,17 @@ class TestCoreLogic:
243
267
  )
244
268
 
245
269
  @patch('ara_cli.prompt_handler.send_prompt')
246
- def test_describe_image(self, mock_send_prompt, tmp_path):
270
+ def test_describe_image(self, mock_send_prompt, tmp_path, mock_langfuse):
247
271
  fake_image_path = tmp_path / "test.jpeg"
248
272
  fake_image_content = b"fakeimagedata"
249
273
  fake_image_path.write_bytes(fake_image_content)
250
274
 
251
275
  mock_send_prompt.return_value = iter([])
252
276
 
277
+ # Ensure the langfuse mock is properly set up for this instance
278
+ instance = prompt_handler.LLMSingleton.get_instance()
279
+ instance.langfuse = mock_langfuse
280
+
253
281
  prompt_handler.describe_image(fake_image_path)
254
282
 
255
283
  mock_send_prompt.assert_called_once()
@@ -265,7 +293,7 @@ class TestCoreLogic:
265
293
  assert message_content[1]['image_url']['url'] == expected_url
266
294
 
267
295
  @patch('ara_cli.prompt_handler.send_prompt')
268
- def test_describe_image_returns_response_text(self, mock_send_prompt, tmp_path):
296
+ def test_describe_image_returns_response_text(self, mock_send_prompt, tmp_path, mock_langfuse):
269
297
  fake_image_path = tmp_path / "test.gif"
270
298
  fake_image_path.touch()
271
299
 
@@ -277,6 +305,10 @@ class TestCoreLogic:
277
305
  mock_chunk3.choices[0].delta.content = None # Test empty chunk
278
306
  mock_send_prompt.return_value = iter([mock_chunk1, mock_chunk3, mock_chunk2])
279
307
 
308
+ # Ensure the langfuse mock is properly set up for this instance
309
+ instance = prompt_handler.LLMSingleton.get_instance()
310
+ instance.langfuse = mock_langfuse
311
+
280
312
  description = prompt_handler.describe_image(fake_image_path)
281
313
  assert description == "This is a description."
282
314
 
@@ -310,7 +342,11 @@ class TestCoreLogic:
310
342
  prompt_handler.write_prompt_result("test_classifier", "my_param", "Test content")
311
343
  assert "Test content" in log_file.read_text()
312
344
 
313
- def test_prepend_system_prompt(self):
345
+ def test_prepend_system_prompt(self, mock_langfuse):
346
+ # Ensure the langfuse mock is properly set up for this instance
347
+ instance = prompt_handler.LLMSingleton.get_instance()
348
+ instance.langfuse = mock_langfuse
349
+
314
350
  messages = [{"role": "user", "content": "Hi"}]
315
351
  result = prompt_handler.prepend_system_prompt(messages)
316
352
  assert len(result) == 2
@@ -0,0 +1,192 @@
1
+ import os
2
+ import pytest
3
+ from unittest.mock import MagicMock, patch, mock_open
4
+ from ara_cli.template_loader import TemplateLoader
5
+
6
+
7
+ @pytest.fixture
8
+ def mock_chat_instance():
9
+ """Fixture for a mocked chat instance."""
10
+ mock = MagicMock()
11
+ mock.choose_file_to_load.return_value = "chosen_file.md"
12
+ mock.load_file.return_value = True
13
+ return mock
14
+
15
+
16
+ @pytest.fixture
17
+ def template_loader_cli():
18
+ """Fixture for a TemplateLoader in CLI mode."""
19
+ return TemplateLoader()
20
+
21
+
22
+ @pytest.fixture
23
+ def template_loader_chat(mock_chat_instance):
24
+ """Fixture for a TemplateLoader in chat mode."""
25
+ return TemplateLoader(chat_instance=mock_chat_instance)
26
+
27
+
28
+ def test_init(mock_chat_instance):
29
+ """Test the constructor."""
30
+ loader_cli = TemplateLoader()
31
+ assert loader_cli.chat_instance is None
32
+
33
+ loader_chat = TemplateLoader(chat_instance=mock_chat_instance)
34
+ assert loader_chat.chat_instance == mock_chat_instance
35
+
36
+
37
+ @pytest.mark.parametrize("template_name, default_pattern, expected_method_to_call", [
38
+ ("", "*.rules.md", "load_template_from_prompt_data"),
39
+ ("my_rule", "*.rules.md", "load_template_from_global_or_local"),
40
+ ])
41
+ def test_load_template_routing(template_loader_cli, template_name, default_pattern, expected_method_to_call):
42
+ """Test that load_template calls the correct downstream method based on inputs."""
43
+ with patch.object(TemplateLoader, 'load_template_from_prompt_data') as mock_from_prompt, \
44
+ patch.object(TemplateLoader, 'load_template_from_global_or_local') as mock_from_global_local:
45
+
46
+ template_loader_cli.load_template(
47
+ template_name, "rules", "chat.md", default_pattern)
48
+
49
+ if expected_method_to_call == "load_template_from_prompt_data":
50
+ mock_from_prompt.assert_called_once()
51
+ mock_from_global_local.assert_not_called()
52
+ else:
53
+ mock_from_prompt.assert_not_called()
54
+ mock_from_global_local.assert_called_once()
55
+
56
+
57
+ def test_load_template_no_name_no_pattern(template_loader_cli, capsys):
58
+ """Test load_template fails gracefully when no name or pattern is given."""
59
+ result = template_loader_cli.load_template("", "blueprint", "chat.md", None)
60
+ assert result is False
61
+ captured = capsys.readouterr()
62
+ assert "A template name is required for template type 'blueprint'" in captured.out
63
+
64
+
65
+ @pytest.mark.parametrize("template_type, expected_plural", [
66
+ ("rules", "rules"),
67
+ ("commands", "commands"),
68
+ ("intention", "intentions"),
69
+ ("blueprint", "blueprints"),
70
+ ("custom", "customs"),
71
+ ])
72
+ def test_get_plural_template_type(template_loader_cli, template_type, expected_plural):
73
+ """Test the pluralization of template types."""
74
+ assert template_loader_cli.get_plural_template_type(
75
+ template_type) == expected_plural
76
+
77
+
78
+ @pytest.mark.parametrize("template_name, expected_method_to_call", [
79
+ ("global/my_rule", "_load_global_template"),
80
+ ("my_rule", "_load_local_template"),
81
+ ])
82
+ def test_load_template_from_global_or_local_routing(template_loader_cli, template_name, expected_method_to_call):
83
+ """Test routing between global and local template loading."""
84
+ with patch.object(TemplateLoader, '_load_global_template') as mock_global, \
85
+ patch.object(TemplateLoader, '_load_local_template') as mock_local:
86
+
87
+ template_loader_cli.load_template_from_global_or_local(
88
+ template_name, "rules", "chat.md")
89
+
90
+ if expected_method_to_call == "_load_global_template":
91
+ mock_global.assert_called_once()
92
+ mock_local.assert_not_called()
93
+ else:
94
+ mock_global.assert_not_called()
95
+ mock_local.assert_called_once()
96
+
97
+
98
+ @pytest.mark.parametrize("files, pattern, user_input, expected_return", [
99
+ (["one.md"], "*.md", "", "one.md"),
100
+ ([], "*.md", "", None),
101
+ (["a.md", "b.md"], "*", "1", "a.md"),
102
+ (["a.md", "b.md"], "*", "2", "b.md"),
103
+ (["a.md", "b.md"], "*", "3", None),
104
+ (["a.md", "b.md"], "*", "invalid", None),
105
+ ])
106
+ def test_choose_file_for_cli(template_loader_cli, files, pattern, user_input, expected_return):
107
+ """Test the interactive file selection for the CLI."""
108
+ with patch('builtins.input', return_value=user_input):
109
+ result = template_loader_cli._choose_file_for_cli(files, pattern)
110
+ assert result == expected_return
111
+
112
+
113
+ def test_load_file_to_chat_cli_context(tmp_path):
114
+ """Test writing template content to a chat file in a CLI context."""
115
+ chat_file = tmp_path / "chat.md"
116
+ chat_file.write_text("# ara prompt:\n")
117
+ template_file = tmp_path / "template.md"
118
+ template_content = "This is the template content."
119
+ template_file.write_text(template_content)
120
+
121
+ loader = TemplateLoader()
122
+ result = loader._load_file_to_chat(
123
+ str(template_file), "rules", str(chat_file))
124
+
125
+ assert result is True
126
+ final_content = chat_file.read_text()
127
+ expected_content = f"# ara prompt:\n\n{template_content}\n"
128
+ assert final_content == expected_content
129
+
130
+
131
+ def test_load_file_to_chat_chat_context(template_loader_chat, mock_chat_instance):
132
+ """Test delegating file loading to the chat instance."""
133
+ result = template_loader_chat._load_file_to_chat(
134
+ "file.md", "rules", "chat.md")
135
+
136
+ assert result is True
137
+ mock_chat_instance.add_prompt_tag_if_needed.assert_called_once_with(
138
+ "chat.md")
139
+ mock_chat_instance.load_file.assert_called_once_with("file.md")
140
+
141
+
142
+ def test_find_project_root(tmp_path):
143
+ """Test finding the project root directory."""
144
+ project_root = tmp_path / "project"
145
+ ara_dir = project_root / "ara"
146
+ nested_dir = project_root / "src" / "component"
147
+ ara_dir.mkdir(parents=True)
148
+ nested_dir.mkdir(parents=True)
149
+
150
+ no_ara_dir = tmp_path / "other"
151
+ no_ara_dir.mkdir()
152
+
153
+ loader = TemplateLoader()
154
+
155
+ # Test finding the root from a nested directory
156
+ assert loader._find_project_root(str(nested_dir)) == str(project_root)
157
+ # Test finding the root from the root itself
158
+ assert loader._find_project_root(str(project_root)) == str(project_root)
159
+ # Test not finding the root
160
+ assert loader._find_project_root(str(no_ara_dir)) is None
161
+
162
+
163
+ @patch('ara_cli.template_loader.TemplatePathManager')
164
+ @patch('ara_cli.template_loader.ConfigManager')
165
+ def test_get_available_templates(MockConfigManager, MockTemplatePathManager, tmp_path):
166
+ """Test the discovery of global and local templates."""
167
+ # Setup mock paths and config
168
+ project_root = tmp_path / "project"
169
+ ara_dir = project_root / "ara"
170
+ araconfig_dir = project_root / ".araconfig"
171
+ custom_modules_dir = araconfig_dir / "custom-prompt-modules" / "rules"
172
+ global_modules_dir = tmp_path / "global_templates" / "prompt-modules" / "rules"
173
+
174
+ for d in [ara_dir, custom_modules_dir, global_modules_dir]:
175
+ d.mkdir(parents=True)
176
+
177
+ (custom_modules_dir / "local_rule.md").touch()
178
+ (global_modules_dir / "global_rule.md").touch()
179
+
180
+ mock_config = MagicMock()
181
+ mock_config.local_prompt_templates_dir = ".araconfig"
182
+ mock_config.custom_prompt_templates_subdir = "custom-prompt-modules"
183
+ MockConfigManager.get_config.return_value = mock_config
184
+ MockTemplatePathManager.get_template_base_path.return_value = str(
185
+ tmp_path / "global_templates")
186
+
187
+ loader = TemplateLoader()
188
+ templates = loader.get_available_templates(
189
+ "rules", context_path=str(project_root))
190
+
191
+ assert sorted(templates) == sorted(
192
+ ["global/global_rule.md", "local_rule.md"])