Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
152 lines
8.5 KiB
Python
152 lines
8.5 KiB
Python
import pytest
|
||
|
||
from jira_hub import commands, db, mirror
|
||
from jira_hub.commands import CommandError
|
||
|
||
NOW = "strftime('%Y-%m-%dT%H:%M:%S.000+0300', 'now')"
|
||
|
||
|
||
def test_validate_ok_and_tail_keys():
|
||
cmds = commands.validate([
|
||
{"type": "ensure_issue", "params": {"temp_key": "new:60", "issue_type": "task", "summary": "Т"}},
|
||
{"type": "ensure_link", "params": {"source": "new:60", "target": "A-1", "link_type": "связанные"}},
|
||
{"type": "refresh", "params": {"keys": ["A-1", "new:60"]}},
|
||
])
|
||
assert [c["type"] for c in cmds] == ["ensure_issue", "ensure_link", "refresh"]
|
||
assert commands.referenced_keys(cmds[1]) == ["new:60", "A-1"]
|
||
assert commands.temp_keys_used(cmds[1]) == ["new:60"]
|
||
assert commands.temp_keys_used(cmds[0]) == []
|
||
assert commands.temp_keys_used(cmds[2]) == ["new:60"]
|
||
|
||
|
||
@pytest.mark.parametrize("bad, msg", [
|
||
([], "без команд"),
|
||
([{"type": "ensure_all"}], "неизвестный тип"),
|
||
([{"type": "ensure_fields", "params": {"key": "A-1"}}], "без полей"),
|
||
([{"type": "ensure_fields", "params": {"key": "A-1", "duedate": "2026-10-23", "foo": 1}}], "лишние параметры"),
|
||
([{"type": "ensure_status", "params": {"key": "A-1"}}], "нет target"),
|
||
([{"type": "refresh", "params": {}}], "без keys и jql"),
|
||
([{"type": "ensure_link", "params": {"source": "new:3", "target": "A-1", "link_type": "связанные"}}], "не объявлен"),
|
||
([{"type": "ensure_issue", "params": {"temp_key": "A-1", "issue_type": "task", "summary": "x"}}], "new:<n>"),
|
||
([{"type": "ensure_issue", "params": {"temp_key": "new:1", "issue_type": "bug", "summary": "x"}}], "task или epic"),
|
||
([{"type": "ensure_issue", "params": {"temp_key": "new:1", "issue_type": "task", "summary": "x"}},
|
||
{"type": "ensure_issue", "params": {"temp_key": "new:1", "issue_type": "task", "summary": "y"}}], "уже объявлен"),
|
||
])
|
||
def test_validate_errors(bad, msg):
|
||
with pytest.raises(CommandError, match=msg):
|
||
commands.validate(bad)
|
||
|
||
|
||
def test_substitute_and_estimate():
|
||
p = commands.substitute("ensure_link", {"source": "new:60", "target": "A-1", "link_type": "связанные"}, {"new:60": "A-9"})
|
||
assert p["source"] == "A-9"
|
||
p = commands.substitute("refresh", {"keys": ["new:60", "A-1"]}, {"new:60": "A-9"})
|
||
assert p["keys"] == ["A-9", "A-1"]
|
||
p = commands.substitute("ensure_issue", {"temp_key": "new:60", "issue_type": "task", "summary": "x", "epic_key": "new:2"},
|
||
{"new:2": "A-7", "new:60": "A-9"})
|
||
assert p["temp_key"] == "new:60" and p["epic_key"] == "A-7"
|
||
assert commands.estimate(["ensure_fields", "ensure_comment", "refresh"], {"ensure_fields": 120, "ensure_comment": 30, "refresh": 2}) == 152
|
||
|
||
|
||
def _mirror():
|
||
conn = db.connect(":memory:")
|
||
mirror.apply_state(conn, [
|
||
{"key": "A-1", "summary": "Тема", "status": "Готово", "priority": "Высокий",
|
||
"duedate": "2026-10-23", "labels": ["AWPCA", "kind-dev"], "sprint": "Спринт 2",
|
||
"created": "2026-10-10T10:00:00.000+0300", "assignee": "ivanov", "reporter": "petrov"},
|
||
{"key": "A-2", "summary": "Вторая", "status": "Разработка", "created": "2026-10-10T11:00:00.000+0300"},
|
||
], [{"source_key": "A-1", "target_key": "A-2", "link_type": "ASUPR_Связанные"}])
|
||
return conn
|
||
|
||
|
||
LT = {"связанные": "ASUPR_Связанные", "блокирует начало": "ASUPR_Блокер_Начала"}
|
||
|
||
|
||
@pytest.mark.parametrize("cmd, expected", [
|
||
({"type": "ensure_fields", "params": {"key": "A-1", "duedate": "2026-10-23", "priority": "Высокий"}}, True),
|
||
({"type": "ensure_fields", "params": {"key": "A-1", "duedate": "2026-10-24"}}, False),
|
||
({"type": "ensure_fields", "params": {"key": "A-1", "labels_add": ["kind-dev"], "labels_remove": ["fact-done"]}}, True),
|
||
({"type": "ensure_fields", "params": {"key": "A-1", "labels_remove": ["kind-dev"]}}, False),
|
||
({"type": "ensure_status", "params": {"key": "A-1", "target": ["Разработка", "Готово"]}}, True),
|
||
({"type": "ensure_status", "params": {"key": "A-1", "target": "Разработка"}}, False),
|
||
({"type": "ensure_link", "params": {"source": "A-1", "target": "A-2", "link_type": "связанные"}}, True),
|
||
({"type": "ensure_link", "params": {"source": "A-1", "target": "A-2", "link_type": "блокирует начало"}}, False),
|
||
({"type": "ensure_link", "params": {"source": "A-2", "target": "A-1", "link_type": "связанные"}}, False),
|
||
({"type": "ensure_sprint", "params": {"key": "A-1", "sprint": "Спринт 2"}}, True),
|
||
({"type": "ensure_sprint", "params": {"key": "A-1", "sprint": "backlog"}}, False),
|
||
({"type": "ensure_comment", "params": {"key": "A-1", "marker": "m", "body": "b"}}, False),
|
||
({"type": "refresh", "params": {"keys": ["A-1"]}}, False),
|
||
({"type": "ensure_fields", "params": {"key": "A-404", "duedate": "2026-10-23"}}, False),
|
||
])
|
||
def test_check_confirmed(cmd, expected):
|
||
ok, _ = commands.check_confirmed(_mirror(), cmd, LT)
|
||
assert ok is expected
|
||
|
||
|
||
def test_ensure_link_reverse_direction_with_both_keys_in_mirror():
|
||
# A-1 -> A-2 есть, спрашиваем A-2 -> A-1 при обоих ключах в зеркале: обратная связь — конфликт
|
||
conn = _mirror()
|
||
ok, _ = commands.check_confirmed(conn, {"type": "ensure_link", "params": {"source": "A-2", "target": "A-1", "link_type": "связанные"}}, LT)
|
||
assert ok is False
|
||
|
||
|
||
def test_ensure_link_conflict_other_type_blocks_confirmation():
|
||
conn = _mirror()
|
||
conn.execute("INSERT INTO issue_link(source_key, target_key, link_type) VALUES ('A-1', 'A-2', 'ASUPR_Блокер_Начала')")
|
||
ok, _ = commands.check_confirmed(conn, {"type": "ensure_link", "params": {"source": "A-1", "target": "A-2", "link_type": "связанные"}}, LT)
|
||
assert ok is False
|
||
|
||
|
||
def test_ensure_link_conflict_reverse_blocks_confirmation():
|
||
conn = _mirror()
|
||
conn.execute("INSERT INTO issue_link(source_key, target_key, link_type) VALUES ('A-2', 'A-1', 'ASUPR_Связанные')")
|
||
ok, _ = commands.check_confirmed(conn, {"type": "ensure_link", "params": {"source": "A-1", "target": "A-2", "link_type": "связанные"}}, LT)
|
||
assert ok is False
|
||
|
||
|
||
def _ensure_issue(summary):
|
||
return {"type": "ensure_issue", "params": {"temp_key": "new:1", "issue_type": "task", "summary": summary}}
|
||
|
||
|
||
def test_check_confirmed_issue_by_summary():
|
||
conn = _mirror()
|
||
conn.execute(f"UPDATE issue SET created = {NOW} WHERE key = 'A-1'")
|
||
ok, result = commands.check_confirmed(conn, _ensure_issue("Тема"), LT, reporter="petrov")
|
||
assert ok and result == {"key": "A-1"}
|
||
ok, _ = commands.check_confirmed(conn, _ensure_issue("Другая"), LT, reporter="petrov")
|
||
assert not ok
|
||
|
||
|
||
def test_ensure_issue_without_reporter_never_confirmed():
|
||
"""R-11: пока автор неизвестен, совпадение темы не подтверждает ensure_issue."""
|
||
conn = _mirror()
|
||
conn.execute(f"UPDATE issue SET created = {NOW} WHERE key = 'A-1'")
|
||
assert commands.check_confirmed(conn, _ensure_issue("Тема"), LT) == (False, None)
|
||
assert commands.check_confirmed(conn, _ensure_issue("Тема"), LT, reporter=None) == (False, None)
|
||
|
||
|
||
def test_ensure_issue_older_than_7_days_not_confirmed():
|
||
conn = _mirror()
|
||
conn.execute(f"UPDATE issue SET created = {NOW} WHERE key = 'A-1'")
|
||
conn.execute(f"UPDATE issue SET created = strftime('%Y-%m-%dT%H:%M:%S.000+0300', 'now', '-30 days') WHERE key = 'A-1'")
|
||
ok, _ = commands.check_confirmed(conn, _ensure_issue("Тема"), LT, reporter="petrov")
|
||
assert ok is False
|
||
|
||
|
||
def test_ensure_issue_reporter_filter():
|
||
conn = _mirror()
|
||
conn.execute(f"UPDATE issue SET created = {NOW} WHERE key = 'A-1'")
|
||
ok, _ = commands.check_confirmed(conn, _ensure_issue("Тема"), LT, reporter="ivanov")
|
||
assert ok is False
|
||
ok, result = commands.check_confirmed(conn, _ensure_issue("Тема"), LT, reporter="petrov")
|
||
assert ok and result == {"key": "A-1"}
|
||
|
||
|
||
def test_ensure_issue_two_matches_takes_newest():
|
||
conn = _mirror()
|
||
mirror.apply_state(conn, [{"key": "A-3", "summary": "Тема", "reporter": "petrov"}], [])
|
||
conn.execute(f"UPDATE issue SET created = strftime('%Y-%m-%dT%H:%M:%S.000+0300', 'now', '-1 day') WHERE key = 'A-1'")
|
||
conn.execute(f"UPDATE issue SET created = {NOW} WHERE key = 'A-3'")
|
||
ok, result = commands.check_confirmed(conn, _ensure_issue("Тема"), LT, reporter="petrov")
|
||
assert ok and result == {"key": "A-3"}
|