Ctrl K
ComunidadGuías…

tests/test_morning_cc_reset.py

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""The 4:30 morning run ends the Exec panel's sidecar thread (2026-10-06).

Since phase 3 the panel's conversation lives in the cc sidecar, not chat.json.
build_morning() still deletes chat.json, but that alone left the panel on the
same thread night after night. api_morning must also call
cc_client.new_conversation(), and a sidecar failure must surface in `errors`
without failing the morning. Any morning error is also posted to the Exec
chat (2026-10-07) so it is seen that day, not only in /debug.
"""
import asyncio
import os
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "api"))
for _k in ("API_KEY", "TURNSTILE_SITE_KEY", "TURNSTILE_SECRET"):
    os.environ.setdefault(_k, "test")  # read at import by auth/routers

import routes_morning as routes_api  # noqa: E402


def _run(monkeypatch, cc_result, morning=None, stamp=None):
    calls = []
    posted = []

    async def fake_new():
        calls.append(1)
        return cc_result

    async def fake_push(payload):
        if "comment" in payload:
            posted.append(payload["comment"])

    monkeypatch.setattr(routes_api, "build_morning", morning or (lambda: {"generated_at": "x"}))
    monkeypatch.setattr(routes_api, "append_monitor_comment", lambda _t: None)
    monkeypatch.setattr(routes_api.cc_client, "new_conversation", fake_new)
    monkeypatch.setattr(routes_api, "push_to_monitor", fake_push)
    monkeypatch.setattr(routes_api, "_NIGHTLY", stamp or Path(os.devnull) / "x")
    return asyncio.run(routes_api.api_morning()), calls, posted


def test_morning_ends_the_sidecar_thread(monkeypatch):
    result, calls, posted = _run(monkeypatch, {"ok": True, "archived": "a.md"})
    assert calls == [1]
    assert "errors" not in result
    assert posted == []   # a clean morning says nothing


def test_sidecar_failure_is_reported_not_raised(monkeypatch):
    result, calls, posted = _run(monkeypatch, {"ok": False, "detail": "unreachable"})
    assert calls == [1]
    assert result["errors"]["cc_new"] == "unreachable"
    assert len(posted) == 1 and "cc_new" in posted[0]


def test_step_errors_are_posted_to_exec_chat(monkeypatch):
    def morning():
        return {"generated_at": "x", "errors": {"gcal_import": "invalid_grant"}}

    _, _, posted = _run(monkeypatch, {"ok": True}, morning)
    assert len(posted) == 1
    assert "gcal_import" in posted[0] and "invalid_grant" in posted[0]


def test_crashed_morning_is_posted_then_raised(monkeypatch):
    import pytest
    from fastapi import HTTPException

    def boom():
        raise RuntimeError("disk full")

    posted = []

    async def fake_push(payload):
        posted.append(payload.get("comment"))

    monkeypatch.setattr(routes_api, "build_morning", boom)
    monkeypatch.setattr(routes_api, "append_monitor_comment", lambda _t: None)
    monkeypatch.setattr(routes_api, "push_to_monitor", fake_push)
    with pytest.raises(HTTPException):
        asyncio.run(routes_api.api_morning())
    assert len(posted) == 1 and "disk full" in posted[0]


def test_gcal_invalid_grant_error_carries_regrant_link():
    """An expired gcal token can only be fixed by hand — the error says where."""
    fix = routes_api._morning_error_fix("gcal_import", "('invalid_grant: Token has been expired or revoked.', {})")
    assert "https://wai-lau.net/api/gcal/auth" in fix
    assert routes_api._morning_error_fix("gcal_import", "timeout") == ""


def test_morning_moves_the_nightly_stamp(monkeypatch, tmp_path):
    """Open pages reload when /api/nightly's stamp moves (web/nightly-reload.js)."""
    stamp = tmp_path / "nightly.json"
    monkeypatch.setattr(routes_api, "_NIGHTLY", stamp)
    assert routes_api.api_nightly() == {"at": None}   # never run: no reload
    _run(monkeypatch, {"ok": True}, stamp=stamp)
    first = routes_api.api_nightly()["at"]
    assert first
    _run(monkeypatch, {"ok": True}, stamp=stamp)
    assert routes_api.api_nightly()["at"] != first