fix(hub): подтверждение ensure_link с конфликтами и ensure_issue по автору и давности
check_confirmed для ensure_link: связь есть и нет конфликтов (другой тип между теми же задачами или обратное направление любого типа). check_confirmed для ensure_issue: необязательный reporter, сужает поиск по теме до задач этого автора. Тесты: конфликты, обратное направление при обоих ключах в зеркале, давность 7 дней, фильтр по reporter, выбор самой новой из двух. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
This commit is contained in:
parent
1fd64e974d
commit
ccf519c9e7
@ -106,14 +106,19 @@ def estimate(types, pacing: dict) -> int:
|
||||
return sum(int(pacing.get(t, 0)) for t in types)
|
||||
|
||||
|
||||
def check_confirmed(conn, cmd, link_types: dict) -> tuple[bool, dict | None]:
|
||||
"""Виден ли желаемый результат в зеркале. ensure_comment и refresh по состоянию не подтверждаются."""
|
||||
def check_confirmed(conn, cmd, link_types: dict, reporter: str | None = None) -> tuple[bool, dict | None]:
|
||||
"""Виден ли желаемый результат в зеркале. ensure_comment и refresh по состоянию не подтверждаются.
|
||||
reporter: если задан, ensure_issue подтверждается только задачей, заведённой этим пользователем."""
|
||||
ctype, p = cmd["type"], cmd["params"]
|
||||
if ctype in ("ensure_comment", "refresh"):
|
||||
return False, None
|
||||
if ctype == "ensure_issue":
|
||||
row = conn.execute("SELECT key FROM issue WHERE summary = ? AND substr(created, 1, 10) >= date('now', '-7 days') "
|
||||
"ORDER BY created DESC LIMIT 1", (p["summary"],)).fetchone()
|
||||
sql = "SELECT key FROM issue WHERE summary = ? AND substr(created, 1, 10) >= date('now', '-7 days')"
|
||||
args = [p["summary"]]
|
||||
if reporter is not None:
|
||||
sql += " AND reporter = ?"
|
||||
args.append(reporter)
|
||||
row = conn.execute(sql + " ORDER BY created DESC LIMIT 1", args).fetchone()
|
||||
return (True, {"key": row["key"]}) if row else (False, None)
|
||||
issue = mirror.get_issue(conn, p.get("key") or p.get("source"))
|
||||
if issue is None:
|
||||
@ -132,8 +137,14 @@ def check_confirmed(conn, cmd, link_types: dict) -> tuple[bool, dict | None]:
|
||||
final = target[-1] if isinstance(target, list) else target
|
||||
return issue["status"] == final, None
|
||||
if ctype == "ensure_link":
|
||||
# связь есть И конфликтов нет: другой тип между теми же задачами или обратное направление любого типа
|
||||
jira_type = link_types.get(p["link_type"], p["link_type"])
|
||||
return any(l["target_key"] == p["target"] and l["link_type"] == jira_type for l in issue["links_out"]), None
|
||||
all_types = set(link_types.values())
|
||||
present = any(l["target_key"] == p["target"] and l["link_type"] == jira_type for l in issue["links_out"])
|
||||
other_type = any(l["target_key"] == p["target"] and l["link_type"] in all_types - {jira_type}
|
||||
for l in issue["links_out"])
|
||||
reverse = any(l["source_key"] == p["target"] and l["link_type"] in all_types for l in issue["links_in"])
|
||||
return present and not other_type and not reverse, None
|
||||
if ctype == "ensure_sprint":
|
||||
want = None if p["sprint"] == "backlog" else p["sprint"]
|
||||
return (issue.get("sprint") or None) == want, None
|
||||
|
||||
@ -3,6 +3,8 @@ 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([
|
||||
@ -48,10 +50,12 @@ def test_substitute_and_estimate():
|
||||
|
||||
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"}],
|
||||
[{"source_key": "A-1", "target_key": "A-2", "link_type": "ASUPR_Связанные"}])
|
||||
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
|
||||
|
||||
|
||||
@ -79,10 +83,61 @@ def test_check_confirmed(cmd, expected):
|
||||
assert ok is expected
|
||||
|
||||
|
||||
def test_check_confirmed_issue_by_summary(monkeypatch):
|
||||
def test_ensure_link_reverse_direction_with_both_keys_in_mirror():
|
||||
# A-1 -> A-2 есть, спрашиваем A-2 -> A-1 при обоих ключах в зеркале: обратная связь — конфликт
|
||||
conn = _mirror()
|
||||
conn.execute("UPDATE issue SET created = strftime('%Y-%m-%dT%H:%M:%S.000+0300', 'now') WHERE key = 'A-1'")
|
||||
ok, result = commands.check_confirmed(conn, {"type": "ensure_issue", "params": {"temp_key": "new:1", "issue_type": "task", "summary": "Тема"}}, LT)
|
||||
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)
|
||||
assert ok and result == {"key": "A-1"}
|
||||
ok, _ = commands.check_confirmed(conn, {"type": "ensure_issue", "params": {"temp_key": "new:1", "issue_type": "task", "summary": "Другая"}}, LT)
|
||||
ok, _ = commands.check_confirmed(conn, _ensure_issue("Другая"), LT)
|
||||
assert not ok
|
||||
|
||||
|
||||
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)
|
||||
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"}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user