215 lines
7.6 KiB
Python
215 lines
7.6 KiB
Python
"""Golden legacy-config fixtures: every released era loads on current code.
|
|
|
|
The fixture homes under ``fixtures/homes/`` freeze what each released line
|
|
actually wrote to disk (generated from each tag's own ``GatewayConfig``
|
|
full dump, with synthetic paths), plus adversarial variants for the known
|
|
compatibility gaps. The matrix test pins the era set, so shipping a new
|
|
release line means extending the fixtures — old-config upgrade behavior is
|
|
tested mechanically, not just fresh installs.
|
|
|
|
The fixtures and matrix below are the executable compatibility contract.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import shutil
|
|
import tomllib
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
import opensquilla.gateway.config as config_module
|
|
from opensquilla.gateway.config import GatewayConfig
|
|
from opensquilla.gateway.config_migration import migrate_config_payload
|
|
from opensquilla.migration.opensquilla_home import (
|
|
OpenSquillaHomeMigrator,
|
|
OpenSquillaMigrationOptions,
|
|
)
|
|
|
|
FIXTURES_ROOT = Path(__file__).parent / "fixtures" / "homes"
|
|
|
|
# One era per released line and install form. A new minor release (or a new
|
|
# install form) must add its era here AND as a fixture home.
|
|
RELEASED_ERAS = frozenset(
|
|
{
|
|
"cli-0.1",
|
|
"cli-0.2",
|
|
"cli-0.3",
|
|
"cli-0.4",
|
|
"cli-0.5",
|
|
"portable-0.4",
|
|
"desktop-0.4",
|
|
"desktop-0.5rc",
|
|
}
|
|
)
|
|
|
|
ERA_DIRS = sorted(
|
|
p.name for p in FIXTURES_ROOT.iterdir() if p.is_dir() and p.name != "adversarial"
|
|
)
|
|
ADVERSARIAL_FILES = sorted((FIXTURES_ROOT / "adversarial").glob("*.toml"))
|
|
|
|
|
|
def _load_fixture(path: Path) -> GatewayConfig:
|
|
data = tomllib.loads(path.read_text(encoding="utf-8"))
|
|
result = migrate_config_payload(data)
|
|
return GatewayConfig(**result.payload)
|
|
|
|
|
|
def test_fixture_matrix_covers_every_released_era() -> None:
|
|
assert set(ERA_DIRS) == set(RELEASED_ERAS), (
|
|
"Fixture homes and RELEASED_ERAS diverged. A new release line must "
|
|
"ship a golden config fixture (see the legacy-home-migration design)."
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("era", ERA_DIRS)
|
|
def test_every_released_era_config_loads_on_current_code(era: str) -> None:
|
|
config_path = FIXTURES_ROOT / era / "config.toml"
|
|
cfg = _load_fixture(config_path)
|
|
assert cfg is not None
|
|
|
|
|
|
@pytest.mark.parametrize("era", ERA_DIRS)
|
|
def test_every_released_era_survives_cross_install_import(
|
|
era: str, tmp_path: Path
|
|
) -> None:
|
|
source = tmp_path / f"source-{era}"
|
|
source.mkdir()
|
|
shutil.copy2(FIXTURES_ROOT / era / "config.toml", source / "config.toml")
|
|
(source / "state").mkdir()
|
|
(source / "workspace").mkdir()
|
|
(source / "workspace" / "era.txt").write_text(era, encoding="utf-8")
|
|
target = tmp_path / f"target-{era}"
|
|
|
|
report = OpenSquillaHomeMigrator(
|
|
OpenSquillaMigrationOptions(source=source, target=target, apply=True)
|
|
).migrate()
|
|
|
|
errors = [item for item in report["items"] if item["status"] == "error"]
|
|
assert not errors, errors
|
|
assert (target / "workspace" / "era.txt").read_text(encoding="utf-8") == era
|
|
imported = tomllib.loads((target / "config.toml").read_text(encoding="utf-8"))
|
|
assert GatewayConfig(**imported) is not None
|
|
|
|
|
|
def test_cli_01_era_migrates_with_expected_strips() -> None:
|
|
data = tomllib.loads(
|
|
(FIXTURES_ROOT / "cli-0.1" / "config.toml").read_text(encoding="utf-8")
|
|
)
|
|
result = migrate_config_payload(data)
|
|
assert result.changed
|
|
removed = set(result.removed_fields)
|
|
assert "memory.index_captured_turns" in removed
|
|
assert any(f.startswith("agent_token_saving.") for f in removed)
|
|
# The 0.1 line pinned the pre-0.2 gateway port; loading must preserve it
|
|
# (relocating it to the current default is the data migrator's transform,
|
|
# not the config loader's).
|
|
cfg = GatewayConfig(**result.payload)
|
|
assert cfg.port == 18790
|
|
|
|
|
|
def test_modern_era_configs_load_without_changes() -> None:
|
|
for era in ("cli-0.3", "cli-0.4", "cli-0.5"):
|
|
data = tomllib.loads(
|
|
(FIXTURES_ROOT / era / "config.toml").read_text(encoding="utf-8")
|
|
)
|
|
result = migrate_config_payload(data)
|
|
assert not result.changed, (era, result.changes, result.removed_fields)
|
|
|
|
|
|
def test_desktop_eras_canonicalize_legacy_tier_keys() -> None:
|
|
for era in ("desktop-0.4", "desktop-0.5rc"):
|
|
cfg = _load_fixture(FIXTURES_ROOT / era / "config.toml")
|
|
tiers = cfg.squilla_router.tiers or {}
|
|
# Pre-rc2 desktops persisted t0-t3 tier keys; validation canonicalizes
|
|
# them to c0-c3 (profile expansion may add non-text tiers like
|
|
# image_model — only the legacy t-spellings must be gone).
|
|
assert not set(tiers) & {"t0", "t1", "t2", "t3"}, (era, sorted(tiers))
|
|
assert {"c0", "c1", "c2", "c3"} <= set(tiers), (era, sorted(tiers))
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"fixture", ADVERSARIAL_FILES, ids=[p.stem for p in ADVERSARIAL_FILES]
|
|
)
|
|
def test_adversarial_gap_fixture_loads(fixture: Path) -> None:
|
|
cfg = _load_fixture(fixture)
|
|
assert cfg is not None
|
|
|
|
|
|
def test_dream_model_override_is_stripped() -> None:
|
|
data = tomllib.loads(
|
|
(FIXTURES_ROOT / "adversarial" / "dream-model-override.toml").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
)
|
|
result = migrate_config_payload(data)
|
|
assert "memory.dream.model_override" in result.removed_fields
|
|
assert "model_override" not in result.payload.get("memory", {}).get("dream", {})
|
|
|
|
|
|
def test_unregistered_channel_entry_is_parked_not_fatal() -> None:
|
|
data = tomllib.loads(
|
|
(FIXTURES_ROOT / "adversarial" / "msteams-channel.toml").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
)
|
|
result = migrate_config_payload(data)
|
|
assert any("msteams" in field for field in result.removed_fields)
|
|
cfg = GatewayConfig(**result.payload)
|
|
kept = [entry.type for entry in cfg.channels.channels]
|
|
assert kept == ["telegram"]
|
|
|
|
|
|
def test_mismatched_tier_profile_is_cleared_not_fatal() -> None:
|
|
data = tomllib.loads(
|
|
(FIXTURES_ROOT / "adversarial" / "tier-profile-mismatch.toml").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
)
|
|
result = migrate_config_payload(data)
|
|
assert any("tier_profile" in change for change in result.changes)
|
|
cfg = GatewayConfig(**result.payload)
|
|
assert cfg.squilla_router.tier_profile is None
|
|
|
|
|
|
def test_matching_tier_profile_is_untouched() -> None:
|
|
result = migrate_config_payload(
|
|
{
|
|
"llm": {"provider": "openrouter", "model": "dummy/model"},
|
|
"squilla_router": {"tier_profile": "openrouter"},
|
|
}
|
|
)
|
|
assert not result.changed
|
|
|
|
|
|
def test_out_of_range_search_max_results_is_clamped() -> None:
|
|
data = tomllib.loads(
|
|
(FIXTURES_ROOT / "adversarial" / "search-max-results-over.toml").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
)
|
|
result = migrate_config_payload(data)
|
|
cfg = GatewayConfig(**result.payload)
|
|
assert cfg.search_max_results <= 20
|
|
|
|
|
|
def test_readonly_config_location_degrades_to_warning(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""A migrated-but-unwritable config must boot from memory, not fail."""
|
|
import tomli_w
|
|
|
|
config_path = tmp_path / "config.toml"
|
|
with config_path.open("wb") as fh:
|
|
tomli_w.dump({"memory": {"dream": {"model_override": "dummy"}}}, fh)
|
|
|
|
def _raise(*args: object, **kwargs: object) -> None:
|
|
raise PermissionError("read-only filesystem")
|
|
|
|
monkeypatch.setattr(config_module, "backup_and_write_migrated_config", _raise)
|
|
cfg = GatewayConfig.load_from_toml(config_path)
|
|
assert cfg is not None
|
|
# The original file is untouched (no partial rewrite).
|
|
original = tomllib.loads(config_path.read_text(encoding="utf-8"))
|
|
assert original["memory"]["dream"]["model_override"] == "dummy"
|