diff --git a/src/schemas.py b/src/schemas.py index 69ecb5ad..a476ee92 100644 --- a/src/schemas.py +++ b/src/schemas.py @@ -1,7 +1,7 @@ import datetime import ipaddress from enum import Enum -from typing import Annotated, Any, Self +from typing import Annotated, Any, Self, cast from urllib.parse import urlparse import tiktoken @@ -173,6 +173,20 @@ class ResolvedConfiguration(BaseModel): summary: ResolvedSummaryConfiguration dream: ResolvedDreamConfiguration + @model_validator(mode="before") + @classmethod + def migrate_deriver_to_reasoning(cls, data: Any) -> Any: + """Handle v3.0.0 migration: 'deriver' was renamed to 'reasoning'.""" + if not isinstance(data, dict): + return data + + config = cast(dict[str, Any], data) + + if "deriver" in config and "reasoning" not in config: + config["reasoning"] = config.pop("deriver") + + return config + class PeerConfig(BaseModel): # TODO: Update description - should say "Whether honcho forms a representation of the peer itself" diff --git a/tests/test_schema_validations.py b/tests/test_schema_validations.py index 345fde23..281522a9 100644 --- a/tests/test_schema_validations.py +++ b/tests/test_schema_validations.py @@ -1,3 +1,5 @@ +from typing import Any + import pytest from pydantic import ValidationError @@ -6,6 +8,7 @@ from src.schemas import ( DocumentMetadata, MessageCreate, PeerCreate, + ResolvedConfiguration, SessionCreate, WorkspaceCreate, ) @@ -140,3 +143,63 @@ class TestDocumentValidations: ) error_dict = exc_info.value.errors()[0] assert error_dict["type"] == "string_too_long" + + +class TestResolvedConfigurationMigration: + """Test backward compatibility for queue items created before v3.0.0. + + In v3.0.0, the 'deriver' field was renamed to 'reasoning'. Old queue items + may still have the 'deriver' field and need to be migrated at validation time. + """ + + def _make_config(self, **overrides: dict[str, Any]): + """Helper to create a valid config dict with overrides.""" + base = { + "reasoning": {"enabled": True}, + "peer_card": {"use": True, "create": True}, + "summary": { + "enabled": True, + "messages_per_short_summary": 20, + "messages_per_long_summary": 60, + }, + "dream": {"enabled": False}, + } + base.update(overrides) + return base + + def test_old_queue_item_with_deriver_field(self): + """Old queue items with 'deriver' should be migrated to 'reasoning'.""" + old_payload = self._make_config() + del old_payload["reasoning"] + old_payload["deriver"] = {"enabled": True} + + config = ResolvedConfiguration.model_validate(old_payload) + + assert config.reasoning.enabled is True + + def test_new_queue_item_with_reasoning_field(self): + """New queue items with 'reasoning' should work normally.""" + new_payload = self._make_config(reasoning={"enabled": False}) + + config = ResolvedConfiguration.model_validate(new_payload) + + assert config.reasoning.enabled is False + + def test_migration_does_not_override_reasoning(self): + """If both 'deriver' and 'reasoning' exist, 'reasoning' takes precedence.""" + payload = self._make_config(reasoning={"enabled": False}) + payload["deriver"] = {"enabled": True} + + config = ResolvedConfiguration.model_validate(payload) + + assert config.reasoning.enabled is False + + def test_missing_reasoning_and_deriver_fails(self): + """Payload missing both 'reasoning' and 'deriver' should fail validation.""" + payload = self._make_config() + del payload["reasoning"] + + with pytest.raises(ValidationError) as exc_info: + ResolvedConfiguration.model_validate(payload) + + assert any(e["loc"] == ("reasoning",) for e in exc_info.value.errors())