"""Integration: Run now on a schedule runs its playbook and leaves the schedule where it was (operator, 2026-10-09): its time of day stays, and the run it started is the one the list shows.""" from __future__ import annotations import asyncio import os import time import uuid from datetime import datetime, timedelta, timezone from types import SimpleNamespace import pytest pytestmark = pytest.mark.integration CSRF = {"X-CSRF-Token": "test-token"} @pytest.fixture def app(): if not os.environ.get("STEWARD_DATABASE_URL"): pytest.skip("needs Postgres") from steward.app import create_app return create_app(testing=False) def test_run_now_leaves_the_schedule_s_time_alone(app, monkeypatch): from steward.ansible import runner from steward.models.ansible_schedule import AnsibleSchedule from steward.models.users import User, UserRole started: list[dict] = [] run_id = str(uuid.uuid4()) async def fake_trigger(_app, **kw): started.append(kw) return SimpleNamespace(id=run_id), None, None monkeypatch.setattr(runner, "trigger_run", fake_trigger) async def _go(): tag = uuid.uuid4().hex[:8] uid, sid = str(uuid.uuid4()), str(uuid.uuid4()) slot = datetime.now(timezone.utc).replace(microsecond=0) - timedelta(hours=3) async with app.db_sessionmaker() as s: async with s.begin(): s.add(User(id=uid, username=f"rn-{tag}", email=f"rn-{tag}@example.com", password_hash="x", role=UserRole.operator, is_active=True)) s.add(AnsibleSchedule(id=sid, name=f"nightly-{tag}", source_name="steward-builtin", playbook_path="maintenance/docker_prune.yml", interval_seconds=86400, enabled=True, last_run_at=slot)) client = app.test_client() async with client.session_transaction() as sess: sess.update(user_id=uid, user_role="operator", username=f"rn-{tag}", _csrf="test-token", login_at=time.time()) resp = await client.post(f"/ansible/schedules/{sid}/run-now", headers=CSRF) assert resp.status_code == 302 and resp.headers["Location"].endswith(run_id) assert [kw["origin_id"] for kw in started] == [sid] async with app.db_sessionmaker() as s: sched = await s.get(AnsibleSchedule, sid) assert sched.last_run_at == slot # its next turn is still a day after the slot assert sched.last_run_id == run_id and sched.last_error is None asyncio.run(_go())