"""Regression coverage for finite repeat counts in the dashboard cron create API (#68012).""" import pytest @pytest.fixture() def isolated_profiles(tmp_path, monkeypatch, _isolate_hermes_home): """Route dashboard cron storage to an isolated default profile.""" from hermes_constants import get_hermes_home from hermes_cli import profiles default_home = get_hermes_home() (default_home / "cron").mkdir(parents=True, exist_ok=True) (default_home / "config.yaml").write_text( "model: test-model\n", encoding="utf-8", ) monkeypatch.setattr(profiles, "_get_default_hermes_home", lambda: default_home) monkeypatch.setattr(profiles, "_get_profiles_root", lambda: default_home / "profiles") return default_home @pytest.fixture() def client(monkeypatch, isolated_profiles): try: from starlette.testclient import TestClient except ImportError: pytest.skip("fastapi/starlette not installed") import hermes_state from hermes_constants import get_hermes_home from hermes_cli.web_server import app, _SESSION_HEADER_NAME, _SESSION_TOKEN monkeypatch.setattr( hermes_state, "DEFAULT_DB_PATH", get_hermes_home() / "state.db", ) test_client = TestClient(app) test_client.headers[_SESSION_HEADER_NAME] = _SESSION_TOKEN return test_client def test_create_preserves_finite_repeat_count(client): """A finite repeat must survive request parsing and storage (route-level persistence).""" response = client.post( "/api/cron/jobs", json={ "prompt": "run the check", "schedule": "every 1h", "name": "two checks", "repeat": 2, }, ) assert response.status_code == 200 assert response.json()["repeat"] == {"times": 2, "completed": 0} listed = client.get("/api/cron/jobs", params={"profile": "default"}).json() assert [job["repeat"] for job in listed if job["name"] == "two checks"] == [ {"times": 2, "completed": 0} ] def test_create_without_repeat_remains_unlimited(client): response = client.post( "/api/cron/jobs", json={ "prompt": "run forever", "schedule": "every 1h", }, ) assert response.status_code == 200 assert response.json()["repeat"] == {"times": None, "completed": 0} @pytest.mark.parametrize("repeat", [0, -1, 1.5, "not-a-count"]) def test_create_rejects_invalid_repeat_count(client, repeat): """Invalid counts must 4xx instead of silently becoming unlimited jobs.""" response = client.post( "/api/cron/jobs", json={ "prompt": "must not run forever", "schedule": "every 1h", "repeat": repeat, }, ) assert response.status_code in (400, 422) detail = response.json()["detail"] if isinstance(detail, str): assert "repeat" in detail.lower() else: assert any( error.get("loc", [None])[-1] == "repeat" or "repeat" in str(error).lower() for error in detail ) assert client.get( "/api/cron/jobs", params={"profile": "default"}, ).json() == [] @pytest.mark.parametrize("repeat,expected_times", [("2", 2), ("once", 1), ("forever", None)]) def test_create_accepts_cli_string_repeat_forms(client, repeat, expected_times): """The dashboard create path validates repeat through the same normalize_repeat_value the CLI uses: numeric strings coerce, 'once' → 1, 'forever' → unlimited.""" response = client.post( "/api/cron/jobs", json={ "prompt": "string repeat form", "schedule": "every 1h", "repeat": repeat, }, ) assert response.status_code == 200 assert response.json()["repeat"] == {"times": expected_times, "completed": 0}