- Core types: Rational time, EntityId, ActionBatch, PngBytes (no Pillow dep) - Config: ~/.quercus/config.toml with 0600 perms, env var override for CI - LLMProvider: Claude (anthropic SDK) and OpenAI-compatible endpoint (covers custom gateways and llama.cpp server mode); graceful degradation without API keys - Tool schemas: 26 curated tools with JSON Schema and read_only flags - AgentLoop: sync dialogue loop with confirmation gate (read-only batches bypass, rejected mutations are reported back to the LLM), PNG frame feedback for multimodal providers - Session log: JSONL recording with frame sha256 hashes, replayable - HostAdapter ABC (signatures only, aligned with OPP/1 method families) - Mock LLM provider (scripted/replay/record) for network-free CI Tests: 69 passed via uv run pytest (no network access)
84 lines
3.1 KiB
Python
84 lines
3.1 KiB
Python
"""配置:0600 权限、环境变量覆盖优先级、密钥不落日志。"""
|
|
import stat
|
|
|
|
import pytest
|
|
|
|
from quercus_core.config import (
|
|
ENV_ANTHROPIC_API_KEY,
|
|
ENV_OPENAI_API_KEY,
|
|
ENV_OPENAI_BASE_URL,
|
|
Config,
|
|
config_home,
|
|
config_summary,
|
|
default_config_path,
|
|
default_sessions_dir,
|
|
load_config,
|
|
save_config,
|
|
)
|
|
|
|
|
|
class TestSaveLoad:
|
|
def test_save_creates_0600(self, tmp_path):
|
|
path = tmp_path / "cfg" / "config.toml"
|
|
save_config(Config(anthropic_api_key="sk-ant-secret"), path)
|
|
assert path.exists()
|
|
assert stat.S_IMODE(path.stat().st_mode) == 0o600
|
|
|
|
def test_roundtrip(self, tmp_path):
|
|
path = tmp_path / "config.toml"
|
|
save_config(
|
|
Config(
|
|
anthropic_api_key="sk-a",
|
|
openai_api_key="sk-o",
|
|
openai_base_url="https://gw.example/v1",
|
|
),
|
|
path,
|
|
)
|
|
cfg = load_config(path)
|
|
assert cfg.anthropic_api_key == "sk-a"
|
|
assert cfg.openai_api_key == "sk-o"
|
|
assert cfg.openai_base_url == "https://gw.example/v1"
|
|
assert cfg.has_anthropic_key and cfg.has_openai_key
|
|
|
|
def test_env_overrides_file(self, tmp_path, monkeypatch):
|
|
path = tmp_path / "config.toml"
|
|
save_config(Config(anthropic_api_key="file-key", openai_api_key="file-o"), path)
|
|
monkeypatch.setenv(ENV_ANTHROPIC_API_KEY, "env-key")
|
|
cfg = load_config(path)
|
|
assert cfg.anthropic_api_key == "env-key" # 环境变量 > 配置文件
|
|
assert cfg.openai_api_key == "file-o" # 未覆盖的保持文件值
|
|
|
|
def test_env_without_file(self, tmp_path, monkeypatch):
|
|
monkeypatch.setenv(ENV_OPENAI_API_KEY, "env-o")
|
|
monkeypatch.setenv(ENV_OPENAI_BASE_URL, "http://localhost:8080/v1")
|
|
monkeypatch.delenv(ENV_ANTHROPIC_API_KEY, raising=False)
|
|
cfg = load_config(tmp_path / "missing.toml")
|
|
assert cfg.openai_api_key == "env-o"
|
|
assert cfg.openai_base_url == "http://localhost:8080/v1"
|
|
assert cfg.anthropic_api_key is None
|
|
|
|
def test_no_config_no_env(self, tmp_path, monkeypatch):
|
|
monkeypatch.delenv(ENV_ANTHROPIC_API_KEY, raising=False)
|
|
monkeypatch.delenv(ENV_OPENAI_API_KEY, raising=False)
|
|
monkeypatch.delenv(ENV_OPENAI_BASE_URL, raising=False)
|
|
cfg = load_config(tmp_path / "missing.toml")
|
|
assert cfg.anthropic_api_key is None
|
|
assert cfg.openai_api_key is None
|
|
assert cfg.openai_base_url is None
|
|
assert not cfg.has_anthropic_key and not cfg.has_openai_key
|
|
|
|
|
|
class TestHome:
|
|
def test_home_override(self, tmp_home):
|
|
assert config_home() == tmp_home / ".quercus"
|
|
assert default_config_path() == tmp_home / ".quercus" / "config.toml"
|
|
assert default_sessions_dir() == tmp_home / ".quercus" / "sessions"
|
|
|
|
|
|
def test_config_summary_hides_secrets():
|
|
cfg = Config(anthropic_api_key="top-secret-value", openai_api_key="second-secret")
|
|
summary = config_summary(cfg)
|
|
assert summary["anthropic_configured"] is True
|
|
assert summary["openai_configured"] is True
|
|
assert "secret" not in str(summary)
|