Files
2026-07-13 13:12:33 +08:00

340 lines
10 KiB
Python

"""Tests for config_store persistence."""
from __future__ import annotations
import datetime as dt
import os
import stat
import tomllib
import pytest
import opensquilla.gateway.config_migration as migration_module
from opensquilla.gateway.config import AgentEntryConfig, GatewayConfig
from opensquilla.onboarding.config_store import (
PersistResult,
default_config_path,
load_config,
persist_config,
resolve_config_path,
validate_config_payload,
)
def _clear_path_env(monkeypatch):
for key in (
"OPENSQUILLA_GATEWAY_CONFIG_PATH",
"OPENSQUILLA_STATE_DIR",
"OPENSQUILLA_HOME",
"OPENSQUILLA_PROFILE",
):
monkeypatch.delenv(key, raising=False)
def test_default_config_path_under_home(monkeypatch, tmp_path):
_clear_path_env(monkeypatch)
monkeypatch.setenv("HOME", str(tmp_path))
monkeypatch.chdir(tmp_path) # no opensquilla.toml here
p = default_config_path()
assert p == tmp_path / ".opensquilla" / "config.toml"
def test_default_path_uses_env_when_set(tmp_path, monkeypatch):
target = tmp_path / "explicit.toml"
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(target))
monkeypatch.chdir(tmp_path)
assert default_config_path() == target
def test_default_path_prefers_cwd_when_present(tmp_path, monkeypatch):
monkeypatch.delenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", raising=False)
monkeypatch.setenv("HOME", str(tmp_path / "home"))
project = tmp_path / "project"
project.mkdir()
(project / "opensquilla.toml").write_text("")
monkeypatch.chdir(project)
assert default_config_path() == project / "opensquilla.toml"
def test_resolve_config_path_ignores_cwd_directory(tmp_path, monkeypatch):
_clear_path_env(monkeypatch)
home = tmp_path / "home"
home.mkdir()
monkeypatch.setenv("HOME", str(home))
(tmp_path / "opensquilla.toml").mkdir()
monkeypatch.chdir(tmp_path)
path, source = resolve_config_path(None)
assert path == home / ".opensquilla" / "config.toml"
assert source == "home"
def test_default_path_falls_back_to_home(tmp_path, monkeypatch):
_clear_path_env(monkeypatch)
home = tmp_path / "home"
home.mkdir()
monkeypatch.setenv("HOME", str(home))
monkeypatch.chdir(tmp_path) # no opensquilla.toml in cwd
assert default_config_path() == home / ".opensquilla" / "config.toml"
def test_resolve_config_path_returns_source(tmp_path, monkeypatch):
monkeypatch.delenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", raising=False)
home = tmp_path / "home"
home.mkdir()
monkeypatch.setenv("HOME", str(home))
project = tmp_path / "project"
project.mkdir()
(project / "opensquilla.toml").write_text("")
monkeypatch.chdir(project)
path, source = resolve_config_path(None)
assert path == project / "opensquilla.toml"
assert source == "cwd"
def test_persist_creates_file_with_mode_0600(tmp_path):
cfg = GatewayConfig()
target = tmp_path / "config.toml"
result = persist_config(cfg, path=target)
assert isinstance(result, PersistResult)
assert target.exists()
mode = stat.S_IMODE(os.stat(target).st_mode)
if os.name == "nt":
assert mode & stat.S_IWRITE
else:
assert mode == 0o600
def test_persist_creates_backup_when_target_exists(tmp_path):
target = tmp_path / "config.toml"
target.write_text("port = 18791\n")
cfg = GatewayConfig()
result = persist_config(cfg, path=target)
assert result.backup_path is not None
assert result.backup_path.exists()
assert result.backup_path.name.startswith("config.toml.backup.")
def test_persist_atomic_no_leftover_tmp(tmp_path):
target = tmp_path / "config.toml"
cfg = GatewayConfig()
persist_config(cfg, path=target)
leftovers = [p for p in tmp_path.iterdir() if p.suffix == ".tmp"]
assert leftovers == []
def test_persist_validates_before_writing():
with pytest.raises(Exception):
validate_config_payload({"port": "not-a-port"})
def test_load_returns_gateway_config(tmp_path):
target = tmp_path / "config.toml"
target.write_text("port = 18792\n")
cfg = load_config(target)
assert cfg.port == 18792
def test_load_sets_config_path_for_existing_config(tmp_path):
target = tmp_path / "config.toml"
target.write_text("port = 18792\n")
cfg = load_config(target)
assert cfg.config_path == str(target)
def test_load_accepts_existing_operator_toggle_config(tmp_path):
target = tmp_path / "config.toml"
target.write_text(
"\n".join(
[
"[skills]",
"disabled = []",
"coding_mode = false",
"",
"[meta_skill]",
"auto_trigger = false",
"",
"[naming]",
"enabled = true",
'surfaces = ["webchat", "cli", "channel"]',
"max_chars = 48",
'language = "auto"',
"",
]
)
)
cfg = load_config(target)
assert cfg.skills.disabled == []
assert cfg.skills.coding_mode is False
assert cfg.meta_skill.auto_trigger is False
assert cfg.naming.enabled is True
assert cfg.naming.max_chars == 48
assert cfg.config_path == str(target)
def test_load_migrates_legacy_memory_config_before_validation(tmp_path):
target = tmp_path / "config.toml"
target.write_text(
"\n".join(
[
"[memory]",
'capture_mode = "archive_turn_pair"',
"index_captured_turns = false",
"prefetch_enabled = true",
"prefetch_max_results = 3",
"prefetch_min_score = 0.3",
"",
]
)
)
cfg = load_config(target)
assert cfg.memory.capture_mode == "turn_pair"
assert cfg.config_path == str(target)
backups = sorted(tmp_path.glob("config.toml.backup.*"))
assert backups
backup_text = backups[-1].read_text()
assert 'capture_mode = "archive_turn_pair"' in backup_text
assert "index_captured_turns = false" in backup_text
data = tomllib.loads(target.read_text())
assert data["memory"]["capture_mode"] == "turn_pair"
assert "index_captured_turns" not in data["memory"]
assert "prefetch_enabled" not in data["memory"]
assert "prefetch_max_results" not in data["memory"]
assert "prefetch_min_score" not in data["memory"]
def test_migration_then_persist_keeps_distinct_backups_on_name_collision(
tmp_path, monkeypatch
):
class FixedDateTime(dt.datetime):
@classmethod
def now(cls, tz=None):
value = cls(2026, 5, 19, 20, 40, 0)
if tz is not None:
return value.replace(tzinfo=tz)
return value
monkeypatch.setattr(migration_module.datetime, "datetime", FixedDateTime)
target = tmp_path / "config.toml"
target.write_text(
"\n".join(
[
"[memory]",
'capture_mode = "archive_turn_pair"',
"index_captured_turns = false",
"",
]
),
encoding="utf-8",
)
cfg = load_config(target)
cfg.port = 18793
persist_config(cfg, path=target, backup=True)
backups = sorted(tmp_path.glob("config.toml.backup.*"))
assert len(backups) == 2
backup_texts = [path.read_text(encoding="utf-8") for path in backups]
assert any('capture_mode = "archive_turn_pair"' in text for text in backup_texts)
assert any('capture_mode = "turn_pair"' in text for text in backup_texts)
def test_validate_config_payload_does_not_migrate_legacy_memory_payload() -> None:
with pytest.raises(Exception):
validate_config_payload({"memory": {"capture_mode": "archive_turn_pair"}})
with pytest.raises(Exception):
validate_config_payload({"memory": {"index_captured_turns": False}})
with pytest.raises(Exception):
validate_config_payload({"memory": {"prefetch_enabled": True}})
with pytest.raises(Exception):
validate_config_payload({"memory": {"cost": {"embedding_cache": True}}})
def test_persist_round_trip_preserves_unrelated(tmp_path):
target = tmp_path / "config.toml"
target.write_text(
'[llm]\nprovider = "openrouter"\n'
'model = "deepseek/deepseek-v4-flash"\n'
'base_url = "https://openrouter.ai/api/v1"\n'
)
cfg = load_config(target)
cfg.port = 18793
persist_config(cfg, path=target)
text = target.read_text()
assert "openrouter" in text
assert "18793" in text
def test_persist_omits_runtime_secret_paths(tmp_path):
cfg = GatewayConfig()
cfg.llm.api_key = "from-env"
cfg.mark_runtime_secret("llm.api_key")
target = tmp_path / "config.toml"
persist_config(cfg, path=target)
text = target.read_text()
data = tomllib.loads(text)
assert "api_key" not in data.get("llm", {})
assert "from-env" not in text
def test_env_sourced_llm_key_is_not_persisted(tmp_path, monkeypatch):
monkeypatch.setenv("OPENROUTER_API_KEY", "sk-from-env")
from opensquilla.gateway.llm_runtime import resolve_llm_runtime_config
cfg = GatewayConfig(
llm={
"provider": "openrouter",
"model": "deepseek/deepseek-v4-flash",
"api_key": "",
"api_key_env": "OPENROUTER_API_KEY",
}
)
runtime = resolve_llm_runtime_config(cfg)
assert runtime.api_key == "sk-from-env"
target = tmp_path / "config.toml"
persist_config(cfg, path=target)
data = tomllib.loads(target.read_text())
assert data["llm"]["api_key_env"] == "OPENROUTER_API_KEY"
assert "api_key" not in data["llm"]
assert "sk-from-env" not in target.read_text()
def test_load_nonexistent_returns_default(tmp_path):
cfg = load_config(tmp_path / "missing.toml")
assert isinstance(cfg, GatewayConfig)
assert cfg.port == 18791 # default
def test_persist_omits_empty_agents_table(tmp_path):
target = tmp_path / "config.toml"
persist_config(GatewayConfig(), path=target)
data = tomllib.loads(target.read_text())
assert "agents" not in data
def test_persist_round_trips_agents_list(tmp_path):
target = tmp_path / "config.toml"
cfg = GatewayConfig(agents=[AgentEntryConfig(id="ops", model="openai/test")])
persist_config(cfg, path=target)
loaded = load_config(target)
assert len(loaded.agents) == 1
assert loaded.agents[0].id == "ops"
assert loaded.agents[0].model == "openai/test"