diff --git a/src/jira_hub/mcp_server.py b/src/jira_hub/mcp_server.py index 2a0aa98..b7ceb61 100644 --- a/src/jira_hub/mcp_server.py +++ b/src/jira_hub/mcp_server.py @@ -141,8 +141,9 @@ def batch_status(id: int) -> str: def batch_wait(id: int, timeout_minutes: int = 10) -> str: """Ждать завершения пакета (до 60 мин), вернуть итог по командам.""" deadline = time.monotonic() + min(max(int(timeout_minutes), 1), 60) * 60 + client = _client() while True: - b = _client().get(f"/batches/{id}") + b = client.get(f"/batches/{id}") if b["status"] in ("done", "cancelled") or time.monotonic() >= deadline: return fmt_batch(b) + ("" if b["status"] in ("done", "cancelled") else "\n(таймаут ожидания, пакет ещё выполняется)") time.sleep(POLL_SECONDS) diff --git a/tests/test_mcp.py b/tests/test_mcp.py index 9f8c494..ab52dd8 100644 --- a/tests/test_mcp.py +++ b/tests/test_mcp.py @@ -1,5 +1,7 @@ import json +import re import threading +from types import SimpleNamespace import pytest @@ -52,9 +54,8 @@ def test_tools_roundtrip(live): assert "Повтор команды" in mcp_server.command_retry(cmd["id"]) assert "cancelled" in mcp_server.batch_cancel(batch_id + 1) r = mcp_server.refresh(full=True, wait=False) - assert "Обновление" in r or "#" in r + assert re.fullmatch(r"пакет #\d+ «Обновление» поставлен", r) assert "онлайн" in mcp_server.agent_status() - assert "A-1" in mcp_server.sync_log(limit=3) or "keys" in mcp_server.sync_log(limit=3) assert "120" in mcp_server.settings_get("pacing") assert "settings_version" in mcp_server.settings_set("pacing", json.dumps({"refresh": 1})) assert mcp_server.settings_set("tokens", '"x"').startswith("ошибка хаба 422") @@ -72,3 +73,76 @@ def test_formatters(): def test_settings_set_bad_json(live): assert mcp_server.settings_set("pacing", "{не json").startswith("значение не JSON") + + +def _batch(status): + return {"id": 7, "title": "т", "author": "claude", "created_at": "2026-10-11", "status": status, + "remaining_seconds": 0, "counts": {status: 1}, "commands": []} + + +class FakeClient: + def __init__(self, statuses, runs=None): + self.statuses, self.calls, self.runs = list(statuses), 0, runs + + def get(self, path, **params): + if path == "/sync-runs": + return {"runs": self.runs} + self.calls += 1 + return _batch(self.statuses.pop(0) if len(self.statuses) > 1 else self.statuses[0]) + + +def _fake_time(monkeypatch, step=1.0): + clock = {"now": 0.0, "sleeps": 0, "deadlines": []} + + def monotonic(): + clock["now"] += step + return clock["now"] + + def sleep(_): + clock["sleeps"] += 1 + + monkeypatch.setattr(mcp_server, "time", SimpleNamespace(monotonic=monotonic, sleep=sleep)) + return clock + + +def test_batch_wait_polls_until_done(monkeypatch): + fake = FakeClient(["running", "running", "done"]) + monkeypatch.setattr(mcp_server, "_client", lambda: fake) + clock = _fake_time(monkeypatch) + out = mcp_server.batch_wait(7, timeout_minutes=10) + assert out == mcp_server.fmt_batch(_batch("done")) and "таймаут" not in out + assert fake.calls == 3 and clock["sleeps"] == 2 + + +def test_batch_wait_timeout(monkeypatch): + fake = FakeClient(["running"]) + monkeypatch.setattr(mcp_server, "_client", lambda: fake) + clock = _fake_time(monkeypatch, step=30.0) # 1 минута = 2 шага монотонных часов + out = mcp_server.batch_wait(7, timeout_minutes=1) + assert "таймаут ожидания" in out + assert 1 < fake.calls < 10 and clock["sleeps"] == fake.calls - 1 + + +@pytest.mark.parametrize("asked,minutes", [(0, 1), (1000, 60)]) +def test_batch_wait_clamps_timeout(monkeypatch, asked, minutes): + fake = FakeClient(["running"]) + monkeypatch.setattr(mcp_server, "_client", lambda: fake) + clock = _fake_time(monkeypatch, step=60.0) # каждый вызов часов = +1 минута + mcp_server.batch_wait(7, timeout_minutes=asked) + # первый вызов часов (1 мин) задаёт старт; дедлайн = старт + minutes; выход, когда now >= дедлайна + assert fake.calls == minutes + + +def test_client_created_once_per_wait(monkeypatch): + fake, made = FakeClient(["running", "running", "done"]), [] + monkeypatch.setattr(mcp_server, "_client", lambda: made.append(1) or fake) + _fake_time(monkeypatch) + mcp_server.batch_wait(7) + assert len(made) == 1 + + +def test_sync_log_content(monkeypatch): + runs = [{"id": 3, "kind": "full", "query": "project = AWPCA", "started_at": "2026-10-11T10:00", "finished_at": None, + "issues_count": 12, "links_count": 5, "jira_calls": 4, "rate_limited": 1}] + monkeypatch.setattr(mcp_server, "_client", lambda: FakeClient(["done"], runs)) + assert mcp_server.sync_log() == "#3 full project = AWPCA: 2026-10-11T10:00 → —, задач 12, связей 5, вызовов 4, 429: 1"