diff --git a/pyproject.toml b/pyproject.toml index 86597be..c70af53 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,3 +31,6 @@ dev = [ "pytest>=9.0.3", "ruff>=0.15.10", ] + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..5fee3e7 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +import sys +from pathlib import Path + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) + +from cc_slim.engine import resolve_config + + +def test_resolve_config_uses_defaults(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + monkeypatch.setenv("OPENAI_API_KEY", "env-openai-key") + + config = resolve_config(tmp_path, {}) + + assert config.provider == "openai" + assert config.model == "gpt-4.1-mini" + assert config.api_key == "env-openai-key" + assert config.base_url is None + assert config.max_turns == 12 + + +def test_resolve_config_priority_cli_over_env_over_file(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + (tmp_path / ".cc-slim.toml").write_text( + """ +[cc_slim] +provider = "anthropic" +model = "file-model" +api_key = "file-key" +base_url = "https://file.example" +max_turns = 4 +""".strip(), + encoding="utf-8", + ) + monkeypatch.setenv("CC_SLIM_PROVIDER", "openai") + monkeypatch.setenv("CC_SLIM_MODEL", "env-model") + monkeypatch.setenv("CC_SLIM_API_KEY", "env-key") + monkeypatch.setenv("CC_SLIM_BASE_URL", "https://env.example") + monkeypatch.setenv("CC_SLIM_MAX_TURNS", "9") + + config = resolve_config( + tmp_path, + { + "provider": "anthropic", + "model": "cli-model", + "api_key": "cli-key", + "base_url": "https://cli.example", + "max_turns": 15, + }, + ) + + assert config.provider == "anthropic" + assert config.model == "cli-model" + assert config.api_key == "cli-key" + assert config.base_url == "https://cli.example" + assert config.max_turns == 15 + + +def test_resolve_config_requires_api_key(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + monkeypatch.delenv("CC_SLIM_API_KEY", raising=False) + monkeypatch.delenv("OPENAI_API_KEY", raising=False) + monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False) + + with pytest.raises(ValueError, match="缺少 API key"): + resolve_config(tmp_path, {}) diff --git a/tests/test_tools.py b/tests/test_tools.py new file mode 100644 index 0000000..7bdbf7b --- /dev/null +++ b/tests/test_tools.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +import json +import sys +from pathlib import Path + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) + +from cc_slim.tools import ( + _safe_path, + bash_tool, + edit_tool, + glob_tool, + grep_tool, + read_tool, + write_tool, +) + + +def test_safe_path_allows_workspace_children(tmp_path: Path) -> None: + path = _safe_path(tmp_path, "a/b.txt") + + assert path == (tmp_path / "a/b.txt").resolve() + + +def test_safe_path_blocks_escape(tmp_path: Path) -> None: + with pytest.raises(ValueError, match="路径越过工作区边界"): + _safe_path(tmp_path, "../outside.txt") + + +def test_write_edit_read_glob_grep_flow(tmp_path: Path) -> None: + created = write_tool(tmp_path, {"path": "hello.py", "content": "print('hello')\n"}) + edited = edit_tool(tmp_path, {"path": "hello.py", "content": "print('world')\n"}) + read_back = read_tool(tmp_path, {"path": "hello.py"}) + globbed = glob_tool(tmp_path, {"pattern": "*.py"}) + grepped = grep_tool(tmp_path, {"pattern": "world", "path": "."}) + + assert created == "已创建文件: hello.py" + assert edited == "已修改文件: hello.py" + assert "1: print('world')" in read_back + assert globbed == "hello.py" + assert "hello.py:1: print('world')" in grepped + + +def test_bash_tool_success(tmp_path: Path) -> None: + result = bash_tool(tmp_path, {"command": "cmd /c exit 0"}) + payload = json.loads(result) + + assert payload["returncode"] == 0 + + +def test_edit_requires_existing_file(tmp_path: Path) -> None: + result = edit_tool(tmp_path, {"path": "missing.txt", "content": "x"}) + + assert result == "文件不存在: missing.txt"