227 lines
9.5 KiB
Python
227 lines
9.5 KiB
Python
"""Unit tests for opensquilla.contrib.codetask.agent_config (pure assembly)."""
|
|
|
|
import tomllib
|
|
|
|
import pytest
|
|
|
|
from opensquilla.contrib.codetask import agent_config as agent_config_mod
|
|
from opensquilla.contrib.codetask.agent_config import (
|
|
OPERATOR_SECTIONS,
|
|
AgentConfigError,
|
|
build_per_run_agent_config,
|
|
load_agent_config_bundle,
|
|
user_config_payload,
|
|
)
|
|
|
|
_TEMPLATE = {
|
|
"workspace_strict": False,
|
|
"sandbox": {"sandbox": False, "security_grading": False},
|
|
"tools": {"deny": ["memory*"]},
|
|
"meta_skill": {"enabled": False},
|
|
"memory": {"flush_enabled": False},
|
|
}
|
|
|
|
|
|
def test_operator_sections_replace_template_wholesale():
|
|
template = dict(_TEMPLATE, llm={"provider": "openrouter", "max_tokens": 32768})
|
|
user = {
|
|
"llm": {"provider": "deepseek", "model": "deepseek-chat"},
|
|
"models": {"deepseek": {"deepseek-chat": {"context_window": 128000}}},
|
|
}
|
|
bundle = build_per_run_agent_config(template, user)
|
|
# Replaced, not field-merged: the template's max_tokens must not leak
|
|
# under the operator's provider.
|
|
assert bundle.payload["llm"] == {"provider": "deepseek", "model": "deepseek-chat"}
|
|
assert bundle.payload["models"]["deepseek"]["deepseek-chat"]["context_window"] == 128000
|
|
# Template policy survives untouched.
|
|
assert bundle.payload["tools"]["deny"] == ["memory*"]
|
|
assert bundle.payload["meta_skill"]["enabled"] is False
|
|
|
|
|
|
def test_operator_absent_sections_are_dropped_from_template():
|
|
template = dict(_TEMPLATE, llm={"provider": "openrouter"}, squilla_router={"enabled": True})
|
|
bundle = build_per_run_agent_config(template, {})
|
|
for section in OPERATOR_SECTIONS:
|
|
assert section not in bundle.payload, section
|
|
|
|
|
|
def test_primary_api_key_moves_to_child_env():
|
|
user = {"llm": {"provider": "deepseek", "model": "deepseek-chat", "api_key": "sk-secret"}}
|
|
bundle = build_per_run_agent_config(dict(_TEMPLATE), user)
|
|
assert "api_key" not in bundle.payload["llm"]
|
|
assert bundle.child_env == {"OPENSQUILLA_LLM_API_KEY": "sk-secret"}
|
|
# The source payload is not mutated.
|
|
assert user["llm"]["api_key"] == "sk-secret"
|
|
|
|
|
|
def test_empty_api_key_is_not_stripped():
|
|
# An explicitly empty api_key must stay in the written [llm] (the operator
|
|
# resolves via api_key_env); dropping it would let a stale inherited
|
|
# OPENSQUILLA_LLM_API_KEY fill the field in the child.
|
|
user = {"llm": {"provider": "deepseek", "model": "m", "api_key": "", "api_key_env": "DS_KEY"}}
|
|
bundle = build_per_run_agent_config(dict(_TEMPLATE), user)
|
|
assert bundle.payload["llm"]["api_key"] == ""
|
|
assert bundle.child_env == {}
|
|
|
|
|
|
def test_profile_api_keys_stay_in_payload():
|
|
# Profile keys have no env transport channel; they ride in the 0600 file.
|
|
user = {
|
|
"llm": {"provider": "deepseek", "model": "deepseek-chat"},
|
|
"llm_profiles": {"moonshot": {"api_key": "sk-profile"}},
|
|
}
|
|
bundle = build_per_run_agent_config(dict(_TEMPLATE), user)
|
|
assert bundle.payload["llm_profiles"]["moonshot"]["api_key"] == "sk-profile"
|
|
assert bundle.child_env == {}
|
|
|
|
|
|
def test_llm_ensemble_is_never_carried():
|
|
user = {
|
|
"llm": {"provider": "deepseek", "model": "deepseek-chat"},
|
|
"llm_ensemble": {"enabled": True},
|
|
}
|
|
bundle = build_per_run_agent_config(dict(_TEMPLATE), user)
|
|
assert "llm_ensemble" not in bundle.payload
|
|
|
|
|
|
def test_mismatched_tier_profile_heals_in_merged_config():
|
|
# A tier_profile that no longer matches llm.provider used to hard-fail
|
|
# validation; the config-migration layer now clears it (the same healing
|
|
# gateway boot applies to hand-edited configs), so the merged per-run
|
|
# config builds cleanly with the profile pointer dropped instead of
|
|
# failing at subagent boot.
|
|
user = {
|
|
"llm": {"provider": "deepseek", "model": "deepseek-chat"},
|
|
"squilla_router": {"enabled": True, "tier_profile": "moonshot"},
|
|
}
|
|
bundle = build_per_run_agent_config(
|
|
dict(_TEMPLATE), user, user_config_path="/x/config.toml"
|
|
)
|
|
assert "tier_profile" not in bundle.payload.get("squilla_router", {})
|
|
|
|
|
|
def test_user_config_payload_explicit_env_path(monkeypatch, tmp_path):
|
|
cfg = tmp_path / "operator.toml"
|
|
cfg.write_text('[llm]\nprovider = "deepseek"\nmodel = "m"\n', encoding="utf-8")
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(cfg))
|
|
payload, source = user_config_payload()
|
|
assert payload["llm"]["provider"] == "deepseek"
|
|
assert source == str(cfg)
|
|
|
|
|
|
def test_user_config_payload_explicit_env_path_missing_is_sole_candidate(
|
|
monkeypatch, tmp_path
|
|
):
|
|
# Mirrors GatewayConfig.load: an explicit path never falls back to the
|
|
# cwd/home chain.
|
|
(tmp_path / "state").mkdir()
|
|
(tmp_path / "state" / "config.toml").write_text('[llm]\nprovider = "moonshot"\n', "utf-8")
|
|
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(tmp_path / "state"))
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(tmp_path / "missing.toml"))
|
|
payload, source = user_config_payload()
|
|
assert payload == {}
|
|
assert source is None
|
|
|
|
|
|
def test_user_config_payload_falls_back_to_home_config(monkeypatch, tmp_path):
|
|
monkeypatch.delenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", raising=False)
|
|
monkeypatch.chdir(tmp_path) # no ./opensquilla.toml here
|
|
home = tmp_path / "state"
|
|
home.mkdir()
|
|
(home / "config.toml").write_text('[llm]\nprovider = "deepseek"\nmodel = "m"\n', "utf-8")
|
|
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(home))
|
|
payload, source = user_config_payload()
|
|
assert payload["llm"]["provider"] == "deepseek"
|
|
assert source == str(home / "config.toml")
|
|
|
|
|
|
def test_user_config_payload_cwd_toml_wins_over_home(monkeypatch, tmp_path):
|
|
monkeypatch.delenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", raising=False)
|
|
monkeypatch.chdir(tmp_path)
|
|
(tmp_path / "opensquilla.toml").write_text(
|
|
'[llm]\nprovider = "deepseek"\nmodel = "m"\n', "utf-8"
|
|
)
|
|
home = tmp_path / "state"
|
|
home.mkdir()
|
|
(home / "config.toml").write_text('[llm]\nprovider = "moonshot"\nmodel = "k"\n', "utf-8")
|
|
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(home))
|
|
payload, _source = user_config_payload()
|
|
assert payload["llm"]["provider"] == "deepseek"
|
|
|
|
|
|
def test_unparseable_operator_config_raises_actionably(monkeypatch, tmp_path):
|
|
cfg = tmp_path / "broken.toml"
|
|
cfg.write_text("[llm\nprovider=", encoding="utf-8")
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(cfg))
|
|
with pytest.raises(AgentConfigError) as exc:
|
|
user_config_payload()
|
|
assert str(cfg) in str(exc.value)
|
|
|
|
|
|
def test_override_env_skips_inheritance(monkeypatch, tmp_path):
|
|
override = tmp_path / "override.toml"
|
|
override.write_text(
|
|
'[llm]\nprovider = "deepseek"\nmodel = "deepseek-chat"\napi_key = "sk-keep"\n',
|
|
encoding="utf-8",
|
|
)
|
|
operator = tmp_path / "operator.toml"
|
|
operator.write_text('[llm]\nprovider = "moonshot"\nmodel = "kimi-k2.6"\n', "utf-8")
|
|
monkeypatch.setenv("OPENSQUILLA_CODETASK_AGENT_CONFIG", str(override))
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(operator))
|
|
bundle = load_agent_config_bundle()
|
|
assert bundle.payload["llm"]["provider"] == "deepseek"
|
|
assert bundle.payload["llm"]["api_key"] == "sk-keep" # full authority, untouched
|
|
assert bundle.child_env == {}
|
|
|
|
|
|
def test_blank_override_env_is_treated_as_unset(monkeypatch, tmp_path):
|
|
# A quoted-empty OPENSQUILLA_CODETASK_AGENT_CONFIG must not be taken as a
|
|
# path (which would hard-fail every run reading a whitespace filename);
|
|
# inheritance proceeds normally.
|
|
operator = tmp_path / "operator.toml"
|
|
operator.write_text('[llm]\nprovider = "deepseek"\nmodel = "m"\n', encoding="utf-8")
|
|
monkeypatch.setenv("OPENSQUILLA_CODETASK_AGENT_CONFIG", " ")
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(operator))
|
|
bundle = load_agent_config_bundle()
|
|
assert bundle.payload["llm"]["provider"] == "deepseek"
|
|
|
|
|
|
def test_bundled_template_pins_no_provider_sections():
|
|
# The shipped template is a run-policy file; pinning [llm]/[squilla_router]
|
|
# there would shadow the operator's provider (issue #541 regression guard).
|
|
from opensquilla.contrib.codetask.config import _DATA_DIR
|
|
|
|
template = tomllib.loads(
|
|
(_DATA_DIR / "agent_config" / "config.toml").read_text(encoding="utf-8")
|
|
)
|
|
for section in OPERATOR_SECTIONS:
|
|
assert section not in template, section
|
|
for section in ("tools", "sandbox", "meta_skill", "memory"):
|
|
assert section in template, section
|
|
|
|
|
|
def test_merged_payload_round_trips_through_gateway_config(monkeypatch, tmp_path):
|
|
"""End to end: the assembled payload for a deepseek operator loads as a
|
|
deepseek config with an auto-aligned router tier profile."""
|
|
operator = tmp_path / "operator.toml"
|
|
operator.write_text(
|
|
'[llm]\nprovider = "deepseek"\nmodel = "deepseek-chat"\n', encoding="utf-8"
|
|
)
|
|
monkeypatch.setenv("OPENSQUILLA_GATEWAY_CONFIG_PATH", str(operator))
|
|
monkeypatch.delenv("OPENSQUILLA_CODETASK_AGENT_CONFIG", raising=False)
|
|
bundle = load_agent_config_bundle()
|
|
|
|
from opensquilla.gateway.config import GatewayConfig
|
|
|
|
conf = GatewayConfig(**bundle.payload)
|
|
assert conf.llm.provider == "deepseek"
|
|
assert conf.llm.model == "deepseek-chat"
|
|
assert conf.squilla_router.tier_profile in ("deepseek", None)
|
|
|
|
|
|
def test_agent_config_path_still_honors_override(monkeypatch, tmp_path):
|
|
# Regression guard for the existing escape-hatch contract.
|
|
custom = tmp_path / "custom.toml"
|
|
monkeypatch.setenv("OPENSQUILLA_CODETASK_AGENT_CONFIG", str(custom))
|
|
assert agent_config_mod.agent_config_path() == custom
|