fix(queue): ensure_issue не подтверждается по состоянию без автора (R-11, I-1); reconcile не затирает отчёт и отмену (M-1)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
This commit is contained in:
parent
3c37e851e9
commit
a09b794302
@ -108,17 +108,18 @@ def estimate(types, pacing: dict) -> int:
|
||||
|
||||
def check_confirmed(conn, cmd, link_types: dict, reporter: str | None = None) -> tuple[bool, dict | None]:
|
||||
"""Виден ли желаемый результат в зеркале. ensure_comment и refresh по состоянию не подтверждаются.
|
||||
reporter: если задан, ensure_issue подтверждается только задачей, заведённой этим пользователем."""
|
||||
reporter: логин Jira, под которым агент заводит задачи. ensure_issue подтверждается ТОЛЬКО задачей этого
|
||||
автора; пока reporter неизвестен (None), по состоянию не подтверждается (R-11): совпадение темы с чужой
|
||||
задачей не доказательство, а запись в чужую задачу недопустима."""
|
||||
ctype, p = cmd["type"], cmd["params"]
|
||||
if ctype in ("ensure_comment", "refresh"):
|
||||
return False, None
|
||||
if ctype == "ensure_issue":
|
||||
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()
|
||||
if reporter is None:
|
||||
return False, None
|
||||
row = conn.execute("SELECT key FROM issue WHERE summary = ? AND reporter = ? "
|
||||
"AND substr(created, 1, 10) >= date('now', '-7 days') ORDER BY created DESC LIMIT 1",
|
||||
(p["summary"], reporter)).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:
|
||||
|
||||
@ -206,7 +206,8 @@ def report_result(conn, command_id, status, result=None, error=None, jira_calls=
|
||||
def reconcile(conn) -> list[int]:
|
||||
"""Взятые команды без ответа, чей результат уже виден в зеркале → confirmed_by_state."""
|
||||
link_types = settings.get(conn, "link_types") or {}
|
||||
reporter = settings.get(conn, "reporter")
|
||||
reporter = settings.get(conn, "reporter") # задел: сейчас не задаётся, ensure_issue по состоянию не подтверждается (R-11);
|
||||
# позже сюда придёт jira_user от агента
|
||||
changed = []
|
||||
for row in conn.execute("SELECT * FROM command WHERE status = 'claimed'").fetchall():
|
||||
cmd = _cmd(row)
|
||||
@ -214,8 +215,11 @@ def reconcile(conn) -> list[int]:
|
||||
if not ok:
|
||||
continue
|
||||
with db.transaction(conn):
|
||||
conn.execute("UPDATE command SET status = 'confirmed_by_state', finished_at = ?, result = ? WHERE id = ?",
|
||||
(db.now(), _dump(result) if result else None, cmd["id"]))
|
||||
cur = conn.execute("UPDATE command SET status = 'confirmed_by_state', finished_at = ?, result = ? "
|
||||
"WHERE id = ? AND status = 'claimed'",
|
||||
(db.now(), _dump(result) if result else None, cmd["id"]))
|
||||
if cur.rowcount == 0: # отчёт агента или отмена опередили сверку
|
||||
continue
|
||||
if cmd["type"] == "ensure_issue" and result:
|
||||
mirror.rename_note(conn, cmd["params"]["temp_key"], result["key"])
|
||||
changed.append(cmd["id"])
|
||||
|
||||
@ -111,17 +111,25 @@ def _ensure_issue(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)
|
||||
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)
|
||||
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)
|
||||
ok, _ = commands.check_confirmed(conn, _ensure_issue("Тема"), LT, reporter="petrov")
|
||||
assert ok is False
|
||||
|
||||
|
||||
|
||||
@ -225,3 +225,43 @@ def test_reconcile_respects_reporter_setting():
|
||||
mirror.apply_state(conn, [{"key": "A-8", "summary": "Тема", "reporter": "robot", "created": created, "labels": []}], [])
|
||||
assert queue.reconcile(conn) == [c["id"]]
|
||||
assert queue.get_command(conn, c["id"])["result"] == {"key": "A-8"}
|
||||
|
||||
|
||||
def _claimed_ensure_issue(conn, summary="Тема"):
|
||||
queue.create_batch(conn, "x", "claude", [{"type": "ensure_issue", "params": {"temp_key": "new:1", "issue_type": "task", "summary": summary}}])
|
||||
c = queue.claim(conn)
|
||||
conn.execute("UPDATE command SET claimed_at = '2020-01-01T00:00:00+00:00' WHERE id = ?", (c["id"],))
|
||||
return c
|
||||
|
||||
|
||||
def test_reconcile_does_not_confirm_ensure_issue_without_reporter():
|
||||
"""R-11: настройка reporter не задана — совпадение темы не подтверждает, заметка new:N не переезжает."""
|
||||
conn = fresh()
|
||||
c = _claimed_ensure_issue(conn)
|
||||
mirror.set_note(conn, "new:1", {"work_kind": "разработка"}, "t")
|
||||
created = db.utcnow().isoformat()
|
||||
mirror.apply_state(conn, [{"key": "A-7", "summary": "Тема", "reporter": "other", "created": created, "labels": []}], [])
|
||||
assert queue.reconcile(conn) == []
|
||||
assert queue.get_command(conn, c["id"])["status"] == "claimed"
|
||||
assert mirror.get_note(conn, "new:1") is not None and mirror.get_note(conn, "A-7") is None
|
||||
|
||||
|
||||
def test_reconcile_does_not_overwrite_finished_command(monkeypatch):
|
||||
"""M-1: отчёт агента/отмена между check_confirmed и UPDATE не перезаписываются."""
|
||||
conn = fresh()
|
||||
settings.set_(conn, "reporter", "robot")
|
||||
c = _claimed_ensure_issue(conn)
|
||||
mirror.set_note(conn, "new:1", {"work_kind": "разработка"}, "t")
|
||||
mirror.apply_state(conn, [{"key": "A-8", "summary": "Тема", "reporter": "robot",
|
||||
"created": db.utcnow().isoformat(), "labels": []}], [])
|
||||
real = queue.C.check_confirmed
|
||||
|
||||
def racing(*a, **kw):
|
||||
out = real(*a, **kw)
|
||||
queue.cancel_command(conn, c["id"], force=True) # гонка: отмена после проверки
|
||||
return out
|
||||
|
||||
monkeypatch.setattr(queue.C, "check_confirmed", racing)
|
||||
assert queue.reconcile(conn) == []
|
||||
assert queue.get_command(conn, c["id"])["status"] == "cancelled"
|
||||
assert mirror.get_note(conn, "new:1") is not None # rename_note не выполнялся
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user