Ctrl K
ComunidadGuías…

tests/test_archive_card.py

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

Archivo original ↗Preguntar a KiBot
"""Unit tests for the Exec `archive_card` tool + the shared recurrence clone.

Pure unit tests against the api modules — no live app, no container; the
helpers.DATA_DIR monkeypatch is the same one test_rd_lock.py uses.

What's pinned here is the parity that makes chat-side archiving safe: archiving
through Exec must do what the card dialog's archive button does — keep
`scheduled_day` (the only record of the day the work happened, and what /rd's
calendar counts archived cards on), put the nudge loop to rest, and revive a
recurring card's next occurrence exactly once.
"""
import json
import sys
from pathlib import Path

import pytest

sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "api"))

import helpers  # noqa: E402


def _seed(data_dir: Path, cards: list) -> None:
    (data_dir / "rd.json").write_text(json.dumps(
        {"columns": ["rd", "hq", "archives", "exile"], "cards": cards}))


@pytest.fixture
def data_dir(tmp_path, monkeypatch):
    monkeypatch.setattr(helpers, "DATA_DIR", tmp_path)
    monkeypatch.setattr(helpers, "_ACTIVITY_LOG", tmp_path / "activity_log.json")
    helpers._json_cache.clear()
    yield tmp_path
    helpers._json_cache.clear()


def _cards(data_dir: Path) -> list:
    return json.loads((data_dir / "rd.json").read_text())["cards"]


def test_archive_moves_card_and_keeps_scheduled_day(data_dir):
    import chat_tools
    _seed(data_dir, [{"id": "card-1", "title": "file taxes", "column": "hq",
                      "scheduled_day": "2026-09-15", "dir_start_min": 600}])
    res = chat_tools._tool_archive_card({"id": "card-1"})
    assert res["ok"] and res["column"] == "archives"
    card = _cards(data_dir)[0]
    assert card["column"] == "archives"
    # the day the work happened must survive the archive — /rd's calendar reads it
    assert card["scheduled_day"] == "2026-09-15"


def test_archive_resolves_the_nudge_loop(data_dir):
    import chat_tools
    _seed(data_dir, [{"id": "card-1", "title": "file taxes", "column": "hq",
                      "scheduled_day": "2026-09-15",
                      "nudge": {"stage": "awaiting", "awaiting_reply": True,
                                "next_nudge_at": "2026-09-15T11:00:00"}}])
    chat_tools._tool_archive_card({"id": "card-1"})
    n = _cards(data_dir)[0]["nudge"]
    assert n["stage"] == "resolved"
    assert n["awaiting_reply"] is False
    assert n["next_nudge_at"] is None


def test_archive_unknown_card_is_an_error_not_a_write(data_dir):
    import chat_tools
    _seed(data_dir, [{"id": "card-1", "title": "x", "column": "hq"}])
    assert "error" in chat_tools._tool_archive_card({"id": "card-nope"})
    assert _cards(data_dir)[0]["column"] == "hq"


def test_archive_twice_does_not_double_anything(data_dir):
    """Second call is a no-op report — it must not re-log or re-clone."""
    import chat_tools
    _seed(data_dir, [{"id": "card-1", "title": "weekly review", "column": "hq",
                      "due_date": "2026-09-15", "recur_type": "week"}])
    chat_tools._tool_archive_card({"id": "card-1"})
    again = chat_tools._tool_archive_card({"id": "card-1"})
    assert again["ok"] and "next_occurrence" not in again
    assert len(_cards(data_dir)) == 2  # the archived card + exactly one clone


def test_archive_revives_a_recurring_card(data_dir):
    import chat_tools
    _seed(data_dir, [{"id": "card-1", "title": "weekly review", "column": "hq",
                      "due_date": "2026-09-15", "recur_type": "week",
                      "scheduled_day": "2026-09-15", "dir_start_min": 600,
                      "nudge": {"stage": "nudging"}}])
    res = chat_tools._tool_archive_card({"id": "card-1"})
    clone = [c for c in _cards(data_dir) if c["id"] != "card-1"][0]
    assert res["next_occurrence"] == clone["due_date"]
    assert clone["column"] == "rd"
    assert clone["due_date"] > "2026-09-15"
    assert clone["scheduled_day"] is None
    # each occurrence starts its own loop / placement
    assert "nudge" not in clone and "dir_start_min" not in clone


def test_recurring_clone_dedupes_against_an_existing_occurrence(data_dir):
    """The next occurrence already on the board must not be cloned again — a
    re-archive would otherwise stack duplicates of the same week."""
    nxt = helpers._next_recurrence("2026-09-15", "week")
    done = {"id": "card-1", "title": "weekly review", "column": "archives",
            "due_date": "2026-09-15", "recur_type": "week"}
    existing = {"id": "card-2", "title": "Weekly Review", "column": "rd",
                "due_date": nxt, "recur_type": "week"}
    assert helpers.recurring_clone(done, [done, existing]) is None


def test_recurring_clone_id_never_collides(data_dir):
    """Two cards archived in the same millisecond must not share an id."""
    a = {"id": "card-1", "title": "one", "column": "archives",
         "due_date": "2026-09-15", "recur_type": "week"}
    b = {"id": "card-2", "title": "two", "column": "archives",
         "due_date": "2026-09-15", "recur_type": "week"}
    board = [a, b]
    c1 = helpers.recurring_clone(a, board)
    board = board + [c1]
    c2 = helpers.recurring_clone(b, board)
    assert c1["id"] != c2["id"]


def test_non_recurring_card_clones_nothing(data_dir):
    import chat_tools
    _seed(data_dir, [{"id": "card-1", "title": "one-off", "column": "hq",
                      "due_date": "2026-09-15"}])
    res = chat_tools._tool_archive_card({"id": "card-1"})
    assert "next_occurrence" not in res
    assert len(_cards(data_dir)) == 1


def test_archive_card_is_dispatchable_by_name(data_dir):
    """The handler map is what the model actually reaches — a tool declared in
    chat._chat_tools() but missing here answers 'Unknown tool'."""
    import chat_tools
    _seed(data_dir, [{"id": "card-1", "title": "x", "column": "hq"}])
    assert chat_tools._handle_tool("archive_card", {"id": "card-1"})["ok"]


def test_every_declared_tool_has_a_handler():
    """Schema and handler map must stay in sync in BOTH directions."""
    import chat
    import chat_tools
    declared = {t["name"] for t in chat._chat_tools()}
    assert declared == set(chat_tools._TOOL_HANDLERS)