mirror of
https://github.com/shareAI-lab/analysis_claude_code.git
synced 2026-09-20 12:13:38 +08:00
fix: use UTF-8 for text file operations
This commit is contained in:
@@ -147,6 +147,47 @@ def run_glob_tool(lesson, workdir: Path, pattern: str) -> str:
|
||||
return session._run_tool("glob", {"pattern": pattern})
|
||||
|
||||
|
||||
def run_text_tool(lesson, workdir: Path, name: str, arguments: dict) -> str:
|
||||
handlers = {
|
||||
"read_file": "run_read",
|
||||
"write_file": "run_write",
|
||||
"edit_file": "run_edit",
|
||||
}
|
||||
handler = getattr(lesson, handlers[name], None)
|
||||
if handler is not None:
|
||||
return handler(**arguments)
|
||||
session = object.__new__(lesson.AgentSession)
|
||||
session.workdir = workdir.resolve()
|
||||
return session._run_tool(name, arguments)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("lesson_path", GLOB_LESSONS,
|
||||
ids=lambda path: path.parent.name)
|
||||
def test_text_tools_use_utf8_for_non_ascii_content(
|
||||
tmp_path: Path, lesson_path: Path):
|
||||
lesson = load_lesson(tmp_path, lesson_path)
|
||||
path = tmp_path / "note.txt"
|
||||
original = "你好,UTF-8\n"
|
||||
|
||||
written = run_text_tool(
|
||||
lesson, tmp_path, "write_file", {"path": path.name, "content": original}
|
||||
)
|
||||
read = run_text_tool(
|
||||
lesson, tmp_path, "read_file", {"path": path.name}
|
||||
)
|
||||
edited = run_text_tool(
|
||||
lesson,
|
||||
tmp_path,
|
||||
"edit_file",
|
||||
{"path": path.name, "old_text": "UTF-8", "new_text": "跨平台"},
|
||||
)
|
||||
|
||||
assert not written.startswith("Error:")
|
||||
assert read == original.rstrip()
|
||||
assert not edited.startswith("Error:")
|
||||
assert path.read_bytes() == "你好,跨平台\n".encode("utf-8")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("lesson_path", GLOB_LESSONS,
|
||||
ids=lambda path: path.parent.name)
|
||||
def test_glob_double_star_matches_files_at_any_depth(
|
||||
|
||||
@@ -24,7 +24,7 @@ def test_every_chapter_has_the_same_language_navigation() -> None:
|
||||
|
||||
for chapter in CHAPTERS:
|
||||
for filename in ("README.md", "README.zh.md", "README.ja.md"):
|
||||
lines = (chapter / filename).read_text().splitlines()
|
||||
lines = (chapter / filename).read_text(encoding="utf-8").splitlines()
|
||||
assert lines[2] == expected
|
||||
|
||||
|
||||
|
||||
@@ -93,6 +93,33 @@ UNIQUE_FULL_INSTRUCTION
|
||||
assert lesson.TOOL_HANDLERS["load_skill"]("code-review") == manifest
|
||||
|
||||
|
||||
def test_skill_loaders_read_utf8_manifests() -> None:
|
||||
manifest = """---
|
||||
name: chinese-skill
|
||||
description: 处理中文内容
|
||||
---
|
||||
|
||||
# 中文技能
|
||||
"""
|
||||
for lesson_path in SKILL_LESSONS:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
skill_dir = root / "skills" / "chinese-skill"
|
||||
skill_dir.mkdir(parents=True)
|
||||
(skill_dir / "SKILL.md").write_bytes(manifest.encode("utf-8"))
|
||||
|
||||
lesson = load_lesson(root, lesson_path)
|
||||
registry = (lesson.SKILL_LOADER.skills
|
||||
if hasattr(lesson, "SKILL_LOADER")
|
||||
else lesson.SKILL_REGISTRY)
|
||||
loaded = (lesson.SKILL_LOADER.load("chinese-skill")
|
||||
if hasattr(lesson, "SKILL_LOADER")
|
||||
else lesson.load_skill("chinese-skill"))
|
||||
|
||||
assert registry["chinese-skill"]["description"] == "处理中文内容"
|
||||
assert loaded == manifest
|
||||
|
||||
|
||||
def test_s07_exposes_only_base_tools_and_load_skill() -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
lesson = load_lesson(Path(tmp))
|
||||
|
||||
62
tests/test_utf8_text_io.py
Normal file
62
tests/test_utf8_text_io.py
Normal file
@@ -0,0 +1,62 @@
|
||||
import ast
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SOURCE_FILES = tuple(sorted([
|
||||
*ROOT.glob("s*/code.py"),
|
||||
*ROOT.glob("agents/*.py"),
|
||||
*ROOT.glob("skills/agent-builder/**/*.py"),
|
||||
]))
|
||||
|
||||
|
||||
def missing_encoding(path: Path) -> list[str]:
|
||||
tree = ast.parse(path.read_text(encoding="utf-8"))
|
||||
missing = []
|
||||
for node in ast.walk(tree):
|
||||
if not isinstance(node, ast.Call):
|
||||
continue
|
||||
|
||||
path_method = (
|
||||
isinstance(node.func, ast.Attribute)
|
||||
and node.func.attr in {"read_text", "write_text"}
|
||||
)
|
||||
builtin_open = isinstance(node.func, ast.Name) and node.func.id == "open"
|
||||
path_open = (
|
||||
isinstance(node.func, ast.Attribute)
|
||||
and node.func.attr == "open"
|
||||
and not (
|
||||
isinstance(node.func.value, ast.Name)
|
||||
and node.func.value.id == "os"
|
||||
)
|
||||
)
|
||||
if not (path_method or builtin_open or path_open):
|
||||
continue
|
||||
if not any(keyword.arg == "encoding" for keyword in node.keywords):
|
||||
mode_index = 1 if builtin_open else 0
|
||||
mode = node.args[mode_index] if len(node.args) > mode_index else None
|
||||
if (isinstance(mode, ast.Constant) and isinstance(mode.value, str)
|
||||
and "b" in mode.value):
|
||||
continue
|
||||
label = path.relative_to(ROOT) if path.is_relative_to(ROOT) else path
|
||||
missing.append(f"{label}:{node.lineno}")
|
||||
return missing
|
||||
|
||||
|
||||
def test_teaching_sources_declare_text_encoding() -> None:
|
||||
missing = [item for path in SOURCE_FILES for item in missing_encoding(path)]
|
||||
assert not missing, "text operations missing encoding:\n" + "\n".join(missing)
|
||||
|
||||
|
||||
def test_agent_builder_generates_utf8_text_tools(tmp_path: Path) -> None:
|
||||
script = ROOT / "skills" / "agent-builder" / "scripts" / "init_agent.py"
|
||||
spec = importlib.util.spec_from_file_location("agent_builder_init", script)
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
module.create_agent("utf8-agent", 2, tmp_path)
|
||||
|
||||
generated = tmp_path / "utf8-agent" / "utf8-agent.py"
|
||||
assert not missing_encoding(generated)
|
||||
Reference in New Issue
Block a user