mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
155 lines
5.9 KiB
Python
155 lines
5.9 KiB
Python
"""Upgrade/migration robustness for settings.json: a user who upgrades from an
|
|
older app version (legacy field names, removed fields, a field whose type drifted,
|
|
a retired Literal value, or an outright corrupt file) must still boot. load_settings
|
|
is called at startup, by GET /api/settings, and on every agent dispatch, so a raise
|
|
here bricks the whole app. These tests pin both the migration mapping and the
|
|
never-raise contract, and assert install-id / first_opened_at continuity."""
|
|
import json
|
|
import os
|
|
|
|
import pytest
|
|
|
|
from backend.apps.settings import store
|
|
from backend.apps.settings.models import AppSettings, DEFAULT_SYSTEM_PROMPT
|
|
|
|
|
|
@pytest.fixture
|
|
def settings_file(tmp_path, monkeypatch):
|
|
"""Point the store at an isolated settings.json under tmp_path."""
|
|
f = str(tmp_path / "settings.json")
|
|
monkeypatch.setattr(store, "DATA_DIR", str(tmp_path))
|
|
monkeypatch.setattr(store, "SETTINGS_FILE", f)
|
|
return f
|
|
|
|
|
|
def p_write(path, obj):
|
|
with open(path, "w", encoding="utf-8") as fh:
|
|
json.dump(obj, fh)
|
|
|
|
|
|
# ---------------- migrate_legacy_fields ----------------
|
|
|
|
def test_migrate_managed_to_openswarm_pro():
|
|
assert store.migrate_legacy_fields({"connection_mode": "managed"})["connection_mode"] == "openswarm-pro"
|
|
|
|
|
|
def test_migrate_auth_token_renamed_and_popped():
|
|
out = store.migrate_legacy_fields({"openswarm_auth_token": "tok"})
|
|
assert out["openswarm_bearer_token"] == "tok"
|
|
assert "openswarm_auth_token" not in out
|
|
|
|
|
|
def test_migrate_does_not_clobber_existing_bearer():
|
|
out = store.migrate_legacy_fields({"openswarm_auth_token": "old", "openswarm_bearer_token": "new"})
|
|
assert out["openswarm_bearer_token"] == "new"
|
|
|
|
|
|
def test_migrate_leaves_modern_values_untouched():
|
|
out = store.migrate_legacy_fields({"connection_mode": "own_key"})
|
|
assert out["connection_mode"] == "own_key"
|
|
|
|
|
|
# ---------------- load_settings: happy paths ----------------
|
|
|
|
def test_no_file_returns_defaults(settings_file):
|
|
s = store.load_settings()
|
|
assert isinstance(s, AppSettings)
|
|
assert s.default_system_prompt == DEFAULT_SYSTEM_PROMPT
|
|
assert s.theme == "light"
|
|
|
|
|
|
def test_minimal_old_file_fills_missing_with_defaults(settings_file):
|
|
# An old build wrote only a couple of fields; everything else must default.
|
|
p_write(settings_file, {"theme": "light"})
|
|
s = store.load_settings()
|
|
assert s.theme == "light"
|
|
assert s.default_model == "opus-5" # filled from default
|
|
assert s.auto_reveal_sub_agents is True
|
|
|
|
|
|
def test_legacy_fields_migrated_end_to_end(settings_file):
|
|
p_write(settings_file, {"connection_mode": "managed", "openswarm_auth_token": "tok"})
|
|
s = store.load_settings()
|
|
assert s.connection_mode == "openswarm-pro"
|
|
assert s.openswarm_bearer_token == "tok"
|
|
|
|
|
|
def test_install_id_and_first_opened_continuity(settings_file):
|
|
# The identity carried across upgrades must survive a load untouched.
|
|
p_write(settings_file, {"installation_id": "abc-123", "first_opened_at": "2025-01-01T00:00:00Z"})
|
|
s = store.load_settings()
|
|
assert s.installation_id == "abc-123"
|
|
assert s.first_opened_at == "2025-01-01T00:00:00Z"
|
|
|
|
|
|
def test_null_system_prompt_backfilled(settings_file):
|
|
p_write(settings_file, {"default_system_prompt": None})
|
|
assert store.load_settings().default_system_prompt == DEFAULT_SYSTEM_PROMPT
|
|
|
|
|
|
# ---------------- load_settings: forward/backward-compat robustness ----------------
|
|
|
|
def test_unknown_removed_fields_are_ignored(settings_file):
|
|
# A field that existed in a future/older schema but not this one must not crash.
|
|
p_write(settings_file, {"theme": "light", "a_field_we_removed": 999, "another_ghost": {"x": 1}})
|
|
s = store.load_settings()
|
|
assert s.theme == "light"
|
|
|
|
|
|
def test_type_drifted_field_reverts_to_default_keeps_rest(settings_file):
|
|
# dismissed_mcp_suggestions is dict[str,str] now; an old build stored a list. The bad field must revert to its default, every valid field must survive.
|
|
p_write(settings_file, {"theme": "light", "dismissed_mcp_suggestions": ["legacy", "list"]})
|
|
s = store.load_settings()
|
|
assert s.theme == "light"
|
|
assert s.dismissed_mcp_suggestions == {}
|
|
|
|
|
|
def test_retired_literal_value_reverts_to_default(settings_file):
|
|
# default_thinking_level is a Literal; a retired value must not brick load.
|
|
p_write(settings_file, {"theme": "light", "default_thinking_level": "ultra"})
|
|
s = store.load_settings()
|
|
assert s.theme == "light"
|
|
assert s.default_thinking_level == "auto"
|
|
|
|
|
|
def test_multiple_bad_fields_all_revert_valid_survive(settings_file):
|
|
p_write(settings_file, {
|
|
"theme": "light",
|
|
"default_thinking_level": "ultra", # retired literal
|
|
"dismissed_mcp_suggestions": [1, 2, 3], # wrong type
|
|
"zoom_sensitivity": "not-a-number", # wrong type
|
|
})
|
|
s = store.load_settings()
|
|
assert s.theme == "light"
|
|
assert s.default_thinking_level == "auto"
|
|
assert s.dismissed_mcp_suggestions == {}
|
|
assert s.zoom_sensitivity == 50.0
|
|
|
|
|
|
def test_corrupt_json_returns_defaults_and_preserves_file(settings_file):
|
|
with open(settings_file, "w", encoding="utf-8") as fh:
|
|
fh.write("{ this is : not json ,,, ")
|
|
s = store.load_settings()
|
|
assert s.theme == "light" # defaults
|
|
# Original is moved aside (recoverable), not silently destroyed.
|
|
assert os.path.exists(settings_file + ".corrupt")
|
|
assert not os.path.exists(settings_file)
|
|
|
|
|
|
def test_non_dict_top_level_returns_defaults(settings_file):
|
|
p_write(settings_file, ["not", "an", "object"])
|
|
s = store.load_settings()
|
|
assert s.theme == "light"
|
|
assert os.path.exists(settings_file + ".corrupt")
|
|
|
|
|
|
# ---------------- round-trip ----------------
|
|
|
|
def test_save_then_load_roundtrip(settings_file):
|
|
s = AppSettings(theme="light", default_model="opus", installation_id="keep-me")
|
|
store.save_settings(s)
|
|
loaded = store.load_settings()
|
|
assert loaded.theme == "light"
|
|
assert loaded.default_model == "opus"
|
|
assert loaded.installation_id == "keep-me"
|