test: bool и [] в параметрах, журнал при пределе выдач, 501, эпик без ключа, «подготовлена заново», предупреждение и офлайн агента, тик (T3.2, T5.2, M-8, minor-3 #16; M-10 #4) (#32)
Решение 13: тесты фиксируют уже верное поведение; каждый проверен мутацией. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e180bb4e5f
commit
1f1dad6911
@ -3,10 +3,11 @@ import os
|
||||
import threading
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from datetime import timedelta
|
||||
|
||||
import pytest
|
||||
|
||||
from jira_hub import api, db, release, settings
|
||||
from jira_hub import api, db, queue, release, settings
|
||||
from jira_hub.client import HubClient, HubError
|
||||
|
||||
|
||||
@ -209,13 +210,15 @@ def test_numeric_param_400_internal_bug_500(hub, monkeypatch):
|
||||
assert e.value.status == 500
|
||||
|
||||
|
||||
def test_tick_survives_connection_failure(hub, monkeypatch):
|
||||
def test_tick_survives_connection_failure(hub, monkeypatch, caplog):
|
||||
def broken():
|
||||
raise OSError("диск недоступен")
|
||||
monkeypatch.setattr(hub["hub"], "conn", broken)
|
||||
hub["hub"].tick() # не бросает
|
||||
assert "автообновление: сбой тика" in caplog.text
|
||||
monkeypatch.undo()
|
||||
hub["hub"].tick()
|
||||
assert hub["client"].get("/batches")["batches"] == [] # агент не на связи — автообновления нет
|
||||
|
||||
|
||||
def test_claim_long_poll_returns_new_command(hub):
|
||||
@ -526,3 +529,47 @@ def test_settings_put_empty_body_returns_settings_unchanged(hub):
|
||||
c = hub["client"]
|
||||
before = c.get("/settings")
|
||||
assert c.put("/settings", {}) == before
|
||||
|
||||
|
||||
@pytest.mark.parametrize("method", ["DELETE", "PATCH"])
|
||||
def test_other_methods_are_501_not_json(hub, method):
|
||||
"""M-8 #16, спека «Неподдерживаемый метод»: 501 стандартной страницей http.server, не JSON; база не меняется."""
|
||||
import http.client
|
||||
hub["client"].post("/batches", {"title": "x", "commands": [{"type": "ensure_fields",
|
||||
"params": {"key": "A-1", "duedate": "2026-10-23"}}]})
|
||||
host, port = hub["base"].removeprefix("http://").split(":")
|
||||
conn = http.client.HTTPConnection(host, int(port), timeout=10)
|
||||
try:
|
||||
conn.request(method, "/batches/1", headers={"Authorization": "Bearer " + hub["client"].token})
|
||||
r = conn.getresponse()
|
||||
r.read()
|
||||
assert r.status == 501 and not (r.getheader("Content-Type") or "").startswith("application/json")
|
||||
finally:
|
||||
conn.close()
|
||||
assert hub["client"].get("/batches/1")["status"] == "queued"
|
||||
|
||||
|
||||
def test_agent_warning_and_offline(hub):
|
||||
"""M-10 #4: «обновите агента» при agent_version < min_agent_version; нет контакта дольше порога — офлайн."""
|
||||
c = hub["client"]
|
||||
c.put("/settings", {"min_agent_version": 3})
|
||||
hub["agent"].post("/agent/claim", {"agent_version": 2})
|
||||
v = c.get("/agent")
|
||||
assert v["online"] is True and v["warning"] == "обновите агента"
|
||||
hub["agent"].post("/agent/claim", {"agent_version": 3})
|
||||
assert c.get("/agent")["warning"] is None
|
||||
old = (db.utcnow() - timedelta(minutes=queue.ONLINE_MINUTES + 1)).isoformat()
|
||||
conn = hub["hub"].conn()
|
||||
conn.execute("UPDATE agent_status SET last_seen_at = ? WHERE id = 1", (old,))
|
||||
conn.close()
|
||||
assert c.get("/agent")["online"] is False
|
||||
|
||||
|
||||
def test_tick_creates_auto_refresh_once(hub):
|
||||
"""M-10 #4: тик при агенте на связи и пустой очереди ставит «Автообновление»; второй тик — нет (очередь занята)."""
|
||||
hub["agent"].post("/agent/claim", {"agent_version": 9})
|
||||
hub["hub"].tick()
|
||||
batches = hub["client"].get("/batches", limit=5)["batches"]
|
||||
assert [b["title"] for b in batches] == ["Автообновление"] and batches[0]["author"] == "hub"
|
||||
hub["hub"].tick()
|
||||
assert len(hub["client"].get("/batches", limit=5)["batches"]) == 1
|
||||
|
||||
@ -177,3 +177,24 @@ def test_ensure_fields_text_compare_ignores_crlf(conn, mirror_value, param, expe
|
||||
def test_type_error_messages(ctype, name, value, msg):
|
||||
"""T3.1 #16, решение 15: _type_error по веткам — текст ошибки для каждого вида параметра (до и после правки один)."""
|
||||
assert commands._type_error(ctype, name, value) == msg
|
||||
|
||||
|
||||
@pytest.mark.parametrize("cmd, name", [
|
||||
({"type": "ensure_fields", "params": {"key": "A-1", "duedate": True}}, "duedate"),
|
||||
({"type": "ensure_comment", "params": {"key": "A-1", "marker": "m", "body": False}}, "body"),
|
||||
({"type": "ensure_issue", "params": {"temp_key": "new:1", "issue_type": "task", "summary": True}}, "summary"),
|
||||
])
|
||||
def test_validate_rejects_bool_in_string_param(cmd, name):
|
||||
"""T3.2 #16: true/false в строковом параметре — 422 с именем параметра, а не запись «True» в Jira."""
|
||||
with pytest.raises(CommandError, match=f"{name} — строка"):
|
||||
commands.validate([cmd])
|
||||
|
||||
|
||||
def test_validate_empty_list_params():
|
||||
"""T3.2 #16: [] в необязательном списке допустим; [] в обязательном или единственном — как отсутствие."""
|
||||
out = commands.validate([{"type": "ensure_fields", "params": {"key": "A-1", "labels_add": [], "labels_remove": []}}])
|
||||
assert out[0]["params"] == {"key": "A-1", "labels_add": [], "labels_remove": []}
|
||||
with pytest.raises(CommandError, match="нет target"):
|
||||
commands.validate([{"type": "ensure_status", "params": {"key": "A-1", "target": []}}])
|
||||
with pytest.raises(CommandError, match="refresh без keys и jql"):
|
||||
commands.validate([{"type": "refresh", "params": {"keys": []}}])
|
||||
|
||||
@ -113,3 +113,12 @@ def test_reversed_link_reported_separately(conn, tmp_path):
|
||||
rep = importer.import_package(conn, tmp_path)
|
||||
assert rep["reversed_links"] == [{"source": "AURORA-1533", "target": "AURORA-1586", "link_type": "ASUPR_Связанные"}]
|
||||
assert rep["missing_links"] == [{"source": "AURORA-1533", "target": "AURORA-1416", "link_type": "ASUPR_Блокер_Начала"}]
|
||||
|
||||
|
||||
def test_epic_without_key_is_skipped(tmp_path, conn):
|
||||
"""M-8 #16 (T7.3 #4), спека migration «Эпик без ключа»: строка без «Ключ» пропущена, заметки new:* нет."""
|
||||
write_csv(tmp_path / "epics.csv", ["Код", "Ключ", "Эпик", "Комментарий"],
|
||||
[["NEW-2", "", "Без ключа", "ещё не создан"], ["AURORA-1350", "AURORA-1350", "Загрузка", "ядро"]])
|
||||
rep = importer.import_package(conn, tmp_path)
|
||||
assert rep["notes"] == 1 and rep["missing_keys"] == ["AURORA-1350"]
|
||||
assert [r["key"] for r in conn.execute("SELECT key FROM issue_note ORDER BY key")] == ["AURORA-1350"]
|
||||
|
||||
@ -499,3 +499,19 @@ def test_reconcile_confirms_fields_with_crlf(conn):
|
||||
mirror.apply_state(conn, [{"key": "A-1", "summary": "т", "description": "a\r\nb", "labels": []}], [])
|
||||
assert queue.reconcile(conn) == [c["id"]]
|
||||
assert queue.get_command(conn, c["id"])["status"] == "confirmed_by_state"
|
||||
|
||||
|
||||
def test_delivery_limit_of_write_command_keeps_open_sync_run(conn):
|
||||
"""T5.2 #16: предел выдач у не-refresh команды не закрывает журнал чужой загрузки (finished_at — только для refresh)."""
|
||||
queue.create_batch(conn, "x", "claude", [fields("A-1")])
|
||||
c = queue.claim(conn)
|
||||
r = queue.create_batch(conn, "обновить", "claude", [{"type": "refresh", "params": {"keys": ["A-9"]}}])
|
||||
rid = queue.batch_view(conn, r["id"])["commands"][0]["id"]
|
||||
conn.execute("UPDATE command SET status = 'claimed', claimed_at = '2099-01-01T00:00:00+00:00', deliveries = 1 "
|
||||
"WHERE id = ?", (rid,))
|
||||
conn.execute("INSERT INTO sync_run(started_at, kind, query, command_id) VALUES (?, 'keys', 'A-9', ?)", (db.now(), rid))
|
||||
for _ in range(queue.MAX_DELIVERIES - 1):
|
||||
assert queue.claim(conn)["id"] == c["id"]
|
||||
assert queue.claim(conn)["id"] == rid # c — failed по пределу, довыдаётся refresh
|
||||
assert queue.get_command(conn, c["id"])["status"] == "failed"
|
||||
assert conn.execute("SELECT finished_at FROM sync_run WHERE command_id = ?", (rid,)).fetchone()["finished_at"] is None
|
||||
|
||||
@ -365,7 +365,8 @@ def test_vanished_tree_keeps_work(env, capsys):
|
||||
for _ in range(3):
|
||||
r.step()
|
||||
assert (root / "work" / "busy.json").exists() and (root / "req").is_dir()
|
||||
assert "удалён незавершённый запрос" not in capsys.readouterr().out
|
||||
out = capsys.readouterr().out
|
||||
assert "удалён незавершённый запрос" not in out and "подготовлена заново" in out # minor-3 #16
|
||||
|
||||
|
||||
def test_each_dir_cleaned_once(env, tmp_path, capsys):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user