auto-coder 0.1.362__py3-none-any.whl → 0.1.364__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of auto-coder might be problematic. Click here for more details.
- {auto_coder-0.1.362.dist-info → auto_coder-0.1.364.dist-info}/METADATA +2 -2
- {auto_coder-0.1.362.dist-info → auto_coder-0.1.364.dist-info}/RECORD +65 -22
- autocoder/agent/base_agentic/__init__.py +0 -0
- autocoder/agent/base_agentic/agent_hub.py +169 -0
- autocoder/agent/base_agentic/agentic_lang.py +112 -0
- autocoder/agent/base_agentic/agentic_tool_display.py +180 -0
- autocoder/agent/base_agentic/base_agent.py +1582 -0
- autocoder/agent/base_agentic/default_tools.py +683 -0
- autocoder/agent/base_agentic/test_base_agent.py +82 -0
- autocoder/agent/base_agentic/tool_registry.py +425 -0
- autocoder/agent/base_agentic/tools/__init__.py +12 -0
- autocoder/agent/base_agentic/tools/ask_followup_question_tool_resolver.py +72 -0
- autocoder/agent/base_agentic/tools/attempt_completion_tool_resolver.py +37 -0
- autocoder/agent/base_agentic/tools/base_tool_resolver.py +35 -0
- autocoder/agent/base_agentic/tools/example_tool_resolver.py +46 -0
- autocoder/agent/base_agentic/tools/execute_command_tool_resolver.py +72 -0
- autocoder/agent/base_agentic/tools/list_files_tool_resolver.py +110 -0
- autocoder/agent/base_agentic/tools/plan_mode_respond_tool_resolver.py +35 -0
- autocoder/agent/base_agentic/tools/read_file_tool_resolver.py +54 -0
- autocoder/agent/base_agentic/tools/replace_in_file_tool_resolver.py +156 -0
- autocoder/agent/base_agentic/tools/search_files_tool_resolver.py +134 -0
- autocoder/agent/base_agentic/tools/talk_to_group_tool_resolver.py +96 -0
- autocoder/agent/base_agentic/tools/talk_to_tool_resolver.py +79 -0
- autocoder/agent/base_agentic/tools/use_mcp_tool_resolver.py +44 -0
- autocoder/agent/base_agentic/tools/write_to_file_tool_resolver.py +58 -0
- autocoder/agent/base_agentic/types.py +189 -0
- autocoder/agent/base_agentic/utils.py +100 -0
- autocoder/auto_coder_runner.py +6 -4
- autocoder/chat/conf_command.py +11 -10
- autocoder/common/__init__.py +2 -0
- autocoder/common/file_checkpoint/__init__.py +21 -0
- autocoder/common/file_checkpoint/backup.py +264 -0
- autocoder/common/file_checkpoint/examples.py +217 -0
- autocoder/common/file_checkpoint/manager.py +404 -0
- autocoder/common/file_checkpoint/models.py +156 -0
- autocoder/common/file_checkpoint/store.py +383 -0
- autocoder/common/file_checkpoint/test_backup.py +242 -0
- autocoder/common/file_checkpoint/test_manager.py +570 -0
- autocoder/common/file_checkpoint/test_models.py +360 -0
- autocoder/common/file_checkpoint/test_store.py +327 -0
- autocoder/common/file_checkpoint/test_utils.py +297 -0
- autocoder/common/file_checkpoint/utils.py +119 -0
- autocoder/common/rulefiles/autocoderrules_utils.py +138 -55
- autocoder/common/save_formatted_log.py +76 -5
- autocoder/common/v2/agent/agentic_edit.py +339 -216
- autocoder/common/v2/agent/agentic_edit_tools/read_file_tool_resolver.py +2 -2
- autocoder/common/v2/agent/agentic_edit_tools/replace_in_file_tool_resolver.py +100 -5
- autocoder/common/v2/agent/agentic_edit_tools/test_write_to_file_tool_resolver.py +322 -0
- autocoder/common/v2/agent/agentic_edit_tools/write_to_file_tool_resolver.py +160 -10
- autocoder/common/v2/agent/agentic_edit_types.py +1 -2
- autocoder/common/v2/agent/agentic_tool_display.py +2 -3
- autocoder/compilers/normal_compiler.py +64 -0
- autocoder/events/event_manager_singleton.py +133 -4
- autocoder/linters/normal_linter.py +373 -0
- autocoder/linters/python_linter.py +4 -2
- autocoder/rag/long_context_rag.py +424 -397
- autocoder/rag/test_doc_filter.py +393 -0
- autocoder/rag/test_long_context_rag.py +473 -0
- autocoder/rag/test_token_limiter.py +342 -0
- autocoder/shadows/shadow_manager.py +1 -3
- autocoder/version.py +1 -1
- {auto_coder-0.1.362.dist-info → auto_coder-0.1.364.dist-info}/LICENSE +0 -0
- {auto_coder-0.1.362.dist-info → auto_coder-0.1.364.dist-info}/WHEEL +0 -0
- {auto_coder-0.1.362.dist-info → auto_coder-0.1.364.dist-info}/entry_points.txt +0 -0
- {auto_coder-0.1.362.dist-info → auto_coder-0.1.364.dist-info}/top_level.txt +0 -0
|
@@ -35,7 +35,7 @@ class ReadFileToolResolver(BaseToolResolver):
|
|
|
35
35
|
with open(shadow_path, 'r', encoding='utf-8', errors='replace') as f:
|
|
36
36
|
content = f.read()
|
|
37
37
|
logger.info(f"[Shadow] Successfully read shadow file: {shadow_path}")
|
|
38
|
-
return ToolResult(success=True, message=f"
|
|
38
|
+
return ToolResult(success=True, message=f"{file_path}", content=content)
|
|
39
39
|
except Exception as e:
|
|
40
40
|
pass
|
|
41
41
|
# else fallback to original file
|
|
@@ -48,7 +48,7 @@ class ReadFileToolResolver(BaseToolResolver):
|
|
|
48
48
|
with open(abs_file_path, 'r', encoding='utf-8', errors='replace') as f:
|
|
49
49
|
content = f.read()
|
|
50
50
|
logger.info(f"Successfully read file: {file_path}")
|
|
51
|
-
return ToolResult(success=True, message=f"
|
|
51
|
+
return ToolResult(success=True, message=f"{file_path}", content=content)
|
|
52
52
|
except Exception as e:
|
|
53
53
|
logger.error(f"Error reading file '{file_path}': {str(e)}")
|
|
54
54
|
return ToolResult(success=False, message=f"An error occurred while reading the file: {str(e)}")
|
|
@@ -5,6 +5,8 @@ import typing
|
|
|
5
5
|
from autocoder.common import AutoCoderArgs
|
|
6
6
|
from autocoder.common.v2.agent.agentic_edit_tools.base_tool_resolver import BaseToolResolver
|
|
7
7
|
from autocoder.common.v2.agent.agentic_edit_types import ReplaceInFileTool, ToolResult # Import ToolResult from types
|
|
8
|
+
from autocoder.common.file_checkpoint.models import FileChange as CheckpointFileChange
|
|
9
|
+
from autocoder.common.file_checkpoint.manager import FileChangeManager as CheckpointFileChangeManager
|
|
8
10
|
from loguru import logger
|
|
9
11
|
from autocoder.common.auto_coder_lang import get_message_with_format
|
|
10
12
|
if typing.TYPE_CHECKING:
|
|
@@ -14,7 +16,9 @@ class ReplaceInFileToolResolver(BaseToolResolver):
|
|
|
14
16
|
def __init__(self, agent: Optional['AgenticEdit'], tool: ReplaceInFileTool, args: AutoCoderArgs):
|
|
15
17
|
super().__init__(agent, tool, args)
|
|
16
18
|
self.tool: ReplaceInFileTool = tool # For type hinting
|
|
19
|
+
self.args = args
|
|
17
20
|
self.shadow_manager = self.agent.shadow_manager if self.agent else None
|
|
21
|
+
self.shadow_linter = self.agent.shadow_linter if self.agent else None
|
|
18
22
|
|
|
19
23
|
def parse_diff(self, diff_content: str) -> List[Tuple[str, str]]:
|
|
20
24
|
"""
|
|
@@ -58,6 +62,30 @@ class ReplaceInFileToolResolver(BaseToolResolver):
|
|
|
58
62
|
logger.warning(f"Could not parse any SEARCH/REPLACE blocks from diff: {diff_content}")
|
|
59
63
|
return blocks
|
|
60
64
|
|
|
65
|
+
def _format_lint_issues(self, lint_result):
|
|
66
|
+
"""
|
|
67
|
+
将 lint 结果格式化为可读的文本格式
|
|
68
|
+
|
|
69
|
+
参数:
|
|
70
|
+
lint_result: 单个文件的 lint 结果对象
|
|
71
|
+
|
|
72
|
+
返回:
|
|
73
|
+
str: 格式化的问题描述
|
|
74
|
+
"""
|
|
75
|
+
formatted_issues = []
|
|
76
|
+
|
|
77
|
+
for issue in lint_result.issues:
|
|
78
|
+
severity = "错误" if issue.severity.value == 3 else "警告" if issue.severity.value == 2 else "信息"
|
|
79
|
+
line_info = f"第{issue.position.line}行"
|
|
80
|
+
if issue.position.column:
|
|
81
|
+
line_info += f", 第{issue.position.column}列"
|
|
82
|
+
|
|
83
|
+
formatted_issues.append(
|
|
84
|
+
f" - [{severity}] {line_info}: {issue.message} (规则: {issue.code})"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return "\n".join(formatted_issues)
|
|
88
|
+
|
|
61
89
|
def resolve(self) -> ToolResult:
|
|
62
90
|
file_path = self.tool.path
|
|
63
91
|
diff_content = self.tool.diff
|
|
@@ -125,11 +153,62 @@ class ReplaceInFileToolResolver(BaseToolResolver):
|
|
|
125
153
|
return ToolResult(success=False, message=get_message_with_format("replace_in_file.apply_failed", errors="\n".join(errors)))
|
|
126
154
|
|
|
127
155
|
try:
|
|
128
|
-
os.makedirs(os.path.dirname(target_path), exist_ok=True)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
156
|
+
os.makedirs(os.path.dirname(target_path), exist_ok=True)
|
|
157
|
+
|
|
158
|
+
if self.agent and self.agent.checkpoint_manager:
|
|
159
|
+
changes = {
|
|
160
|
+
file_path: CheckpointFileChange(
|
|
161
|
+
file_path=file_path,
|
|
162
|
+
content=current_content,
|
|
163
|
+
is_deletion=False,
|
|
164
|
+
is_new=True
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
change_group_id = self.args.event_file
|
|
168
|
+
self.agent.checkpoint_manager.apply_changes(changes,change_group_id)
|
|
169
|
+
else:
|
|
170
|
+
with open(target_path, 'w', encoding='utf-8') as f:
|
|
171
|
+
f.write(current_content)
|
|
172
|
+
|
|
173
|
+
logger.info(f"Successfully applied {applied_count}/{len(parsed_blocks)} changes to file: {file_path}")
|
|
174
|
+
|
|
175
|
+
# 新增:执行代码质量检查
|
|
176
|
+
lint_results = None
|
|
177
|
+
lint_message = ""
|
|
178
|
+
formatted_issues = ""
|
|
179
|
+
has_lint_issues = False
|
|
180
|
+
|
|
181
|
+
# 检查是否启用了Lint功能
|
|
182
|
+
enable_lint = self.args.enable_auto_fix_lint
|
|
183
|
+
|
|
184
|
+
if enable_lint:
|
|
185
|
+
try:
|
|
186
|
+
if self.shadow_linter and self.shadow_manager:
|
|
187
|
+
# 对修改后的文件进行 lint 检查
|
|
188
|
+
shadow_path = target_path # 已经是影子路径
|
|
189
|
+
lint_results = self.shadow_linter.lint_shadow_file(shadow_path)
|
|
190
|
+
|
|
191
|
+
if lint_results and lint_results.issues:
|
|
192
|
+
has_lint_issues = True
|
|
193
|
+
# 格式化 lint 问题
|
|
194
|
+
formatted_issues = self._format_lint_issues(lint_results)
|
|
195
|
+
lint_message = f"\n\n代码质量检查发现 {len(lint_results.issues)} 个问题:\n{formatted_issues}"
|
|
196
|
+
else:
|
|
197
|
+
lint_message = "\n\n代码质量检查通过,未发现问题。"
|
|
198
|
+
if self.agent.linter:
|
|
199
|
+
lint_results = self.agent.linter.lint_file(file_path)
|
|
200
|
+
if lint_results and lint_results.issues:
|
|
201
|
+
has_lint_issues = True
|
|
202
|
+
# 格式化 lint 问题
|
|
203
|
+
formatted_issues = self._format_lint_issues(lint_results)
|
|
204
|
+
lint_message = f"\n\n代码质量检查发现 {len(lint_results.issues)} 个问题:\n{formatted_issues}"
|
|
205
|
+
except Exception as e:
|
|
206
|
+
logger.error(f"Lint 检查失败: {str(e)}")
|
|
207
|
+
lint_message = "\n\n尝试进行代码质量检查时出错。"
|
|
208
|
+
else:
|
|
209
|
+
logger.info("代码质量检查已禁用")
|
|
132
210
|
|
|
211
|
+
# 构建包含 lint 结果的返回消息
|
|
133
212
|
if errors:
|
|
134
213
|
message = get_message_with_format("replace_in_file.apply_success_with_warnings",
|
|
135
214
|
applied=applied_count,
|
|
@@ -141,13 +220,29 @@ class ReplaceInFileToolResolver(BaseToolResolver):
|
|
|
141
220
|
applied=applied_count,
|
|
142
221
|
total=len(parsed_blocks),
|
|
143
222
|
file_path=file_path)
|
|
223
|
+
|
|
224
|
+
# 将 lint 消息添加到结果中,如果启用了Lint
|
|
225
|
+
if enable_lint:
|
|
226
|
+
message += lint_message
|
|
144
227
|
|
|
145
228
|
# 变更跟踪,回调AgenticEdit
|
|
146
229
|
if self.agent:
|
|
147
230
|
rel_path = os.path.relpath(abs_file_path, abs_project_dir)
|
|
148
231
|
self.agent.record_file_change(rel_path, "modified", diff=diff_content, content=current_content)
|
|
149
232
|
|
|
150
|
-
|
|
233
|
+
# 附加 lint 结果到返回内容
|
|
234
|
+
result_content = {
|
|
235
|
+
"content": current_content,
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
# 只有在启用Lint时才添加Lint结果
|
|
239
|
+
if enable_lint:
|
|
240
|
+
result_content["lint_results"] = {
|
|
241
|
+
"has_issues": has_lint_issues,
|
|
242
|
+
"issues": formatted_issues if has_lint_issues else None
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return ToolResult(success=True, message=message, content=result_content)
|
|
151
246
|
except Exception as e:
|
|
152
247
|
logger.error(f"Error writing replaced content to file '{file_path}': {str(e)}")
|
|
153
248
|
return ToolResult(success=False, message=get_message_with_format("replace_in_file.write_error", error=str(e)))
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
|
|
2
|
+
import pytest
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import json
|
|
6
|
+
from unittest.mock import MagicMock, patch
|
|
7
|
+
|
|
8
|
+
from autocoder.common import AutoCoderArgs
|
|
9
|
+
from autocoder.common.v2.agent.agentic_edit_types import WriteToFileTool, ToolResult
|
|
10
|
+
from autocoder.common.v2.agent.agentic_edit_tools.write_to_file_tool_resolver import WriteToFileToolResolver
|
|
11
|
+
from autocoder.auto_coder_runner import load_tokenizer as load_tokenizer_global
|
|
12
|
+
from autocoder.utils.llms import get_single_llm
|
|
13
|
+
from autocoder.common.file_monitor.monitor import get_file_monitor, FileMonitor
|
|
14
|
+
from autocoder.common.rulefiles.autocoderrules_utils import get_rules, reset_rules_manager
|
|
15
|
+
from loguru import logger
|
|
16
|
+
|
|
17
|
+
# Helper to create a temporary test directory
|
|
18
|
+
@pytest.fixture(scope="function")
|
|
19
|
+
def temp_test_dir(tmp_path_factory):
|
|
20
|
+
temp_dir = tmp_path_factory.mktemp("test_write_to_file_resolver_")
|
|
21
|
+
logger.info(f"Created temp dir for test: {temp_dir}")
|
|
22
|
+
# Create a dummy .autocoderignore to avoid issues with default ignore patterns loading
|
|
23
|
+
# from unexpected places if the test is run from a different CWD.
|
|
24
|
+
with open(os.path.join(temp_dir, ".autocoderignore"), "w") as f:
|
|
25
|
+
f.write("# Dummy ignore file for tests\n")
|
|
26
|
+
yield temp_dir
|
|
27
|
+
logger.info(f"Cleaning up temp dir: {temp_dir}")
|
|
28
|
+
# shutil.rmtree(temp_dir) # tmp_path_factory handles cleanup
|
|
29
|
+
|
|
30
|
+
@pytest.fixture(scope="function")
|
|
31
|
+
def setup_file_monitor_and_rules(temp_test_dir):
|
|
32
|
+
"""Initializes FileMonitor and RulesManager for the test session."""
|
|
33
|
+
# Resetting instances to ensure test isolation
|
|
34
|
+
FileMonitor.reset_instance()
|
|
35
|
+
reset_rules_manager()
|
|
36
|
+
|
|
37
|
+
monitor = get_file_monitor(str(temp_test_dir))
|
|
38
|
+
if not monitor.is_running():
|
|
39
|
+
monitor.start()
|
|
40
|
+
logger.info(f"File monitor initialized with root: {monitor.root_dir}")
|
|
41
|
+
|
|
42
|
+
rules = get_rules(str(temp_test_dir))
|
|
43
|
+
logger.info(f"Rules loaded for dir: {temp_test_dir}, count: {len(rules)}")
|
|
44
|
+
return str(temp_test_dir)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@pytest.fixture(scope="function")
|
|
48
|
+
def load_tokenizer_fixture(setup_file_monitor_and_rules):
|
|
49
|
+
"""Loads the tokenizer."""
|
|
50
|
+
try:
|
|
51
|
+
load_tokenizer_global()
|
|
52
|
+
logger.info("Tokenizer loaded successfully.")
|
|
53
|
+
except Exception as e:
|
|
54
|
+
logger.error(f"Failed to load tokenizer: {e}")
|
|
55
|
+
# Depending on test requirements, you might want to raise an error or skip tests
|
|
56
|
+
pytest.skip(f"Skipping tests due to tokenizer loading failure: {e}")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@pytest.fixture(scope="function")
|
|
60
|
+
def test_args(temp_test_dir, setup_file_monitor_and_rules, load_tokenizer_fixture):
|
|
61
|
+
"""Provides default AutoCoderArgs for tests."""
|
|
62
|
+
args = AutoCoderArgs(
|
|
63
|
+
source_dir=str(temp_test_dir),
|
|
64
|
+
enable_auto_fix_lint=False, # Default to no linting for basic tests
|
|
65
|
+
# Potentially mock other args if needed by resolver or its dependencies
|
|
66
|
+
)
|
|
67
|
+
return args
|
|
68
|
+
|
|
69
|
+
@pytest.fixture
|
|
70
|
+
def mock_agent_no_shadow(test_args):
|
|
71
|
+
"""Mocks an AgenticEdit instance that does not provide shadow capabilities."""
|
|
72
|
+
agent = MagicMock()
|
|
73
|
+
agent.shadow_manager = None
|
|
74
|
+
agent.shadow_linter = None
|
|
75
|
+
agent.args = test_args
|
|
76
|
+
agent.record_file_change = MagicMock()
|
|
77
|
+
return agent
|
|
78
|
+
|
|
79
|
+
@pytest.fixture
|
|
80
|
+
def mock_agent_with_shadow(test_args, temp_test_dir):
|
|
81
|
+
"""Mocks an AgenticEdit instance with shadow capabilities."""
|
|
82
|
+
from autocoder.shadows.shadow_manager import ShadowManager
|
|
83
|
+
from autocoder.linters.shadow_linter import ShadowLinter
|
|
84
|
+
|
|
85
|
+
# Ensure the shadow base directory exists within the temp_test_dir for isolation
|
|
86
|
+
shadow_base_dir = os.path.join(temp_test_dir, ".auto-coder", "shadows")
|
|
87
|
+
os.makedirs(shadow_base_dir, exist_ok=True)
|
|
88
|
+
|
|
89
|
+
# Patch ShadowManager's default shadow_base to use our temp one
|
|
90
|
+
with patch('autocoder.shadows.shadow_manager.ShadowManager.DEFAULT_SHADOW_BASE_DIR', new=shadow_base_dir):
|
|
91
|
+
shadow_manager = ShadowManager(source_dir=str(temp_test_dir), event_file_id="test_event")
|
|
92
|
+
|
|
93
|
+
shadow_linter = ShadowLinter(shadow_manager=shadow_manager, verbose=False)
|
|
94
|
+
|
|
95
|
+
agent = MagicMock()
|
|
96
|
+
agent.shadow_manager = shadow_manager
|
|
97
|
+
agent.shadow_linter = shadow_linter
|
|
98
|
+
agent.args = test_args
|
|
99
|
+
agent.record_file_change = MagicMock()
|
|
100
|
+
return agent
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_create_new_file(test_args, temp_test_dir, mock_agent_no_shadow):
|
|
104
|
+
logger.info(f"Running test_create_new_file in {temp_test_dir}")
|
|
105
|
+
file_path = "new_file.txt"
|
|
106
|
+
content = "This is a new file."
|
|
107
|
+
tool = WriteToFileTool(path=file_path, content=content)
|
|
108
|
+
|
|
109
|
+
resolver = WriteToFileToolResolver(agent=mock_agent_no_shadow, tool=tool, args=test_args)
|
|
110
|
+
result = resolver.resolve()
|
|
111
|
+
|
|
112
|
+
assert result.success is True
|
|
113
|
+
assert "成功写入文件" in result.message or "Successfully wrote file" in result.message
|
|
114
|
+
|
|
115
|
+
expected_file_abs_path = os.path.join(temp_test_dir, file_path)
|
|
116
|
+
assert os.path.exists(expected_file_abs_path)
|
|
117
|
+
with open(expected_file_abs_path, "r", encoding="utf-8") as f:
|
|
118
|
+
assert f.read() == content
|
|
119
|
+
mock_agent_no_shadow.record_file_change.assert_called_once_with(file_path, "added", content=content, diffs=None)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_overwrite_existing_file(test_args, temp_test_dir, mock_agent_no_shadow):
|
|
123
|
+
logger.info(f"Running test_overwrite_existing_file in {temp_test_dir}")
|
|
124
|
+
file_path = "existing_file.txt"
|
|
125
|
+
initial_content = "Initial content."
|
|
126
|
+
new_content = "This is the new content."
|
|
127
|
+
|
|
128
|
+
abs_file_path = os.path.join(temp_test_dir, file_path)
|
|
129
|
+
with open(abs_file_path, "w", encoding="utf-8") as f:
|
|
130
|
+
f.write(initial_content)
|
|
131
|
+
|
|
132
|
+
tool = WriteToFileTool(path=file_path, content=new_content)
|
|
133
|
+
resolver = WriteToFileToolResolver(agent=mock_agent_no_shadow, tool=tool, args=test_args)
|
|
134
|
+
result = resolver.resolve()
|
|
135
|
+
|
|
136
|
+
assert result.success is True
|
|
137
|
+
assert os.path.exists(abs_file_path)
|
|
138
|
+
with open(abs_file_path, "r", encoding="utf-f8") as f:
|
|
139
|
+
assert f.read() == new_content
|
|
140
|
+
mock_agent_no_shadow.record_file_change.assert_called_once_with(file_path, "modified", content=new_content, diffs=None)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_create_file_in_new_directory(test_args, temp_test_dir, mock_agent_no_shadow):
|
|
144
|
+
logger.info(f"Running test_create_file_in_new_directory in {temp_test_dir}")
|
|
145
|
+
file_path = "new_dir/another_new_dir/file.txt"
|
|
146
|
+
content = "Content in a nested directory."
|
|
147
|
+
|
|
148
|
+
tool = WriteToFileTool(path=file_path, content=content)
|
|
149
|
+
resolver = WriteToFileToolResolver(agent=mock_agent_no_shadow, tool=tool, args=test_args)
|
|
150
|
+
result = resolver.resolve()
|
|
151
|
+
|
|
152
|
+
assert result.success is True
|
|
153
|
+
expected_file_abs_path = os.path.join(temp_test_dir, file_path)
|
|
154
|
+
assert os.path.exists(expected_file_abs_path)
|
|
155
|
+
with open(expected_file_abs_path, "r", encoding="utf-8") as f:
|
|
156
|
+
assert f.read() == content
|
|
157
|
+
mock_agent_no_shadow.record_file_change.assert_called_once_with(file_path, "added", content=content, diffs=None)
|
|
158
|
+
|
|
159
|
+
def test_path_outside_project_root_fails(test_args, temp_test_dir, mock_agent_no_shadow):
|
|
160
|
+
logger.info(f"Running test_path_outside_project_root_fails in {temp_test_dir}")
|
|
161
|
+
# Construct a path that tries to go outside the source_dir
|
|
162
|
+
# Note: The resolver's check is os.path.abspath(target_path).startswith(os.path.abspath(source_dir))
|
|
163
|
+
# So, a direct "../" might be normalized. We need a path that, when absolutized,
|
|
164
|
+
# is still outside an absolutized source_dir. This is tricky if source_dir is already root-like.
|
|
165
|
+
# For this test, we'll assume source_dir is not the filesystem root.
|
|
166
|
+
|
|
167
|
+
# A more robust way is to try to write to a known safe, but distinct, temporary directory
|
|
168
|
+
another_temp_dir = temp_test_dir.parent / "another_temp_dir_for_outside_test"
|
|
169
|
+
another_temp_dir.mkdir(exist_ok=True)
|
|
170
|
+
|
|
171
|
+
# This relative path, if source_dir is temp_test_dir, would resolve outside.
|
|
172
|
+
# However, the resolver joins it with source_dir first.
|
|
173
|
+
# file_path = "../outside_file.txt" # This will be joined with source_dir
|
|
174
|
+
|
|
175
|
+
# Let's try an absolute path that is outside temp_test_dir
|
|
176
|
+
outside_abs_path = os.path.join(another_temp_dir, "outside_file.txt")
|
|
177
|
+
|
|
178
|
+
# The tool path is relative to source_dir. So, to make it point outside,
|
|
179
|
+
# we need to construct a relative path that goes "up" from source_dir.
|
|
180
|
+
# This requires knowing the relative position of temp_test_dir.
|
|
181
|
+
# A simpler test for the security check:
|
|
182
|
+
# Give an absolute path to the tool that is outside test_args.source_dir.
|
|
183
|
+
# The resolver logic is:
|
|
184
|
+
# abs_file_path = os.path.abspath(os.path.join(source_dir, file_path_from_tool))
|
|
185
|
+
# So, if file_path_from_tool is already absolute, os.path.join might behave unexpectedly on Windows.
|
|
186
|
+
# On POSIX, if file_path_from_tool is absolute, os.path.join returns file_path_from_tool.
|
|
187
|
+
|
|
188
|
+
if os.name == 'posix':
|
|
189
|
+
file_path_for_tool = outside_abs_path
|
|
190
|
+
else: # Windows, os.path.join with absolute second path is tricky
|
|
191
|
+
# For windows, this test might need adjustment or rely on the fact that
|
|
192
|
+
# the file_path parameter to the tool is *expected* to be relative.
|
|
193
|
+
# Providing an absolute path might be an invalid use case for the tool itself.
|
|
194
|
+
# The resolver's security check should still catch it if os.path.join(source_dir, abs_path)
|
|
195
|
+
# results in abs_path and abs_path is outside source_dir.
|
|
196
|
+
file_path_for_tool = outside_abs_path
|
|
197
|
+
|
|
198
|
+
content = "Attempting to write outside."
|
|
199
|
+
tool = WriteToFileTool(path=str(file_path_for_tool), content=content)
|
|
200
|
+
|
|
201
|
+
resolver = WriteToFileToolResolver(agent=mock_agent_no_shadow, tool=tool, args=test_args)
|
|
202
|
+
result = resolver.resolve()
|
|
203
|
+
|
|
204
|
+
assert result.success is False
|
|
205
|
+
assert "访问被拒绝" in result.message or "Access denied" in result.message
|
|
206
|
+
assert not os.path.exists(outside_abs_path)
|
|
207
|
+
|
|
208
|
+
# shutil.rmtree(another_temp_dir) # Clean up the other temp dir if created by this test
|
|
209
|
+
|
|
210
|
+
def test_linting_not_called_if_disabled(test_args, temp_test_dir, mock_agent_no_shadow):
|
|
211
|
+
logger.info(f"Running test_linting_not_called_if_disabled in {temp_test_dir}")
|
|
212
|
+
test_args.enable_auto_fix_lint = False # Explicitly disable
|
|
213
|
+
|
|
214
|
+
file_path = "no_lint_file.py"
|
|
215
|
+
content = "print('hello')"
|
|
216
|
+
tool = WriteToFileTool(path=file_path, content=content)
|
|
217
|
+
|
|
218
|
+
# Mock the linter parts if they were to be called
|
|
219
|
+
if mock_agent_no_shadow and hasattr(mock_agent_no_shadow, 'shadow_linter') and mock_agent_no_shadow.shadow_linter:
|
|
220
|
+
mock_agent_no_shadow.shadow_linter.lint_shadow_file = MagicMock(return_value=None) # Should not be called
|
|
221
|
+
|
|
222
|
+
resolver = WriteToFileToolResolver(agent=mock_agent_no_shadow, tool=tool, args=test_args)
|
|
223
|
+
result = resolver.resolve()
|
|
224
|
+
|
|
225
|
+
assert result.success is True
|
|
226
|
+
if mock_agent_no_shadow and hasattr(mock_agent_no_shadow, 'shadow_linter') and mock_agent_no_shadow.shadow_linter:
|
|
227
|
+
mock_agent_no_shadow.shadow_linter.lint_shadow_file.assert_not_called()
|
|
228
|
+
|
|
229
|
+
# Check if "代码质量检查已禁用" or "Linting is disabled" is in message
|
|
230
|
+
assert "代码质量检查已禁用" in result.message or "Linting is disabled" in result.message or "成功写入文件" in result.message
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def test_linting_called_if_enabled(test_args, temp_test_dir, mock_agent_with_shadow):
|
|
234
|
+
logger.info(f"Running test_linting_called_if_enabled in {temp_test_dir}")
|
|
235
|
+
test_args.enable_auto_fix_lint = True # Explicitly enable
|
|
236
|
+
|
|
237
|
+
file_path = "lint_file.py"
|
|
238
|
+
content = "print('hello world')" # Valid python
|
|
239
|
+
tool = WriteToFileTool(path=file_path, content=content)
|
|
240
|
+
|
|
241
|
+
# Mock the lint_shadow_file method on the shadow_linter provided by mock_agent_with_shadow
|
|
242
|
+
mock_lint_result = MagicMock()
|
|
243
|
+
mock_lint_result.issues = [] # No issues
|
|
244
|
+
mock_agent_with_shadow.shadow_linter.lint_shadow_file = MagicMock(return_value=mock_lint_result)
|
|
245
|
+
|
|
246
|
+
resolver = WriteToFileToolResolver(agent=mock_agent_with_shadow, tool=tool, args=test_args)
|
|
247
|
+
result = resolver.resolve()
|
|
248
|
+
|
|
249
|
+
assert result.success is True
|
|
250
|
+
mock_agent_with_shadow.shadow_linter.lint_shadow_file.assert_called_once()
|
|
251
|
+
# The actual path passed to lint_shadow_file will be the shadow path
|
|
252
|
+
shadow_path = mock_agent_with_shadow.shadow_manager.to_shadow_path(os.path.join(temp_test_dir, file_path))
|
|
253
|
+
mock_agent_with_shadow.shadow_linter.lint_shadow_file.assert_called_with(shadow_path)
|
|
254
|
+
assert "代码质量检查通过" in result.message or "Linting passed" in result.message
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def test_create_file_with_shadow_manager(test_args, temp_test_dir, mock_agent_with_shadow):
|
|
258
|
+
logger.info(f"Running test_create_file_with_shadow_manager in {temp_test_dir}")
|
|
259
|
+
file_path = "shadowed_file.txt"
|
|
260
|
+
content = "This file should be in the shadow realm."
|
|
261
|
+
tool = WriteToFileTool(path=file_path, content=content)
|
|
262
|
+
|
|
263
|
+
resolver = WriteToFileToolResolver(agent=mock_agent_with_shadow, tool=tool, args=test_args)
|
|
264
|
+
result = resolver.resolve()
|
|
265
|
+
|
|
266
|
+
assert result.success is True
|
|
267
|
+
|
|
268
|
+
real_file_abs_path = os.path.join(temp_test_dir, file_path)
|
|
269
|
+
shadow_file_abs_path = mock_agent_with_shadow.shadow_manager.to_shadow_path(real_file_abs_path)
|
|
270
|
+
|
|
271
|
+
assert not os.path.exists(real_file_abs_path) # Real file should not be created directly
|
|
272
|
+
assert os.path.exists(shadow_file_abs_path) # Shadow file should exist
|
|
273
|
+
with open(shadow_file_abs_path, "r", encoding="utf-8") as f:
|
|
274
|
+
assert f.read() == content
|
|
275
|
+
|
|
276
|
+
# Agent's record_file_change should still be called with the original relative path
|
|
277
|
+
mock_agent_with_shadow.record_file_change.assert_called_once_with(file_path, "added", content=content, diffs=None)
|
|
278
|
+
|
|
279
|
+
# Clean up shadows for this test if needed, though mock_agent_with_shadow might do it
|
|
280
|
+
# mock_agent_with_shadow.shadow_manager.clean_shadows()
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def test_linting_error_message_propagation(test_args, temp_test_dir, mock_agent_with_shadow):
|
|
284
|
+
logger.info(f"Running test_linting_error_message_propagation in {temp_test_dir}")
|
|
285
|
+
test_args.enable_auto_fix_lint = True
|
|
286
|
+
|
|
287
|
+
file_path = "lint_error_file.py"
|
|
288
|
+
content = "print 'hello'" # Python 2 print, will cause lint error in Python 3 env with basic pyflakes
|
|
289
|
+
tool = WriteToFileTool(path=file_path, content=content)
|
|
290
|
+
|
|
291
|
+
mock_issue = MagicMock()
|
|
292
|
+
mock_issue.severity = MagicMock() # Simulate IssueSeverity enum if needed by _format_lint_issues
|
|
293
|
+
mock_issue.severity.value = "ERROR" # Assuming _format_lint_issues checks for severity.value
|
|
294
|
+
mock_issue.position.line = 1
|
|
295
|
+
mock_issue.position.column = 0
|
|
296
|
+
mock_issue.message = "SyntaxError: Missing parentheses in call to 'print'"
|
|
297
|
+
mock_issue.code = "E999"
|
|
298
|
+
|
|
299
|
+
mock_lint_result = MagicMock()
|
|
300
|
+
mock_lint_result.issues = [mock_issue]
|
|
301
|
+
mock_lint_result.file_results = {
|
|
302
|
+
mock_agent_with_shadow.shadow_manager.to_shadow_path(os.path.join(temp_test_dir, file_path)): mock_lint_result
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
mock_agent_with_shadow.shadow_linter.lint_shadow_file = MagicMock(return_value=mock_lint_result)
|
|
307
|
+
# Mock _format_lint_issues if it's complex or to control its output precisely
|
|
308
|
+
# For now, assume it works as expected based on WriteToFileToolResolver's internal call
|
|
309
|
+
|
|
310
|
+
resolver = WriteToFileToolResolver(agent=mock_agent_with_shadow, tool=tool, args=test_args)
|
|
311
|
+
|
|
312
|
+
# Temporarily patch _format_lint_issues within the resolver instance for this test
|
|
313
|
+
# to ensure consistent output for assertion.
|
|
314
|
+
formatted_issue_text = f"文件: {mock_agent_with_shadow.shadow_manager.to_shadow_path(os.path.join(temp_test_dir, file_path))}\n - [错误] 第1行, 第0列: SyntaxError: Missing parentheses in call to 'print' (规则: E999)\n"
|
|
315
|
+
with patch.object(resolver, '_format_lint_issues', return_value=formatted_issue_text) as mock_format:
|
|
316
|
+
result = resolver.resolve()
|
|
317
|
+
|
|
318
|
+
assert result.success is True # Write itself is successful
|
|
319
|
+
mock_format.assert_called_once_with(mock_lint_result)
|
|
320
|
+
assert "代码质量检查发现 1 个问题" in result.message or "Linting found 1 issue(s)" in result.message
|
|
321
|
+
assert "SyntaxError: Missing parentheses in call to 'print'" in result.message
|
|
322
|
+
|