Удалить «статус по факту» полностью (#104) #105
@ -16,7 +16,7 @@ log = logging.getLogger(__name__)
|
||||
|
||||
MIME_RE = re.compile(r"[\w!#$&^.+-]+/[\w!#$&^.+-]+", re.ASCII) # тип/подтип без параметров, пробелов и не-ASCII: заголовок не внушить
|
||||
DOWNLOADS_EVERY = 24 * 3600 # уборка downloads/ не чаще раза в сутки
|
||||
ISSUE_FILTERS = ("sprint", "assignee", "work_kind", "fact_status", "status", "label", "epic", "neighbor", "q", "hide_closed", "project")
|
||||
ISSUE_FILTERS = ("sprint", "assignee", "work_kind", "status", "label", "epic", "neighbor", "q", "hide_closed", "project")
|
||||
_OFFSET = re.compile(r"(Z|[+-]\d{2}:?\d{2})$")
|
||||
|
||||
|
||||
|
||||
@ -42,7 +42,7 @@ def _import(conn, folder: Path) -> dict:
|
||||
if not key:
|
||||
continue
|
||||
_note(conn, key, {
|
||||
"fact_status": _clean(r.get("Статус по факту")), "work_kind": _clean(r.get("Вид работы")),
|
||||
"work_kind": _clean(r.get("Вид работы")),
|
||||
"done_text": _clean(r.get("Что сделано по факту (внутр.)")), "rest_text": _clean(r.get("Что осталось (внутр.)")),
|
||||
"owner_note": _clean(r.get("Комментарий актуализации"))})
|
||||
notes += 1
|
||||
@ -56,7 +56,7 @@ def _import(conn, folder: Path) -> dict:
|
||||
new_keys[f"new:{i}"] = real
|
||||
csv_keys.append(real)
|
||||
_note(conn, key, {
|
||||
"fact_status": _clean(r.get("Статус по факту")), "work_kind": _clean(r.get("Вид работы")),
|
||||
"work_kind": _clean(r.get("Вид работы")),
|
||||
"rest_text": _clean(r.get("Основание (внутр.)")),
|
||||
"owner_note": None if real else _clean(r.get("Тема"))})
|
||||
notes += 1
|
||||
|
||||
@ -26,10 +26,10 @@ fmt_agent = core.fmt_agent
|
||||
def fmt_issues(issues) -> str:
|
||||
if not issues:
|
||||
return "задач нет"
|
||||
lines = ["ключ | тема | статус | срок | исполнитель | спринт | статус по факту"]
|
||||
lines = ["ключ | тема | статус | срок | исполнитель | спринт"]
|
||||
for i in issues:
|
||||
lines.append(" | ".join([i["key"], core.cut(i.get("summary")), core.value(i.get("status")), core.value(i.get("duedate")),
|
||||
core.value(i.get("assignee")), core.value(i.get("sprint")), core.value(i.get("fact_status"))]))
|
||||
core.value(i.get("assignee")), core.value(i.get("sprint"))]))
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
@ -73,7 +73,7 @@ def fmt_issue(d) -> str:
|
||||
links = [f" → {l['target_key']} ({l['link_type']})" for l in d.get("links_out", [])] + \
|
||||
[f" ← {l['source_key']} ({l['link_type']})" for l in d.get("links_in", [])]
|
||||
note = d.get("note") or {}
|
||||
notes = [f" {k}: {note[k]}" for k in ("fact_status", "work_kind", "done_text", "rest_text", "owner_note") if note.get(k)]
|
||||
notes = [f" {k}: {note[k]}" for k in ("work_kind", "done_text", "rest_text", "owner_note") if note.get(k)]
|
||||
cmds = [f" #{c['id']} {c['type']} {c['status']} {c.get('finished_at') or ''} {c.get('error') or ''}".rstrip() for c in d.get("commands", [])]
|
||||
comments = d.get("comments") or []
|
||||
if comments:
|
||||
@ -180,11 +180,11 @@ def fmt_batch(b) -> str:
|
||||
@mcp.tool()
|
||||
@core.guard
|
||||
@core.scoped(True)
|
||||
def issues(sprint: str = "", assignee: str = "", work_kind: str = "", fact_status: str = "", status: str = "",
|
||||
def issues(sprint: str = "", assignee: str = "", work_kind: str = "", status: str = "",
|
||||
label: str = "", epic: str = "", q: str = "", neighbor: str = "", limit: int = 50) -> str:
|
||||
"""Задачи зеркала по фильтрам (спринт, исполнитель, вид работы, статус по факту, статус Jira, метка, эпик,
|
||||
"""Задачи зеркала по фильтрам (спринт, исполнитель, вид работы, статус Jira, метка, эпик,
|
||||
подстрока темы/ключа, neighbor=1 — только соседние). Компактная таблица."""
|
||||
data = core.client().get("/issues", project=core.PROJECT, sprint=sprint, assignee=assignee, work_kind=work_kind, fact_status=fact_status,
|
||||
data = core.client().get("/issues", project=core.PROJECT, sprint=sprint, assignee=assignee, work_kind=work_kind,
|
||||
status=status, label=label, epic=epic, q=q, neighbor=neighbor, limit=limit)
|
||||
return fmt_issues(data["issues"])
|
||||
|
||||
@ -297,17 +297,17 @@ def changes(hours: float | None = None, since: str = "", until: str = "", kinds:
|
||||
@mcp.tool()
|
||||
@core.guard
|
||||
@core.scoped(True)
|
||||
def notes_set(key: str, fact_status: str | None = None, work_kind: str | None = None, done_text: str | None = None,
|
||||
def notes_set(key: str, work_kind: str | None = None, done_text: str | None = None,
|
||||
rest_text: str | None = None, owner_note: str | None = None) -> str:
|
||||
"""Правка служебных заметок задачи (только переданные поля)."""
|
||||
fields = {k: v for k, v in dict(fact_status=fact_status, work_kind=work_kind, done_text=done_text,
|
||||
fields = {k: v for k, v in dict(work_kind=work_kind, done_text=done_text,
|
||||
rest_text=rest_text, owner_note=owner_note).items() if v is not None}
|
||||
fields["updated_by"] = core.AUTHOR
|
||||
if not is_temp(key): # новая задача пакета проекта — своя по построению
|
||||
if core.PROJECT not in (core.client().get(f"/issues/{key}").get("labels") or []):
|
||||
return f"{key} не в проекте {core.PROJECT}"
|
||||
note = core.client().put(f"/issues/{key}/note", fields)
|
||||
return "заметки " + key + ": " + "; ".join(f"{k}: {note.get(k)}" for k in ("fact_status", "work_kind", "done_text", "rest_text", "owner_note") if note.get(k))
|
||||
return "заметки " + key + ": " + "; ".join(f"{k}: {note.get(k)}" for k in ("work_kind", "done_text", "rest_text", "owner_note") if note.get(k))
|
||||
|
||||
|
||||
def _read_local(path) -> bytes | str:
|
||||
|
||||
@ -7,7 +7,7 @@ from ...core import db
|
||||
|
||||
ISSUE_COLUMNS = ["key", "id", "issue_type", "summary", "description", "status", "priority", "assignee", "reporter",
|
||||
"duedate", "planned_start", "planned_end", "epic_key", "labels", "sprint", "created", "jira_updated"]
|
||||
NOTE_FIELDS = ["fact_status", "work_kind", "done_text", "rest_text", "owner_note"]
|
||||
NOTE_FIELDS = ["work_kind", "done_text", "rest_text", "owner_note"]
|
||||
|
||||
def check_comments(issues) -> None:
|
||||
"""Форма поля comments у задач состояния (агент v10+); поле необязательно. Нарушение — ValueError с ключом и полем."""
|
||||
@ -54,7 +54,7 @@ def check_attachments(issues) -> None:
|
||||
|
||||
|
||||
_FILTERS = {"sprint": "i.sprint = ?", "assignee": "i.assignee = ?", "status": "i.status = ?", "epic": "i.epic_key = ?",
|
||||
"work_kind": "n.work_kind = ?", "fact_status": "n.fact_status = ?"}
|
||||
"work_kind": "n.work_kind = ?"}
|
||||
|
||||
|
||||
def apply_state(conn, issues, links, neighbor_keys=(), fetched_at=None, authoritative=True) -> dict:
|
||||
@ -155,11 +155,11 @@ def list_issues(conn, filters=None, limit=50, offset=0, closed_statuses=()) -> l
|
||||
where.append("(i.summary LIKE ? OR i.key LIKE ?)")
|
||||
args += [f"%{filters['q']}%", f"%{filters['q']}%"]
|
||||
if project:
|
||||
sql = (SCOPE_CTE + "SELECT i.*, s.nb AS scope_nb, n.fact_status, n.work_kind FROM issue i "
|
||||
sql = (SCOPE_CTE + "SELECT i.*, s.nb AS scope_nb, n.work_kind FROM issue i "
|
||||
"JOIN scope s ON s.key = i.key LEFT JOIN issue_note n ON n.key = i.key")
|
||||
args = [project] + args
|
||||
else:
|
||||
sql = "SELECT i.*, n.fact_status, n.work_kind FROM issue i LEFT JOIN issue_note n ON n.key = i.key"
|
||||
sql = "SELECT i.*, n.work_kind FROM issue i LEFT JOIN issue_note n ON n.key = i.key"
|
||||
sql = (sql
|
||||
+ (" WHERE " + " AND ".join(where) if where else "")
|
||||
+ " ORDER BY i.duedate IS NULL, i.duedate, i.key LIMIT ? OFFSET ?")
|
||||
@ -223,7 +223,7 @@ BOARD_FIELDS = ["key", "summary", "issue_type", "status", "priority", "assignee"
|
||||
|
||||
|
||||
def board(conn, base_jql, project: str | None = None) -> dict:
|
||||
"""Снимок зеркала для доски связей (GET /board): все задачи с заметками fact_status/work_kind, все связи и последний
|
||||
"""Снимок зеркала для доски связей (GET /board): все задачи с work_kind из заметок, все связи и последний
|
||||
успешный полный прогон (refresh по базовой JQL, команда done). Данные без преобразований — перевод в модель доски
|
||||
делает интерфейс. С project — свои задачи проекта и соседи (neighbor относительно проекта), связи — те, у которых
|
||||
source или target — своя задача; last_sync — как без проекта. Чтение в одной транзакции: задачи, связи и last_sync — из одного состояния базы."""
|
||||
@ -234,7 +234,7 @@ def board(conn, base_jql, project: str | None = None) -> dict:
|
||||
if project:
|
||||
cols = ", ".join("i." + c for c in BOARD_FIELDS if c != "neighbor")
|
||||
issues = [_issue_dict(r) for r in conn.execute(
|
||||
SCOPE_CTE + f"SELECT {cols}, s.nb AS neighbor, n.fact_status, n.work_kind FROM issue i "
|
||||
SCOPE_CTE + f"SELECT {cols}, s.nb AS neighbor, n.work_kind FROM issue i "
|
||||
"JOIN scope s ON s.key = i.key LEFT JOIN issue_note n ON n.key = i.key ORDER BY i.key", (project,))]
|
||||
links = [dict(r) for r in conn.execute(
|
||||
SCOPE_CTE + "SELECT source_key AS source, target_key AS target, link_type FROM issue_link "
|
||||
@ -242,7 +242,7 @@ def board(conn, base_jql, project: str | None = None) -> dict:
|
||||
"ORDER BY source_key, target_key, link_type", (project,))]
|
||||
else:
|
||||
issues = [_issue_dict(r) for r in conn.execute(
|
||||
f"SELECT {', '.join('i.' + c for c in BOARD_FIELDS)}, n.fact_status, n.work_kind "
|
||||
f"SELECT {', '.join('i.' + c for c in BOARD_FIELDS)}, n.work_kind "
|
||||
"FROM issue i LEFT JOIN issue_note n ON n.key = i.key ORDER BY i.key")]
|
||||
links = [dict(r) for r in conn.execute(
|
||||
"SELECT source_key AS source, target_key AS target, link_type FROM issue_link "
|
||||
|
||||
19
tests/fixtures/baseline/mcp_tools.expected.json
vendored
19
tests/fixtures/baseline/mcp_tools.expected.json
vendored
@ -1,6 +1,6 @@
|
||||
[
|
||||
{
|
||||
"description": "Задачи зеркала по фильтрам (спринт, исполнитель, вид работы, статус по факту, статус Jira, метка, эпик,\nподстрока темы/ключа, neighbor=1 — только соседние). Компактная таблица.",
|
||||
"description": "Задачи зеркала по фильтрам (спринт, исполнитель, вид работы, статус Jira, метка, эпик,\nподстрока темы/ключа, neighbor=1 — только соседние). Компактная таблица.",
|
||||
"inputSchema": {
|
||||
"properties": {
|
||||
"assignee": {
|
||||
@ -13,11 +13,6 @@
|
||||
"title": "Epic",
|
||||
"type": "string"
|
||||
},
|
||||
"fact_status": {
|
||||
"default": "",
|
||||
"title": "Fact Status",
|
||||
"type": "string"
|
||||
},
|
||||
"label": {
|
||||
"default": "",
|
||||
"title": "Label",
|
||||
@ -183,18 +178,6 @@
|
||||
"default": null,
|
||||
"title": "Done Text"
|
||||
},
|
||||
"fact_status": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"title": "Fact Status"
|
||||
},
|
||||
"key": {
|
||||
"title": "Key",
|
||||
"type": "string"
|
||||
|
||||
@ -112,8 +112,14 @@ def test_interface_routes(hub, add_project):
|
||||
hub["agent"].post("/agent/state", {"issues": [{"key": "A-1", "summary": "Т", "sprint": "Спринт 2", "labels": []},
|
||||
{"key": "A-2", "summary": "Д", "sprint": "Спринт 1", "labels": []}], "links": []})
|
||||
assert [i["key"] for i in c.get("/issues", sprint="Спринт 2")["issues"]] == ["A-1"]
|
||||
note = c.put("/issues/A-1/note", {"fact_status": "В работе", "updated_by": "owner"})
|
||||
assert note["fact_status"] == "В работе" and note["updated_by"] == "owner"
|
||||
assert [i["key"] for i in c.get("/issues", fact_status="В работе")["issues"]] == ["A-1", "A-2"] # параметр игнорируется
|
||||
assert all("fact_status" not in i for i in c.get("/issues")["issues"])
|
||||
with pytest.raises(HubError) as e: # статуса по факту нет: поле — неизвестное (fact-status-removal)
|
||||
c.put("/issues/A-1/note", {"fact_status": "В работе", "work_kind": "Аналитика", "updated_by": "owner"})
|
||||
assert e.value.status == 422 and "неизвестные поля заметки: fact_status" in e.value.detail
|
||||
assert c.get("/issues/A-1")["note"] is None # и work_kind не записан
|
||||
note = c.put("/issues/A-1/note", {"work_kind": "Аналитика", "updated_by": "owner"})
|
||||
assert note["work_kind"] == "Аналитика" and note["updated_by"] == "owner" and note.get("fact_status") is None
|
||||
with pytest.raises(HubError) as e:
|
||||
c.put("/issues/A-1/note", {"summary": "x"})
|
||||
assert e.value.status == 422
|
||||
|
||||
@ -27,7 +27,7 @@ def live(tmp_path, add_project):
|
||||
|
||||
def test_create_link_sprint_flow(live):
|
||||
agent, client = live
|
||||
client.put("/issues/new:60/note", {"fact_status": "Ожидание разработки", "work_kind": "разработка", "updated_by": "import"})
|
||||
client.put("/issues/new:60/note", {"rest_text": "Ожидание разработки", "work_kind": "разработка", "updated_by": "import"})
|
||||
b = client.post("/batches", {"title": "новая задача в спринт", "author": "claude", "commands": [
|
||||
{"type": "ensure_issue", "params": {"temp_key": "new:60", "issue_type": "task", "summary": "Новая", "duedate": "2026-10-23",
|
||||
"labels": ["AWPCA"]}},
|
||||
|
||||
@ -48,7 +48,7 @@ def test_import_notes_and_report(conn, tmp_path):
|
||||
rep = importer.import_package(conn, tmp_path)
|
||||
assert rep["notes"] == 6
|
||||
n = mirror.get_note(conn, "AURORA-1533")
|
||||
assert n["fact_status"] == "Ожидание разработки" and n["work_kind"] == "тестирование"
|
||||
assert n["work_kind"] == "тестирование" and n.get("fact_status") is None # колонка CSV пропускается (fact-status-removal)
|
||||
assert n["rest_text"] == "Прогнать на стенде" and n["owner_note"] == "Актуализация 10.10.2026. Порядок" and n["updated_by"] == "import"
|
||||
assert mirror.get_note(conn, "AURORA-1416")["done_text"] == "Сделан список"
|
||||
assert mirror.get_note(conn, "AURORA-1586")["rest_text"] == "решение 11.10"
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import inspect
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
@ -47,9 +48,9 @@ def test_tools_roundtrip(live):
|
||||
"assignee": "ivanov", "sprint": "Спринт 2", "labels": ["AWPCA"]}], "links": []})
|
||||
out = jmcp.issues(sprint="Спринт 2")
|
||||
assert "A-1" in out and "Тема раз" in out and "ivanov" in out
|
||||
assert "В работе" in jmcp.notes_set("A-1", fact_status="В работе")
|
||||
assert "Аналитика" in jmcp.notes_set("A-1", work_kind="Аналитика")
|
||||
detail = jmcp.issue("A-1")
|
||||
assert "fact_status: В работе" in detail and "Готово" in detail
|
||||
assert "work_kind: Аналитика" in detail and "Готово" in detail
|
||||
created = jmcp.batch_create("срок", [{"type": "ensure_fields", "params": {"key": "A-1", "duedate": "2026-10-24"}}])
|
||||
batch_id = int(created.split("#")[1].split()[0])
|
||||
assert "2 команд" in created and "мин" in created
|
||||
@ -73,11 +74,19 @@ def test_tools_roundtrip(live):
|
||||
assert jmcp.settings_set("tokens", '"x"').startswith("ошибка хаба 422")
|
||||
|
||||
|
||||
def test_no_fact_status_params():
|
||||
for tool in (jmcp.issues, jmcp.notes_set):
|
||||
assert "fact_status" not in inspect.signature(tool).parameters
|
||||
|
||||
|
||||
def test_formatters():
|
||||
assert jmcp.fmt_issues([]) == "задач нет"
|
||||
text = jmcp.fmt_issues([{"key": "A-1", "summary": "x" * 100, "status": "Готово", "duedate": None, "assignee": None,
|
||||
"sprint": None, "fact_status": None}])
|
||||
"sprint": None, "fact_status": "В работе"}])
|
||||
assert "x" * 79 + "…" in text and "x" * 80 not in text and "—" in text
|
||||
head, row = text.splitlines()
|
||||
assert head == "ключ | тема | статус | срок | исполнитель | спринт" # статуса по факту нет (fact-status-removal)
|
||||
assert len(row.split(" | ")) == 6 and "В работе" not in row
|
||||
assert "очередь" in jmcp.fmt_agent({"online": False, "agent_version": None, "current_command_id": None,
|
||||
"rate_limited_24h": 0, "redelivered": [], "warning": None, "last_seen_at": None,
|
||||
"settings_version": None})
|
||||
|
||||
@ -60,14 +60,16 @@ def test_partial_load_never_lowers_neighbor(conn):
|
||||
|
||||
|
||||
def test_note_survives_refresh_and_renames(conn):
|
||||
mirror.set_note(conn, "new:7", {"fact_status": "В работе", "work_kind": "разработка"}, "import")
|
||||
mirror.set_note(conn, "new:7", {"rest_text": "В работе", "work_kind": "разработка"}, "import")
|
||||
mirror.rename_note(conn, "new:7", "A-5")
|
||||
mirror.apply_state(conn, [issue("A-5")], [])
|
||||
got = mirror.get_issue(conn, "A-5")
|
||||
assert got["note"]["fact_status"] == "В работе" and got["note"]["updated_by"] == "import"
|
||||
assert got["note"]["rest_text"] == "В работе" and got["note"]["updated_by"] == "import"
|
||||
assert mirror.get_note(conn, "new:7") is None
|
||||
with pytest.raises(ValueError):
|
||||
mirror.set_note(conn, "A-5", {"summary": "нельзя"}, "x")
|
||||
with pytest.raises(ValueError, match="неизвестные поля заметки: fact_status"): # fact-status-removal
|
||||
mirror.set_note(conn, "A-5", {"fact_status": "В работе"}, "x")
|
||||
|
||||
|
||||
def test_list_issues_filters(conn):
|
||||
@ -86,9 +88,9 @@ def test_list_issues_filters(conn):
|
||||
|
||||
def test_rename_note_same_key_keeps_note(conn):
|
||||
mirror.apply_state(conn, [issue("A-1")], [])
|
||||
mirror.set_note(conn, "A-1", {"fact_status": "В работе"}, "x")
|
||||
mirror.set_note(conn, "A-1", {"rest_text": "В работе"}, "x")
|
||||
mirror.rename_note(conn, "A-1", "A-1")
|
||||
assert mirror.get_note(conn, "A-1")["fact_status"] == "В работе"
|
||||
assert mirror.get_note(conn, "A-1")["rest_text"] == "В работе"
|
||||
|
||||
|
||||
def test_rename_note_missing_is_noop(conn):
|
||||
@ -99,32 +101,32 @@ def test_rename_note_missing_is_noop(conn):
|
||||
|
||||
def test_set_note_partial_keeps_other_fields(conn):
|
||||
mirror.set_note(conn, "A-1", {"work_kind": "разработка", "done_text": "сделано"}, "x")
|
||||
got = mirror.set_note(conn, "A-1", {"fact_status": "Готово"}, "y")
|
||||
got = mirror.set_note(conn, "A-1", {"rest_text": "Готово"}, "y")
|
||||
assert got["work_kind"] == "разработка" and got["done_text"] == "сделано"
|
||||
assert got["fact_status"] == "Готово" and got["updated_by"] == "y"
|
||||
assert got["rest_text"] == "Готово" and got["updated_by"] == "y"
|
||||
|
||||
|
||||
def test_rename_note_merges_with_existing(conn):
|
||||
mirror.set_note(conn, "A-5", {"owner_note": "спросить ИБ", "work_kind": "Старое"}, "human")
|
||||
mirror.set_note(conn, "new:5", {"work_kind": "Аналитика", "fact_status": "В работе"}, "import")
|
||||
mirror.set_note(conn, "new:5", {"work_kind": "Аналитика", "rest_text": "В работе"}, "import")
|
||||
mirror.rename_note(conn, "new:5", "A-5")
|
||||
note = mirror.get_note(conn, "A-5")
|
||||
assert note["work_kind"] == "Аналитика" # непустое поле временной заметки побеждает
|
||||
assert note["owner_note"] == "спросить ИБ" # пустое у временной — остаётся под ключом
|
||||
assert note["fact_status"] == "В работе"
|
||||
assert note["rest_text"] == "В работе"
|
||||
assert note["updated_by"] == "import" # временная заметка записана позже
|
||||
assert mirror.get_note(conn, "new:5") is None
|
||||
|
||||
|
||||
def test_rename_note_keeps_newer_real_key_stamp(conn):
|
||||
mirror.set_note(conn, "new:6", {"fact_status": "В работе"}, "import")
|
||||
mirror.set_note(conn, "new:6", {"rest_text": "В работе"}, "import")
|
||||
mirror.set_note(conn, "A-6", {"owner_note": "позже"}, "human")
|
||||
conn.execute("UPDATE issue_note SET updated_at = '2020-01-01T00:00:00+00:00' WHERE key = 'new:6'")
|
||||
conn.execute("UPDATE issue_note SET updated_at = '2030-01-01T00:00:00+00:00' WHERE key = 'A-6'")
|
||||
mirror.rename_note(conn, "new:6", "A-6")
|
||||
note = mirror.get_note(conn, "A-6")
|
||||
assert note["updated_by"] == "human" and note["updated_at"] == "2030-01-01T00:00:00+00:00"
|
||||
assert note["fact_status"] == "В работе" and note["owner_note"] == "позже"
|
||||
assert note["rest_text"] == "В работе" and note["owner_note"] == "позже"
|
||||
|
||||
|
||||
def test_rename_note_empty_string_does_not_overwrite(conn):
|
||||
@ -160,9 +162,9 @@ def test_board_snapshot_raw_mirror_data(conn):
|
||||
assert [i["key"] for i in snap["issues"]] == ["AURORA-1351", "AURORA-1620", "AURORA-1621"]
|
||||
a = snap["issues"][1]
|
||||
assert set(a) == {"key", "summary", "issue_type", "status", "priority", "assignee", "sprint", "duedate",
|
||||
"planned_start", "planned_end", "epic_key", "labels", "neighbor", "fact_status", "work_kind"}
|
||||
"planned_start", "planned_end", "epic_key", "labels", "neighbor", "work_kind"}
|
||||
assert a["labels"] == ["kind-dev"] and a["epic_key"] == "AURORA-1351"
|
||||
assert a["work_kind"] == "разработка" and a["fact_status"] is None and a["neighbor"] is False
|
||||
assert a["work_kind"] == "разработка" and a["neighbor"] is False
|
||||
assert snap["issues"][2]["neighbor"] is True and snap["issues"][2]["work_kind"] is None
|
||||
assert snap["links"] == [{"source": "AURORA-1620", "target": "AURORA-1621", "link_type": "ASUPR_Блокер_Начала"}]
|
||||
assert snap["last_sync"] is None and snap["generated_at"]
|
||||
|
||||
@ -101,7 +101,7 @@ def test_backup_failure_does_not_block_claim(conn, caplog):
|
||||
|
||||
def test_temp_key_substitution_and_skip(conn, add_project):
|
||||
add_project(conn)
|
||||
mirror.set_note(conn, "new:60", {"fact_status": "В работе"}, "import")
|
||||
mirror.set_note(conn, "new:60", {"rest_text": "В работе"}, "import")
|
||||
queue.create_batch(conn, "создать", "claude", [
|
||||
{"type": "ensure_issue", "params": {"temp_key": "new:60", "issue_type": "task", "summary": "Новая"}},
|
||||
{"type": "ensure_link", "params": {"source": "new:60", "target": "A-1", "link_type": "связанные"}},
|
||||
@ -111,7 +111,7 @@ def test_temp_key_substitution_and_skip(conn, add_project):
|
||||
])
|
||||
c1 = queue.claim(conn, "jira")
|
||||
queue.report_result(conn, c1["id"], "done", result={"key": "A-90"})
|
||||
assert mirror.get_note(conn, "A-90")["fact_status"] == "В работе" and mirror.get_note(conn, "new:60") is None
|
||||
assert mirror.get_note(conn, "A-90")["rest_text"] == "В работе" and mirror.get_note(conn, "new:60") is None
|
||||
c2 = queue.claim(conn, "jira")
|
||||
assert c2["type"] == "ensure_link" and c2["params"]["source"] == "A-90"
|
||||
queue.report_result(conn, c2["id"], "done")
|
||||
@ -362,7 +362,7 @@ def test_report_for_queued_command_rejected(conn):
|
||||
|
||||
|
||||
def test_late_report_after_force_cancel_keeps_issue_key(conn):
|
||||
mirror.set_note(conn, "new:7", {"fact_status": "В работе"}, "import")
|
||||
mirror.set_note(conn, "new:7", {"rest_text": "В работе"}, "import")
|
||||
queue.create_batch(conn, "x", "claude", [
|
||||
{"type": "ensure_issue", "params": {"temp_key": "new:7", "issue_type": "task", "summary": "Тема"}}])
|
||||
c = queue.claim(conn, "jira")
|
||||
@ -370,7 +370,7 @@ def test_late_report_after_force_cancel_keeps_issue_key(conn):
|
||||
late = queue.report_result(conn, c["id"], "done", result={"key": "A-20", "created": True}, jira_calls=3)
|
||||
assert late["status"] == "cancelled" and late["result"] == {"key": "A-20", "created": True} and late["jira_calls"] == 3
|
||||
assert late["finished_at"] == cancelled["finished_at"]
|
||||
assert mirror.get_note(conn, "A-20")["fact_status"] == "В работе" and mirror.get_note(conn, "new:7") is None
|
||||
assert mirror.get_note(conn, "A-20")["rest_text"] == "В работе" and mirror.get_note(conn, "new:7") is None
|
||||
again = queue.report_result(conn, c["id"], "done", result={"key": "A-21"})
|
||||
assert again["result"] == {"key": "A-20", "created": True} # второй поздний отчёт ничего не меняет
|
||||
|
||||
@ -399,7 +399,7 @@ def test_error_is_trimmed_to_1500(conn):
|
||||
|
||||
|
||||
def test_late_done_after_delivery_limit_keeps_issue_key(conn):
|
||||
mirror.set_note(conn, "new:8", {"fact_status": "План"}, "import")
|
||||
mirror.set_note(conn, "new:8", {"rest_text": "План"}, "import")
|
||||
queue.create_batch(conn, "x", "claude", [
|
||||
{"type": "ensure_issue", "params": {"temp_key": "new:8", "issue_type": "task", "summary": "Тема"}}])
|
||||
c = queue.claim(conn, "jira")
|
||||
@ -407,7 +407,7 @@ def test_late_done_after_delivery_limit_keeps_issue_key(conn):
|
||||
late = queue.report_result(conn, c["id"], "done", result={"key": "A-30", "created": True}, jira_calls=2)
|
||||
assert late["status"] == "failed" and late["result"] == {"key": "A-30", "created": True}
|
||||
assert late["error"] == "агент не отчитался после 3 выдач"
|
||||
assert mirror.get_note(conn, "A-30")["fact_status"] == "План" and mirror.get_note(conn, "new:8") is None
|
||||
assert mirror.get_note(conn, "A-30")["rest_text"] == "План" and mirror.get_note(conn, "new:8") is None
|
||||
again = queue.report_result(conn, c["id"], "done", result={"key": "A-31"})
|
||||
assert again["result"] == {"key": "A-30", "created": True} # повтор ничего не меняет
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user