diff --git a/nanobot/config/schema.py b/nanobot/config/schema.py index 06c64ce9..ac8022db 100644 --- a/nanobot/config/schema.py +++ b/nanobot/config/schema.py @@ -56,7 +56,10 @@ class DreamConfig(Base): enabled: bool = True # Register the periodic Dream consolidation job on startup interval_h: int = Field(default=2, ge=1) # Every 2 hours by default - cron: str | None = Field(default=None, exclude=True) # Legacy cron expression override + cron: str | None = Field( + default=None, + exclude_if=lambda value: value is None, + ) # Legacy cron expression override model_override: str | None = Field( default=None, validation_alias=AliasChoices("modelOverride", "model", "model_override"), diff --git a/tests/config/test_dream_config.py b/tests/config/test_dream_config.py index 9266792b..feff587a 100644 --- a/tests/config/test_dream_config.py +++ b/tests/config/test_dream_config.py @@ -29,12 +29,18 @@ def test_dream_config_honors_legacy_cron_override() -> None: assert cfg.describe_schedule() == "cron 0 */4 * * * (legacy)" -def test_dream_config_dump_uses_interval_h_and_hides_legacy_cron() -> None: +def test_dream_config_dump_preserves_legacy_cron_override() -> None: cfg = DreamConfig.model_validate({"intervalH": 5, "cron": "0 */4 * * *"}) dumped = cfg.model_dump(by_alias=True) assert dumped["intervalH"] == 5 + assert dumped["cron"] == "0 */4 * * *" + + +def test_dream_config_dump_omits_empty_legacy_cron() -> None: + dumped = DreamConfig().model_dump(by_alias=True) + assert "cron" not in dumped diff --git a/tests/config/test_env_interpolation.py b/tests/config/test_env_interpolation.py index a0e47d20..dad44382 100644 --- a/tests/config/test_env_interpolation.py +++ b/tests/config/test_env_interpolation.py @@ -81,6 +81,52 @@ class TestResolveConfig: saved = json.loads(config_path.read_text(encoding="utf-8")) assert saved["channels"]["telegram"]["token"] == "${MY_TOKEN}" + def test_save_preserves_dream_legacy_cron(self, tmp_path): + config_path = tmp_path / "config.json" + config_path.write_text( + json.dumps( + {"agents": {"defaults": {"dream": {"cron": "0 */4 * * *"}}}} + ), + encoding="utf-8", + ) + + config = load_config(config_path) + config.agents.defaults.max_tokens = 1234 + save_config(config, config_path) + + saved = json.loads(config_path.read_text(encoding="utf-8")) + assert saved["agents"]["defaults"]["dream"]["cron"] == "0 */4 * * *" + + reloaded = load_config(config_path) + schedule = reloaded.agents.defaults.dream.build_schedule("UTC") + assert schedule.kind == "cron" + assert schedule.expr == "0 */4 * * *" + + def test_save_keeps_oauth_provider_configs_excluded(self, tmp_path): + config_path = tmp_path / "config.json" + config_path.write_text( + json.dumps( + { + "agents": {"defaults": {"dream": {"cron": "0 */4 * * *"}}}, + "providers": { + "openaiCodex": {"apiKey": "codex-secret"}, + "githubCopilot": {"apiKey": "copilot-secret"}, + "groq": {"apiKey": "groq-secret"}, + }, + } + ), + encoding="utf-8", + ) + + config = load_config(config_path) + save_config(config, config_path) + + saved = json.loads(config_path.read_text(encoding="utf-8")) + assert saved["agents"]["defaults"]["dream"]["cron"] == "0 */4 * * *" + assert "openaiCodex" not in saved["providers"] + assert "githubCopilot" not in saved["providers"] + assert saved["providers"]["groq"]["apiKey"] == "groq-secret" + def test_preserves_excluded_fields_when_no_env_refs(self, tmp_path): """Regression: fields with ``exclude=True`` (e.g. ProviderConfig.openai_codex) must survive ``resolve_config_env_vars`` when the config has no