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.

Files changed (65) hide show
  1. {auto_coder-0.1.362.dist-info → auto_coder-0.1.364.dist-info}/METADATA +2 -2
  2. {auto_coder-0.1.362.dist-info → auto_coder-0.1.364.dist-info}/RECORD +65 -22
  3. autocoder/agent/base_agentic/__init__.py +0 -0
  4. autocoder/agent/base_agentic/agent_hub.py +169 -0
  5. autocoder/agent/base_agentic/agentic_lang.py +112 -0
  6. autocoder/agent/base_agentic/agentic_tool_display.py +180 -0
  7. autocoder/agent/base_agentic/base_agent.py +1582 -0
  8. autocoder/agent/base_agentic/default_tools.py +683 -0
  9. autocoder/agent/base_agentic/test_base_agent.py +82 -0
  10. autocoder/agent/base_agentic/tool_registry.py +425 -0
  11. autocoder/agent/base_agentic/tools/__init__.py +12 -0
  12. autocoder/agent/base_agentic/tools/ask_followup_question_tool_resolver.py +72 -0
  13. autocoder/agent/base_agentic/tools/attempt_completion_tool_resolver.py +37 -0
  14. autocoder/agent/base_agentic/tools/base_tool_resolver.py +35 -0
  15. autocoder/agent/base_agentic/tools/example_tool_resolver.py +46 -0
  16. autocoder/agent/base_agentic/tools/execute_command_tool_resolver.py +72 -0
  17. autocoder/agent/base_agentic/tools/list_files_tool_resolver.py +110 -0
  18. autocoder/agent/base_agentic/tools/plan_mode_respond_tool_resolver.py +35 -0
  19. autocoder/agent/base_agentic/tools/read_file_tool_resolver.py +54 -0
  20. autocoder/agent/base_agentic/tools/replace_in_file_tool_resolver.py +156 -0
  21. autocoder/agent/base_agentic/tools/search_files_tool_resolver.py +134 -0
  22. autocoder/agent/base_agentic/tools/talk_to_group_tool_resolver.py +96 -0
  23. autocoder/agent/base_agentic/tools/talk_to_tool_resolver.py +79 -0
  24. autocoder/agent/base_agentic/tools/use_mcp_tool_resolver.py +44 -0
  25. autocoder/agent/base_agentic/tools/write_to_file_tool_resolver.py +58 -0
  26. autocoder/agent/base_agentic/types.py +189 -0
  27. autocoder/agent/base_agentic/utils.py +100 -0
  28. autocoder/auto_coder_runner.py +6 -4
  29. autocoder/chat/conf_command.py +11 -10
  30. autocoder/common/__init__.py +2 -0
  31. autocoder/common/file_checkpoint/__init__.py +21 -0
  32. autocoder/common/file_checkpoint/backup.py +264 -0
  33. autocoder/common/file_checkpoint/examples.py +217 -0
  34. autocoder/common/file_checkpoint/manager.py +404 -0
  35. autocoder/common/file_checkpoint/models.py +156 -0
  36. autocoder/common/file_checkpoint/store.py +383 -0
  37. autocoder/common/file_checkpoint/test_backup.py +242 -0
  38. autocoder/common/file_checkpoint/test_manager.py +570 -0
  39. autocoder/common/file_checkpoint/test_models.py +360 -0
  40. autocoder/common/file_checkpoint/test_store.py +327 -0
  41. autocoder/common/file_checkpoint/test_utils.py +297 -0
  42. autocoder/common/file_checkpoint/utils.py +119 -0
  43. autocoder/common/rulefiles/autocoderrules_utils.py +138 -55
  44. autocoder/common/save_formatted_log.py +76 -5
  45. autocoder/common/v2/agent/agentic_edit.py +339 -216
  46. autocoder/common/v2/agent/agentic_edit_tools/read_file_tool_resolver.py +2 -2
  47. autocoder/common/v2/agent/agentic_edit_tools/replace_in_file_tool_resolver.py +100 -5
  48. autocoder/common/v2/agent/agentic_edit_tools/test_write_to_file_tool_resolver.py +322 -0
  49. autocoder/common/v2/agent/agentic_edit_tools/write_to_file_tool_resolver.py +160 -10
  50. autocoder/common/v2/agent/agentic_edit_types.py +1 -2
  51. autocoder/common/v2/agent/agentic_tool_display.py +2 -3
  52. autocoder/compilers/normal_compiler.py +64 -0
  53. autocoder/events/event_manager_singleton.py +133 -4
  54. autocoder/linters/normal_linter.py +373 -0
  55. autocoder/linters/python_linter.py +4 -2
  56. autocoder/rag/long_context_rag.py +424 -397
  57. autocoder/rag/test_doc_filter.py +393 -0
  58. autocoder/rag/test_long_context_rag.py +473 -0
  59. autocoder/rag/test_token_limiter.py +342 -0
  60. autocoder/shadows/shadow_manager.py +1 -3
  61. autocoder/version.py +1 -1
  62. {auto_coder-0.1.362.dist-info → auto_coder-0.1.364.dist-info}/LICENSE +0 -0
  63. {auto_coder-0.1.362.dist-info → auto_coder-0.1.364.dist-info}/WHEEL +0 -0
  64. {auto_coder-0.1.362.dist-info → auto_coder-0.1.364.dist-info}/entry_points.txt +0 -0
  65. {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"Successfully read file (shadow): {file_path}", content=content)
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"Successfully read file: {file_path}", content=content)
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
- with open(target_path, 'w', encoding='utf-8') as f:
130
- f.write(current_content)
131
- logger.info(f"Successfully applied {applied_count}/{len(parsed_blocks)} changes to file: {file_path}")
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
- return ToolResult(success=True, message=message, content=current_content)
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
+