ara-cli 0.1.10.5__py3-none-any.whl → 0.1.14.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.
- ara_cli/__init__.py +51 -6
- ara_cli/__main__.py +87 -75
- ara_cli/ara_command_action.py +189 -101
- ara_cli/ara_config.py +187 -128
- ara_cli/ara_subcommands/common.py +2 -2
- ara_cli/ara_subcommands/config.py +221 -0
- ara_cli/ara_subcommands/convert.py +107 -0
- ara_cli/ara_subcommands/fetch.py +41 -0
- ara_cli/ara_subcommands/fetch_agents.py +22 -0
- ara_cli/ara_subcommands/fetch_scripts.py +19 -0
- ara_cli/ara_subcommands/fetch_templates.py +15 -10
- ara_cli/ara_subcommands/list.py +97 -23
- ara_cli/ara_subcommands/prompt.py +266 -106
- ara_cli/artefact_autofix.py +117 -64
- ara_cli/artefact_converter.py +355 -0
- ara_cli/artefact_creator.py +41 -17
- ara_cli/artefact_lister.py +3 -3
- ara_cli/artefact_models/artefact_model.py +1 -1
- ara_cli/artefact_models/artefact_templates.py +0 -9
- ara_cli/artefact_models/feature_artefact_model.py +8 -8
- ara_cli/artefact_reader.py +62 -43
- ara_cli/artefact_scan.py +39 -17
- ara_cli/chat.py +300 -71
- ara_cli/chat_agent/__init__.py +0 -0
- ara_cli/chat_agent/agent_process_manager.py +155 -0
- ara_cli/chat_script_runner/__init__.py +0 -0
- ara_cli/chat_script_runner/script_completer.py +23 -0
- ara_cli/chat_script_runner/script_finder.py +41 -0
- ara_cli/chat_script_runner/script_lister.py +36 -0
- ara_cli/chat_script_runner/script_runner.py +36 -0
- ara_cli/chat_web_search/__init__.py +0 -0
- ara_cli/chat_web_search/web_search.py +263 -0
- ara_cli/children_contribution_updater.py +737 -0
- ara_cli/classifier.py +34 -0
- ara_cli/commands/agent_run_command.py +98 -0
- ara_cli/commands/fetch_agents_command.py +106 -0
- ara_cli/commands/fetch_scripts_command.py +43 -0
- ara_cli/commands/fetch_templates_command.py +39 -0
- ara_cli/commands/fetch_templates_commands.py +39 -0
- ara_cli/commands/list_agents_command.py +39 -0
- ara_cli/commands/load_command.py +4 -3
- ara_cli/commands/load_image_command.py +1 -1
- ara_cli/commands/read_command.py +23 -27
- ara_cli/completers.py +95 -35
- ara_cli/constants.py +2 -0
- ara_cli/directory_navigator.py +37 -4
- ara_cli/error_handler.py +26 -11
- ara_cli/file_loaders/document_reader.py +0 -178
- ara_cli/file_loaders/factories/__init__.py +0 -0
- ara_cli/file_loaders/factories/document_reader_factory.py +32 -0
- ara_cli/file_loaders/factories/file_loader_factory.py +27 -0
- ara_cli/file_loaders/file_loader.py +1 -30
- ara_cli/file_loaders/loaders/__init__.py +0 -0
- ara_cli/file_loaders/{document_file_loader.py → loaders/document_file_loader.py} +1 -1
- ara_cli/file_loaders/loaders/text_file_loader.py +47 -0
- ara_cli/file_loaders/readers/__init__.py +0 -0
- ara_cli/file_loaders/readers/docx_reader.py +49 -0
- ara_cli/file_loaders/readers/excel_reader.py +27 -0
- ara_cli/file_loaders/{markdown_reader.py → readers/markdown_reader.py} +1 -1
- ara_cli/file_loaders/readers/odt_reader.py +59 -0
- ara_cli/file_loaders/readers/pdf_reader.py +54 -0
- ara_cli/file_loaders/readers/pptx_reader.py +104 -0
- ara_cli/file_loaders/tools/__init__.py +0 -0
- ara_cli/llm_utils.py +58 -0
- ara_cli/output_suppressor.py +53 -0
- ara_cli/prompt_chat.py +20 -4
- ara_cli/prompt_extractor.py +47 -32
- ara_cli/prompt_handler.py +123 -17
- ara_cli/tag_extractor.py +8 -7
- ara_cli/template_loader.py +2 -1
- ara_cli/template_manager.py +52 -21
- ara_cli/templates/global-scripts/hello_global.py +1 -0
- ara_cli/templates/prompt-modules/commands/add_scenarios_for_new_behaviour.feature_creation_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/align_feature_with_implementation_changes.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/analyze_codebase_and_plan_tasks.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/choose_best_parent_artefact.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/create_tasks_from_artefact_content.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/create_tests_for_uncovered_modules.test_generation_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/derive_features_from_video_description.feature_creation_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/describe_agent_capabilities.agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/empty.commands.md +2 -12
- ara_cli/templates/prompt-modules/commands/execute_scoped_todos_in_task.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/explain_single_file_purpose.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/extract_file_information_bullets.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/extract_general.commands.md +12 -0
- ara_cli/templates/prompt-modules/commands/extract_markdown.commands.md +11 -0
- ara_cli/templates/prompt-modules/commands/extract_python.commands.md +13 -0
- ara_cli/templates/prompt-modules/commands/feature_add_or_modifiy_specified_behavior.commands.md +36 -0
- ara_cli/templates/prompt-modules/commands/feature_generate_initial_specified_bevahior.commands.md +53 -0
- ara_cli/templates/prompt-modules/commands/fix_failing_behave_step_definitions.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/fix_failing_pytest_tests.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/general_instruction_policy.commands.md +47 -0
- ara_cli/templates/prompt-modules/commands/generate_and_fix_pytest_tests.test_generation_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/prompt_template_tech_stack_transformer.commands.md +95 -0
- ara_cli/templates/prompt-modules/commands/python_bug_fixing_code.commands.md +34 -0
- ara_cli/templates/prompt-modules/commands/python_generate_code.commands.md +27 -0
- ara_cli/templates/prompt-modules/commands/python_refactoring_code.commands.md +39 -0
- ara_cli/templates/prompt-modules/commands/python_step_definitions_generation_and_fixing.commands.md +40 -0
- ara_cli/templates/prompt-modules/commands/python_unittest_generation_and_fixing.commands.md +48 -0
- ara_cli/templates/prompt-modules/commands/suggest_next_story_child_tasks.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/summarize_or_transcribe_media.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/update_feature_to_match_implementation.feature_creation_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/update_user_story_with_requirements.interview_agent.commands.md +1 -0
- ara_cli/version.py +1 -1
- {ara_cli-0.1.10.5.dist-info → ara_cli-0.1.14.0.dist-info}/METADATA +49 -11
- ara_cli-0.1.14.0.dist-info/RECORD +253 -0
- {ara_cli-0.1.10.5.dist-info → ara_cli-0.1.14.0.dist-info}/WHEEL +1 -1
- tests/test_ara_command_action.py +31 -19
- tests/test_ara_config.py +177 -90
- tests/test_artefact_autofix.py +170 -97
- tests/test_artefact_autofix_integration.py +495 -0
- tests/test_artefact_converter.py +312 -0
- tests/test_artefact_extraction.py +564 -0
- tests/test_artefact_lister.py +11 -8
- tests/test_chat.py +166 -130
- tests/test_chat_givens_images.py +603 -0
- tests/test_chat_script_runner.py +454 -0
- tests/test_children_contribution_updater.py +98 -0
- tests/test_document_loader_office.py +267 -0
- tests/test_llm_utils.py +164 -0
- tests/test_prompt_chat.py +343 -0
- tests/test_prompt_extractor.py +683 -0
- tests/test_prompt_handler.py +416 -214
- tests/test_setup_default_chat_prompt_mode.py +198 -0
- tests/test_tag_extractor.py +95 -49
- tests/test_web_search.py +467 -0
- ara_cli/file_loaders/document_readers.py +0 -233
- ara_cli/file_loaders/file_loaders.py +0 -123
- ara_cli/file_loaders/text_file_loader.py +0 -187
- ara_cli/templates/prompt-modules/blueprints/complete_pytest_unittest.blueprint.md +0 -27
- ara_cli/templates/prompt-modules/blueprints/pytest_unittest_prompt.blueprint.md +0 -32
- ara_cli/templates/prompt-modules/blueprints/task_todo_list_implement_feature_BDD_way.blueprint.md +0 -30
- ara_cli/templates/prompt-modules/commands/artefact_classification.commands.md +0 -9
- ara_cli/templates/prompt-modules/commands/artefact_extension.commands.md +0 -17
- ara_cli/templates/prompt-modules/commands/artefact_formulation.commands.md +0 -14
- ara_cli/templates/prompt-modules/commands/behave_step_generation.commands.md +0 -102
- ara_cli/templates/prompt-modules/commands/code_generation_complex.commands.md +0 -20
- ara_cli/templates/prompt-modules/commands/code_generation_simple.commands.md +0 -13
- ara_cli/templates/prompt-modules/commands/error_fixing.commands.md +0 -20
- ara_cli/templates/prompt-modules/commands/feature_file_update.commands.md +0 -18
- ara_cli/templates/prompt-modules/commands/feature_formulation.commands.md +0 -43
- ara_cli/templates/prompt-modules/commands/js_code_generation_simple.commands.md +0 -13
- ara_cli/templates/prompt-modules/commands/refactoring.commands.md +0 -15
- ara_cli/templates/prompt-modules/commands/refactoring_analysis.commands.md +0 -9
- ara_cli/templates/prompt-modules/commands/reverse_engineer_feature_file.commands.md +0 -15
- ara_cli/templates/prompt-modules/commands/reverse_engineer_program_flow.commands.md +0 -19
- ara_cli-0.1.10.5.dist-info/RECORD +0 -194
- /ara_cli/file_loaders/{binary_file_loader.py → loaders/binary_file_loader.py} +0 -0
- /ara_cli/file_loaders/{image_processor.py → tools/image_processor.py} +0 -0
- {ara_cli-0.1.10.5.dist-info → ara_cli-0.1.14.0.dist-info}/entry_points.txt +0 -0
- {ara_cli-0.1.10.5.dist-info → ara_cli-0.1.14.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for artefact extraction functionality.
|
|
3
|
+
|
|
4
|
+
These tests cover the functionality tested by:
|
|
5
|
+
- _agile_artefact_extraction.feature
|
|
6
|
+
- agile_artefact_extraction_force.feature
|
|
7
|
+
- agile_artefact_extraction_override.feature
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
import os
|
|
12
|
+
import tempfile
|
|
13
|
+
from unittest.mock import patch, MagicMock, mock_open
|
|
14
|
+
|
|
15
|
+
from ara_cli.prompt_extractor import (
|
|
16
|
+
_find_extract_token,
|
|
17
|
+
_extract_file_path,
|
|
18
|
+
_find_artefact_class,
|
|
19
|
+
_apply_replacements,
|
|
20
|
+
create_file_if_not_exist,
|
|
21
|
+
determine_should_create,
|
|
22
|
+
handle_existing_file,
|
|
23
|
+
FenceDetector,
|
|
24
|
+
extract_responses,
|
|
25
|
+
_perform_extraction_for_block,
|
|
26
|
+
_process_document_blocks,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# =============================================================================
|
|
31
|
+
# Tests for FenceDetector class (artefact marking functionality)
|
|
32
|
+
# =============================================================================
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TestFenceDetector:
|
|
36
|
+
"""Tests for the FenceDetector class used in extraction."""
|
|
37
|
+
|
|
38
|
+
def test_is_extract_fence_with_valid_fence(self):
|
|
39
|
+
"""Detects valid extract fence markers."""
|
|
40
|
+
source_lines = ["```python", "# [x] extract", "print('hello')", "```"]
|
|
41
|
+
detector = FenceDetector(source_lines)
|
|
42
|
+
assert detector.is_extract_fence(0) is True
|
|
43
|
+
|
|
44
|
+
def test_is_extract_fence_with_triple_tilde(self):
|
|
45
|
+
"""Detects triple tilde fence markers."""
|
|
46
|
+
source_lines = ["~~~", "# [x] extract", "some content", "~~~"]
|
|
47
|
+
detector = FenceDetector(source_lines)
|
|
48
|
+
assert detector.is_extract_fence(0) is True
|
|
49
|
+
|
|
50
|
+
def test_is_extract_fence_without_extract_marker(self):
|
|
51
|
+
"""Returns False for fence without extract marker."""
|
|
52
|
+
source_lines = ["```python", "print('hello')", "```"]
|
|
53
|
+
detector = FenceDetector(source_lines)
|
|
54
|
+
assert detector.is_extract_fence(0) is False
|
|
55
|
+
|
|
56
|
+
def test_is_extract_fence_non_fence_line(self):
|
|
57
|
+
"""Returns False for non-fence lines."""
|
|
58
|
+
source_lines = ["some regular text", "# [x] extract"]
|
|
59
|
+
detector = FenceDetector(source_lines)
|
|
60
|
+
assert detector.is_extract_fence(0) is False
|
|
61
|
+
|
|
62
|
+
def test_find_matching_fence_end_simple(self):
|
|
63
|
+
"""Finds matching end fence for simple block."""
|
|
64
|
+
source_lines = ["```", "# [x] extract", "content", "```"]
|
|
65
|
+
detector = FenceDetector(source_lines)
|
|
66
|
+
assert detector.find_matching_fence_end(0) == 3
|
|
67
|
+
|
|
68
|
+
def test_find_matching_fence_end_with_language(self):
|
|
69
|
+
"""Finds matching end fence with language specifier."""
|
|
70
|
+
source_lines = ["```python", "# [x] extract", "print('hello')", "```"]
|
|
71
|
+
detector = FenceDetector(source_lines)
|
|
72
|
+
assert detector.find_matching_fence_end(0) == 3
|
|
73
|
+
|
|
74
|
+
def test_find_matching_fence_end_indented(self):
|
|
75
|
+
"""Finds matching end fence for indented blocks."""
|
|
76
|
+
source_lines = [" ```", " # [x] extract", " content", " ```"]
|
|
77
|
+
detector = FenceDetector(source_lines)
|
|
78
|
+
assert detector.find_matching_fence_end(0) == 3
|
|
79
|
+
|
|
80
|
+
def test_find_matching_fence_end_no_match(self):
|
|
81
|
+
"""Returns -1 when no matching fence end found."""
|
|
82
|
+
source_lines = ["```", "# [x] extract", "content without closing fence"]
|
|
83
|
+
detector = FenceDetector(source_lines)
|
|
84
|
+
assert detector.find_matching_fence_end(0) == -1
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# =============================================================================
|
|
88
|
+
# Tests for extraction helper functions
|
|
89
|
+
# =============================================================================
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class TestExtractionHelpers:
|
|
93
|
+
"""Tests for extraction helper functions."""
|
|
94
|
+
|
|
95
|
+
def test_extract_file_path_valid(self):
|
|
96
|
+
"""Extracts file path from content lines."""
|
|
97
|
+
content_lines = ["# filename: path/to/file.py", "content"]
|
|
98
|
+
result = _extract_file_path(content_lines)
|
|
99
|
+
assert result == "path/to/file.py"
|
|
100
|
+
|
|
101
|
+
def test_extract_file_path_with_spaces(self):
|
|
102
|
+
"""Extracts file path with surrounding spaces."""
|
|
103
|
+
content_lines = ["# filename: file.txt ", "content"]
|
|
104
|
+
result = _extract_file_path(content_lines)
|
|
105
|
+
assert result == "file.txt"
|
|
106
|
+
|
|
107
|
+
def test_extract_file_path_no_match(self):
|
|
108
|
+
"""Returns None when no filename found."""
|
|
109
|
+
content_lines = ["no filename here", "content"]
|
|
110
|
+
result = _extract_file_path(content_lines)
|
|
111
|
+
assert result is None
|
|
112
|
+
|
|
113
|
+
def test_extract_file_path_empty_lines(self):
|
|
114
|
+
"""Returns None for empty content lines."""
|
|
115
|
+
result = _extract_file_path([])
|
|
116
|
+
assert result is None
|
|
117
|
+
|
|
118
|
+
def test_apply_replacements_single(self):
|
|
119
|
+
"""Applies single replacement correctly."""
|
|
120
|
+
content = "# [x] extract\ncode here"
|
|
121
|
+
replacements = [("# [x] extract", "# [v] extract")]
|
|
122
|
+
result = _apply_replacements(content, replacements)
|
|
123
|
+
assert "# [v] extract" in result
|
|
124
|
+
assert "# [x] extract" not in result
|
|
125
|
+
|
|
126
|
+
def test_apply_replacements_multiple(self):
|
|
127
|
+
"""Applies multiple replacements correctly."""
|
|
128
|
+
content = "```\n# [x] extract\ncode1\n```\n\n```\n# [x] extract\ncode2\n```"
|
|
129
|
+
replacements = [
|
|
130
|
+
("```\n# [x] extract\ncode1\n```", "```\n# [v] extract\ncode1\n```"),
|
|
131
|
+
("```\n# [x] extract\ncode2\n```", "```\n# [v] extract\ncode2\n```"),
|
|
132
|
+
]
|
|
133
|
+
result = _apply_replacements(content, replacements)
|
|
134
|
+
assert result.count("# [v] extract") == 2
|
|
135
|
+
assert "# [x] extract" not in result
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# =============================================================================
|
|
139
|
+
# Tests for file creation logic (force flag functionality)
|
|
140
|
+
# =============================================================================
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class TestFileCreation:
|
|
144
|
+
"""Tests for file creation with force flag."""
|
|
145
|
+
|
|
146
|
+
@patch("builtins.input", return_value="y")
|
|
147
|
+
def test_determine_should_create_with_user_confirmation(self, mock_input):
|
|
148
|
+
"""User confirms file creation."""
|
|
149
|
+
result = determine_should_create(skip_query=False)
|
|
150
|
+
assert result is True
|
|
151
|
+
mock_input.assert_called_once()
|
|
152
|
+
|
|
153
|
+
@patch("builtins.input", return_value="n")
|
|
154
|
+
def test_determine_should_create_user_declines(self, mock_input):
|
|
155
|
+
"""User declines file creation."""
|
|
156
|
+
result = determine_should_create(skip_query=False)
|
|
157
|
+
assert result is False
|
|
158
|
+
|
|
159
|
+
def test_determine_should_create_skips_query_when_force(self):
|
|
160
|
+
"""Force flag bypasses user confirmation."""
|
|
161
|
+
result = determine_should_create(skip_query=True)
|
|
162
|
+
assert result is True
|
|
163
|
+
|
|
164
|
+
@patch("builtins.input", return_value="y")
|
|
165
|
+
def test_create_file_if_not_exist_creates_file(self, mock_input):
|
|
166
|
+
"""Creates file when it doesn't exist and user confirms."""
|
|
167
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
168
|
+
file_path = os.path.join(tmpdir, "new_file.txt")
|
|
169
|
+
content = "test content"
|
|
170
|
+
|
|
171
|
+
create_file_if_not_exist(file_path, content, skip_query=False)
|
|
172
|
+
|
|
173
|
+
assert os.path.exists(file_path)
|
|
174
|
+
with open(file_path, "r") as f:
|
|
175
|
+
assert f.read() == content
|
|
176
|
+
|
|
177
|
+
def test_create_file_if_not_exist_with_force(self):
|
|
178
|
+
"""Creates file without user prompt when force is True."""
|
|
179
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
180
|
+
file_path = os.path.join(tmpdir, "new_file.txt")
|
|
181
|
+
content = "test content"
|
|
182
|
+
|
|
183
|
+
create_file_if_not_exist(file_path, content, skip_query=True)
|
|
184
|
+
|
|
185
|
+
assert os.path.exists(file_path)
|
|
186
|
+
with open(file_path, "r") as f:
|
|
187
|
+
assert f.read() == content
|
|
188
|
+
|
|
189
|
+
def test_create_file_if_not_exist_creates_directories(self):
|
|
190
|
+
"""Creates parent directories if they don't exist."""
|
|
191
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
192
|
+
file_path = os.path.join(tmpdir, "subdir", "deep", "file.txt")
|
|
193
|
+
content = "nested content"
|
|
194
|
+
|
|
195
|
+
create_file_if_not_exist(file_path, content, skip_query=True)
|
|
196
|
+
|
|
197
|
+
assert os.path.exists(file_path)
|
|
198
|
+
with open(file_path, "r") as f:
|
|
199
|
+
assert f.read() == content
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# =============================================================================
|
|
203
|
+
# Tests for handle_existing_file (override functionality)
|
|
204
|
+
# =============================================================================
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class TestHandleExistingFile:
|
|
208
|
+
"""Tests for handling existing files during extraction."""
|
|
209
|
+
|
|
210
|
+
@patch("builtins.input", return_value="y")
|
|
211
|
+
def test_handle_existing_file_creates_when_not_exists(self, mock_input):
|
|
212
|
+
"""Creates file when it doesn't exist."""
|
|
213
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
214
|
+
file_path = os.path.join(tmpdir, "new_file.txt")
|
|
215
|
+
content = "new content"
|
|
216
|
+
|
|
217
|
+
handle_existing_file(file_path, content, skip_query=False, write=False)
|
|
218
|
+
|
|
219
|
+
assert os.path.exists(file_path)
|
|
220
|
+
|
|
221
|
+
def test_handle_existing_file_creates_with_force(self):
|
|
222
|
+
"""Creates file with force flag (skip_query=True)."""
|
|
223
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
224
|
+
file_path = os.path.join(tmpdir, "new_file.txt")
|
|
225
|
+
content = "new content"
|
|
226
|
+
|
|
227
|
+
handle_existing_file(file_path, content, skip_query=True, write=False)
|
|
228
|
+
|
|
229
|
+
assert os.path.exists(file_path)
|
|
230
|
+
with open(file_path, "r") as f:
|
|
231
|
+
assert f.read() == content
|
|
232
|
+
|
|
233
|
+
def test_handle_existing_file_overwrites_with_write_flag(self):
|
|
234
|
+
"""Overwrites existing file when write flag is True."""
|
|
235
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
236
|
+
file_path = os.path.join(tmpdir, "existing.txt")
|
|
237
|
+
|
|
238
|
+
# Create existing file
|
|
239
|
+
with open(file_path, "w") as f:
|
|
240
|
+
f.write("old content")
|
|
241
|
+
|
|
242
|
+
new_content = "new overwritten content"
|
|
243
|
+
handle_existing_file(file_path, new_content, skip_query=False, write=True)
|
|
244
|
+
|
|
245
|
+
with open(file_path, "r") as f:
|
|
246
|
+
assert f.read() == new_content
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# =============================================================================
|
|
250
|
+
# Tests for extract_responses (full extraction workflow)
|
|
251
|
+
# =============================================================================
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class TestExtractResponses:
|
|
255
|
+
"""Tests for the main extract_responses function."""
|
|
256
|
+
|
|
257
|
+
def test_extract_responses_marks_blocks(self):
|
|
258
|
+
"""Extracted blocks are marked with [v] instead of [x]."""
|
|
259
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
260
|
+
chat_file = os.path.join(tmpdir, "chat.md")
|
|
261
|
+
target_file = os.path.join(tmpdir, "output.txt")
|
|
262
|
+
|
|
263
|
+
content = f"""# ara prompt:
|
|
264
|
+
Some conversation
|
|
265
|
+
|
|
266
|
+
```
|
|
267
|
+
# [x] extract
|
|
268
|
+
# filename: {target_file}
|
|
269
|
+
print('hello world')
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
More conversation
|
|
273
|
+
"""
|
|
274
|
+
with open(chat_file, "w") as f:
|
|
275
|
+
f.write(content)
|
|
276
|
+
|
|
277
|
+
extract_responses(chat_file, relative_to_ara_root=False, force=True)
|
|
278
|
+
|
|
279
|
+
with open(chat_file, "r") as f:
|
|
280
|
+
updated_content = f.read()
|
|
281
|
+
|
|
282
|
+
assert "# [v] extract" in updated_content
|
|
283
|
+
assert "# [x] extract" not in updated_content
|
|
284
|
+
|
|
285
|
+
def test_extract_responses_creates_target_file(self):
|
|
286
|
+
"""Target file is created with extracted content."""
|
|
287
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
288
|
+
chat_file = os.path.join(tmpdir, "chat.md")
|
|
289
|
+
target_file = os.path.join(tmpdir, "output.py")
|
|
290
|
+
|
|
291
|
+
content = f"""```
|
|
292
|
+
# [x] extract
|
|
293
|
+
# filename: {target_file}
|
|
294
|
+
print('hello world')
|
|
295
|
+
```
|
|
296
|
+
"""
|
|
297
|
+
with open(chat_file, "w") as f:
|
|
298
|
+
f.write(content)
|
|
299
|
+
|
|
300
|
+
extract_responses(chat_file, relative_to_ara_root=False, force=True)
|
|
301
|
+
|
|
302
|
+
assert os.path.exists(target_file)
|
|
303
|
+
with open(target_file, "r") as f:
|
|
304
|
+
assert "print('hello world')" in f.read()
|
|
305
|
+
|
|
306
|
+
def test_extract_responses_handles_missing_file(self, capsys):
|
|
307
|
+
"""Handles missing document file gracefully."""
|
|
308
|
+
extract_responses("nonexistent_file.md", force=True)
|
|
309
|
+
captured = capsys.readouterr()
|
|
310
|
+
assert "File not found" in captured.out
|
|
311
|
+
|
|
312
|
+
def test_extract_responses_multiple_blocks(self):
|
|
313
|
+
"""Extracts multiple blocks from same document."""
|
|
314
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
315
|
+
chat_file = os.path.join(tmpdir, "chat.md")
|
|
316
|
+
file1 = os.path.join(tmpdir, "file1.txt")
|
|
317
|
+
file2 = os.path.join(tmpdir, "file2.txt")
|
|
318
|
+
|
|
319
|
+
content = f"""```
|
|
320
|
+
# [x] extract
|
|
321
|
+
# filename: {file1}
|
|
322
|
+
content 1
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
```
|
|
326
|
+
# [x] extract
|
|
327
|
+
# filename: {file2}
|
|
328
|
+
content 2
|
|
329
|
+
```
|
|
330
|
+
"""
|
|
331
|
+
with open(chat_file, "w") as f:
|
|
332
|
+
f.write(content)
|
|
333
|
+
|
|
334
|
+
extract_responses(chat_file, relative_to_ara_root=False, force=True)
|
|
335
|
+
|
|
336
|
+
assert os.path.exists(file1)
|
|
337
|
+
assert os.path.exists(file2)
|
|
338
|
+
|
|
339
|
+
with open(chat_file, "r") as f:
|
|
340
|
+
updated_content = f.read()
|
|
341
|
+
assert updated_content.count("# [v] extract") == 2
|
|
342
|
+
|
|
343
|
+
@pytest.mark.parametrize(
|
|
344
|
+
"classifier, template_body",
|
|
345
|
+
[
|
|
346
|
+
("businessgoal", "Businessgoal: sample businessgoal\nIn order to impress\nAs a person\nI want world domination\nDescription:"),
|
|
347
|
+
("capability", "Capability: sample capability\nContributes to\nTo be able to do things\nDescription:"),
|
|
348
|
+
("epic", "Epic: sample epic\nIn order to make criminals think twice before breaking the law\nAs a Batman\nI want all the gadgets\nDescription:"),
|
|
349
|
+
("example", "Example: sample example\nIllustrates\nDescription:"),
|
|
350
|
+
("feature", "Feature: sample feature\nAs a Batman\nI want to inspire fear\nSo that criminals don't break the law in the first place\nContributes to\nDescription:"),
|
|
351
|
+
("issue", "Issue: sample issue\nContributes to\nadditional description here\nDescription:"),
|
|
352
|
+
("keyfeature", "Keyfeature: sample keyfeature\nIn order to impress\nAs a person\nI want world domination\nDescription:"),
|
|
353
|
+
("task", "Task: sample task\nContributes to\nDescription:"),
|
|
354
|
+
("userstory", "Userstory: sample userstory\nAs a user\nI want to do things\nSo that valid\nDescription:"),
|
|
355
|
+
("vision", "Vision: sample vision\nContributes to\nFor blah\nWho blahs\nThe blah is a blah\nThat blah\nUnlike blah\nOur product blah\nDescription:"),
|
|
356
|
+
],
|
|
357
|
+
)
|
|
358
|
+
def test_extract_responses_all_types(self, classifier, template_body):
|
|
359
|
+
"""Extracts all supported artefact types correctly."""
|
|
360
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
361
|
+
chat_file = os.path.join(tmpdir, "chat.md")
|
|
362
|
+
|
|
363
|
+
target_file = os.path.join(tmpdir, "ara", f"{classifier}s", f"sample_{classifier}.{classifier}")
|
|
364
|
+
|
|
365
|
+
# Ensure target directory exists for extraction logic that doesn't create it recursively in all paths?
|
|
366
|
+
# Actually extract_responses creates directories.
|
|
367
|
+
|
|
368
|
+
content = f"""# ara prompt:
|
|
369
|
+
Chat content
|
|
370
|
+
|
|
371
|
+
```
|
|
372
|
+
# [x] extract
|
|
373
|
+
# filename: {target_file}
|
|
374
|
+
@creator_unknown
|
|
375
|
+
{template_body}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
End chat
|
|
379
|
+
"""
|
|
380
|
+
with open(chat_file, "w") as f:
|
|
381
|
+
f.write(content)
|
|
382
|
+
|
|
383
|
+
extract_responses(chat_file, relative_to_ara_root=False, force=True)
|
|
384
|
+
|
|
385
|
+
assert os.path.exists(target_file), f"Failed to create {classifier} file"
|
|
386
|
+
|
|
387
|
+
with open(target_file, "r") as f:
|
|
388
|
+
extracted_content = f.read()
|
|
389
|
+
|
|
390
|
+
# Verify some key part of the content exists
|
|
391
|
+
assert f"sample {classifier}" in extracted_content
|
|
392
|
+
|
|
393
|
+
with open(chat_file, "r") as f:
|
|
394
|
+
updated_chat = f.read()
|
|
395
|
+
|
|
396
|
+
assert "# [v] extract" in updated_chat
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
# =============================================================================
|
|
400
|
+
# Tests for process_document_blocks
|
|
401
|
+
# =============================================================================
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
class TestProcessDocumentBlocks:
|
|
405
|
+
"""Tests for processing document blocks."""
|
|
406
|
+
|
|
407
|
+
def test_process_document_blocks_returns_replacements(self):
|
|
408
|
+
"""Returns list of replacements for extracted blocks."""
|
|
409
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
410
|
+
original_dir = os.getcwd()
|
|
411
|
+
os.chdir(tmpdir)
|
|
412
|
+
|
|
413
|
+
try:
|
|
414
|
+
source_lines = [
|
|
415
|
+
"```",
|
|
416
|
+
"# [x] extract",
|
|
417
|
+
"# filename: test_output.txt",
|
|
418
|
+
"test content",
|
|
419
|
+
"```",
|
|
420
|
+
]
|
|
421
|
+
|
|
422
|
+
replacements = _process_document_blocks(
|
|
423
|
+
source_lines, force=True, write=False
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
assert len(replacements) == 1
|
|
427
|
+
original, modified = replacements[0]
|
|
428
|
+
assert "# [x] extract" in original
|
|
429
|
+
assert "# [v] extract" in modified
|
|
430
|
+
finally:
|
|
431
|
+
os.chdir(original_dir)
|
|
432
|
+
|
|
433
|
+
def test_process_document_blocks_no_extract_markers(self):
|
|
434
|
+
"""Returns empty list when no extract markers found."""
|
|
435
|
+
source_lines = ["```python", "print('hello')", "```"]
|
|
436
|
+
|
|
437
|
+
replacements = _process_document_blocks(source_lines, force=True, write=False)
|
|
438
|
+
assert replacements == []
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
# =============================================================================
|
|
442
|
+
# Tests for artefact class detection
|
|
443
|
+
# =============================================================================
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
class TestArtefactClassDetection:
|
|
447
|
+
"""Tests for detecting artefact class from content."""
|
|
448
|
+
|
|
449
|
+
def test_find_artefact_class_feature(self):
|
|
450
|
+
"""Detects Feature artefact class."""
|
|
451
|
+
from ara_cli.artefact_models.feature_artefact_model import FeatureArtefact
|
|
452
|
+
|
|
453
|
+
content_lines = ["Feature: sample feature", "As a user"]
|
|
454
|
+
result = _find_artefact_class(content_lines)
|
|
455
|
+
assert result == FeatureArtefact
|
|
456
|
+
|
|
457
|
+
def test_find_artefact_class_task(self):
|
|
458
|
+
"""Detects Task artefact class."""
|
|
459
|
+
from ara_cli.artefact_models.task_artefact_model import TaskArtefact
|
|
460
|
+
|
|
461
|
+
content_lines = ["Task: sample task", "Contributes to"]
|
|
462
|
+
result = _find_artefact_class(content_lines)
|
|
463
|
+
assert result == TaskArtefact
|
|
464
|
+
|
|
465
|
+
def test_find_artefact_class_with_tag(self):
|
|
466
|
+
"""Detects artefact class even with tags on first line."""
|
|
467
|
+
from ara_cli.artefact_models.feature_artefact_model import FeatureArtefact
|
|
468
|
+
|
|
469
|
+
content_lines = ["@creator_unknown", "Feature: sample feature"]
|
|
470
|
+
result = _find_artefact_class(content_lines)
|
|
471
|
+
assert result == FeatureArtefact
|
|
472
|
+
|
|
473
|
+
def test_find_artefact_class_unknown(self):
|
|
474
|
+
"""Returns None for unknown artefact type."""
|
|
475
|
+
content_lines = ["Unknown: something", "random content"]
|
|
476
|
+
result = _find_artefact_class(content_lines)
|
|
477
|
+
assert result is None
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
# =============================================================================
|
|
481
|
+
# Tests for extraction with different artefact types
|
|
482
|
+
# =============================================================================
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
class TestArtefactTypeExtraction:
|
|
486
|
+
"""Tests extraction for different artefact types (businessgoal, capability, etc.)."""
|
|
487
|
+
|
|
488
|
+
@pytest.fixture
|
|
489
|
+
def temp_ara_structure(self):
|
|
490
|
+
"""Creates a temporary ara directory structure."""
|
|
491
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
492
|
+
ara_dir = os.path.join(tmpdir, "ara")
|
|
493
|
+
os.makedirs(os.path.join(ara_dir, "businessgoals"))
|
|
494
|
+
os.makedirs(os.path.join(ara_dir, "capabilities"))
|
|
495
|
+
os.makedirs(os.path.join(ara_dir, "epics"))
|
|
496
|
+
os.makedirs(os.path.join(ara_dir, "features"))
|
|
497
|
+
os.makedirs(os.path.join(ara_dir, "tasks"))
|
|
498
|
+
yield tmpdir
|
|
499
|
+
|
|
500
|
+
@pytest.mark.parametrize(
|
|
501
|
+
"classifier,prefix",
|
|
502
|
+
[
|
|
503
|
+
("businessgoal", "Businessgoal:"),
|
|
504
|
+
("capability", "Capability:"),
|
|
505
|
+
("epic", "Epic:"),
|
|
506
|
+
("feature", "Feature:"),
|
|
507
|
+
("task", "Task:"),
|
|
508
|
+
("keyfeature", "Keyfeature:"),
|
|
509
|
+
("userstory", "Userstory:"),
|
|
510
|
+
("vision", "Vision:"),
|
|
511
|
+
("example", "Example:"),
|
|
512
|
+
("issue", "Issue:"),
|
|
513
|
+
],
|
|
514
|
+
)
|
|
515
|
+
def test_artefact_prefix_recognition(self, classifier, prefix):
|
|
516
|
+
"""Recognizes different artefact type prefixes."""
|
|
517
|
+
content_lines = [f"{prefix} sample {classifier}", "Contributes to"]
|
|
518
|
+
# Just verify it doesn't crash - detailed validation is done in artefact model tests
|
|
519
|
+
result = _find_artefact_class(content_lines)
|
|
520
|
+
# Some artefact types may not be in the mapping
|
|
521
|
+
# This test ensures we don't crash during recognition
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
# =============================================================================
|
|
525
|
+
# Tests for find_extract_token (markdown parsing)
|
|
526
|
+
# =============================================================================
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
class TestFindExtractToken:
|
|
530
|
+
"""Tests for finding extract tokens in markdown."""
|
|
531
|
+
|
|
532
|
+
def test_find_extract_token_found(self):
|
|
533
|
+
"""Finds extract token in tokens list."""
|
|
534
|
+
from markdown_it import MarkdownIt
|
|
535
|
+
|
|
536
|
+
md = MarkdownIt()
|
|
537
|
+
content = "```\n# [x] extract\ncontent\n```"
|
|
538
|
+
tokens = md.parse(content)
|
|
539
|
+
|
|
540
|
+
result = _find_extract_token(tokens)
|
|
541
|
+
assert result is not None
|
|
542
|
+
assert "# [x] extract" in result.content
|
|
543
|
+
|
|
544
|
+
def test_find_extract_token_not_found(self):
|
|
545
|
+
"""Returns None when no extract token present."""
|
|
546
|
+
from markdown_it import MarkdownIt
|
|
547
|
+
|
|
548
|
+
md = MarkdownIt()
|
|
549
|
+
content = "```\nsome code\n```"
|
|
550
|
+
tokens = md.parse(content)
|
|
551
|
+
|
|
552
|
+
result = _find_extract_token(tokens)
|
|
553
|
+
assert result is None
|
|
554
|
+
|
|
555
|
+
def test_find_extract_token_with_regular_text(self):
|
|
556
|
+
"""Returns None for regular text without code blocks."""
|
|
557
|
+
from markdown_it import MarkdownIt
|
|
558
|
+
|
|
559
|
+
md = MarkdownIt()
|
|
560
|
+
content = "Just some regular markdown text."
|
|
561
|
+
tokens = md.parse(content)
|
|
562
|
+
|
|
563
|
+
result = _find_extract_token(tokens)
|
|
564
|
+
assert result is None
|
tests/test_artefact_lister.py
CHANGED
|
@@ -134,8 +134,9 @@ def test_list_files(
|
|
|
134
134
|
expected_filtered,
|
|
135
135
|
):
|
|
136
136
|
# Mock ArtefactReader.read_artefacts
|
|
137
|
-
with patch("ara_cli.artefact_lister.ArtefactReader") as
|
|
138
|
-
|
|
137
|
+
with patch("ara_cli.artefact_lister.ArtefactReader") as mock_reader_class:
|
|
138
|
+
mock_reader_instance = mock_reader_class.return_value
|
|
139
|
+
mock_reader_instance.read_artefacts.return_value = mock_artefacts
|
|
139
140
|
|
|
140
141
|
# Mock filter_artefacts method
|
|
141
142
|
artefact_lister.filter_artefacts = MagicMock(return_value=filtered_artefacts)
|
|
@@ -153,7 +154,7 @@ def test_list_files(
|
|
|
153
154
|
)
|
|
154
155
|
|
|
155
156
|
# Verify the correct calls were made
|
|
156
|
-
|
|
157
|
+
mock_reader_instance.read_artefacts.assert_called_once_with(tags=tags)
|
|
157
158
|
artefact_lister.filter_artefacts.assert_called_once_with(
|
|
158
159
|
mock_artefacts, list_filter
|
|
159
160
|
)
|
|
@@ -233,7 +234,8 @@ def test_list_children(
|
|
|
233
234
|
mock_classifier_instance.classify_files.return_value = classified_artefacts
|
|
234
235
|
|
|
235
236
|
# Configure ArtefactReader mock
|
|
236
|
-
mock_artefact_reader.
|
|
237
|
+
mock_reader_instance = mock_artefact_reader.return_value
|
|
238
|
+
mock_reader_instance.find_children.return_value = child_artefacts
|
|
237
239
|
|
|
238
240
|
# Mock filter_artefacts method
|
|
239
241
|
artefact_lister.filter_artefacts = MagicMock(return_value=filtered_artefacts)
|
|
@@ -253,7 +255,7 @@ def test_list_children(
|
|
|
253
255
|
mock_suggest.assert_not_called()
|
|
254
256
|
|
|
255
257
|
# Verify ArtefactReader.find_children was called
|
|
256
|
-
|
|
258
|
+
mock_reader_instance.find_children.assert_called_once_with(
|
|
257
259
|
artefact_name=artefact_name, classifier=classifier
|
|
258
260
|
)
|
|
259
261
|
|
|
@@ -348,7 +350,8 @@ def test_list_branch(
|
|
|
348
350
|
for k, v in value_chain_artefacts.items():
|
|
349
351
|
artefacts_by_classifier[k] = v
|
|
350
352
|
|
|
351
|
-
|
|
353
|
+
mock_reader_instance = mock_artefact_reader.return_value
|
|
354
|
+
mock_reader_instance.step_through_value_chain.side_effect = mock_step_through
|
|
352
355
|
|
|
353
356
|
# Mock filter_artefacts method
|
|
354
357
|
artefact_lister.filter_artefacts = MagicMock(return_value=filtered_artefacts)
|
|
@@ -369,8 +372,8 @@ def test_list_branch(
|
|
|
369
372
|
mock_suggest.assert_not_called()
|
|
370
373
|
|
|
371
374
|
# Verify ArtefactReader.step_through_value_chain was called with correct parameters
|
|
372
|
-
|
|
373
|
-
call_args =
|
|
375
|
+
mock_reader_instance.step_through_value_chain.assert_called_once()
|
|
376
|
+
call_args = mock_reader_instance.step_through_value_chain.call_args[1]
|
|
374
377
|
assert call_args["artefact_name"] == artefact_name
|
|
375
378
|
assert call_args["classifier"] == classifier
|
|
376
379
|
assert classifier in call_args["artefacts_by_classifier"]
|