Спринты и бэклог в модуле jira (#108) #109
@ -78,7 +78,7 @@
|
||||
|
||||
- `Module(name, schema, settings_defaults, settings_obsolete, agent_keys, agent_min_version_key, validate_setting, settings_view, on_init, commands, routes, tick, file_refs, project_fields, project_view, on_project_created, on_project_updated, mcp_server, cli_register, cli_run)` — всё необязательное, кроме `name`. Модуль с `commands` — «вид агента»: у него свой агент, токен, `agent_status` и релиз.
|
||||
- Поля проекта: ядро знает только `label`, `name`, `created_at`. Остальное объявляет модуль: `ProjectField(name, validate, editable)`; значения для `GET /projects` (в т.ч. вычисляемые) отдаёт `project_view(conn, labels)`; `on_project_created(conn, project, fields, author)` и `on_project_updated(...)` пишут свои поля и возвращают пакеты. `POST /projects` принимает поля всех модулей (неизвестное — 422), `PATCH /projects/{label}` — только изменяемые (`editable`). `registry.check` падает при совпадении имени поля у двух модулей или с полем ядра.
|
||||
- `CommandHooks` — что модуль знает о своих типах команд: `types`, `read_types` (не пишут во внешнюю систему: без хвоста и копии базы), `validate`, `check_batch`, `tail`, `pause`, `deliver_params`, `temp_definer`, `temp_keys_used`, `substitute`, `drop_unresolved`, `retry_params`, `check_confirmed`, `on_claim`, `on_failed`, `check_result`, `on_result`, `after_result`, `on_late_result` и два необязательных — `before_result`, `abort_result`. Очередь вызывает их по типу команды через `registry.module_of`.
|
||||
- `CommandHooks` — что модуль знает о своих типах команд: `types`, `read_types` (не пишут во внешнюю систему: без хвоста и копии базы), `validate`, `check_batch`, `tail(conn, cmds, project)`, `pause`, `deliver_params`, `temp_definer`, `temp_keys_used`, `substitute`, `drop_unresolved`, `retry_params`, `check_confirmed`, `on_claim`, `on_failed`, `check_result`, `on_result`, `after_result`, `on_late_result` и два необязательных — `before_result`, `abort_result`. Очередь вызывает их по типу команды через `registry.module_of`.
|
||||
- `before_result(conn, cmd, status, result, files_dir)` ядро вызывает в `report_result` до транзакции отчёта, только для выданной (`claimed`) команды. Нужен, чтобы тяжёлая подготовка (сборка копии вики, запись файлов) шла вне `BEGIN IMMEDIATE` и не держала блокировку базы, в которой ждут `claim` другого агента и веб (рулинг R-6). `ValueError` из него ядро превращает в `RuntimeError` (500, агент повторит отчёт), чтобы не спутать с неверным статусом отчёта.
|
||||
- `abort_result(conn, cmd)` ядро вызывает, если отчёт не дошёл до `after_result` (поздний или идемпотентный выход, исключение в транзакции): модуль снимает подготовку `before_result` и её блокировку. При успехе подготовку снимает `after_result`.
|
||||
|
||||
|
||||
@ -307,7 +307,7 @@ make web
|
||||
целиком: `{"task": {…}, "epic": {…}, "bug": {"name": "Ошибка", "id": null}}`.
|
||||
|
||||
Ключ в параметре может быть временным `new:<n>`: команда выдаётся после `ensure_issue` с тем же `temp_key` из того же
|
||||
пакета. В хвост пакета хаб сам добавляет `refresh` по затронутым ключам. Пример пакета (`POST /batches`):
|
||||
пакета. В хвост пакета хаб сам добавляет `refresh`: пакет с `ensure_sprint_meta`/`ensure_sprint_state` получает хвост по JQL проекта пакета; иначе — по затронутым ключам. Пример пакета (`POST /batches`):
|
||||
|
||||
```json
|
||||
{"title": "1538 в спринт 2", "author": "claude", "commands": [
|
||||
|
||||
@ -15,7 +15,7 @@ class CommandHooks:
|
||||
read_types: frozenset[str] # не пишут во внешнюю систему: без хвоста и копии базы
|
||||
validate: Callable[[list], list[dict]] # CommandError
|
||||
check_batch: Callable[[Connection, list[dict], str | None, Path | None], list[dict]] # project, files_dir
|
||||
tail: Callable[[list[dict]], dict | None] # хвостовая команда пишущего пакета
|
||||
tail: Callable[[Connection, list[dict], str | None], dict | None] # (conn, cmds, проект пакета): хвостовая команда пишущего пакета
|
||||
pause: Callable[[Connection, str], int] # пауза типа, с
|
||||
deliver_params: Callable[[Connection, dict, Path | None], dict]
|
||||
temp_definer: tuple[str, str, str] | None # (тип, поле params с временным ключом, поле result с ключом)
|
||||
|
||||
@ -76,7 +76,7 @@ def create_batch(conn, title, author, cmds, files_dir=None, project=None) -> dic
|
||||
h = _batch_hooks(cmds)
|
||||
cmds = h.validate(cmds)
|
||||
cmds = h.check_batch(conn, cmds, project, files_dir)
|
||||
tail = h.tail(cmds)
|
||||
tail = h.tail(conn, cmds, project)
|
||||
if tail is not None:
|
||||
cmds.append(tail)
|
||||
return insert_batch(conn, title, author, cmds, project)
|
||||
|
||||
@ -159,8 +159,12 @@ def check_batch(conn, cmds, project, files_dir) -> list[dict]:
|
||||
return cmds
|
||||
|
||||
|
||||
def tail(cmds) -> dict | None:
|
||||
"""Хвостовой refresh пишущего пакета: ключи из всех команд, чтобы зеркало увидело результат."""
|
||||
def tail(conn, cmds, project) -> dict | None:
|
||||
"""Хвостовой refresh пишущего пакета: ключи из всех команд, чтобы зеркало увидело результат. Пакет с командами
|
||||
спринта — refresh по JQL проекта пакета: у них нет ключей, а перенесённые при закрытии задачи заранее
|
||||
неизвестны (design #13)."""
|
||||
if project and any(c["type"] in commands.SPRINT_TYPES for c in cmds):
|
||||
return {"type": "refresh", "params": {"jql": jira_projects.project_jql(conn, project)}}
|
||||
keys = []
|
||||
for c in cmds:
|
||||
keys += [k for k in commands.referenced_keys(c) if k not in keys]
|
||||
|
||||
@ -24,6 +24,12 @@ def base_jql(conn) -> str | None:
|
||||
return f"project = {key} AND labels in ({', '.join(f'"{x}"' for x in found)})"
|
||||
|
||||
|
||||
def project_jql(conn, label: str) -> str:
|
||||
"""JQL одного проекта-метки — в формате base_jql (тот же project_key и кавычки)."""
|
||||
key = settings.get(conn, "project_key") or "AURORA"
|
||||
return f'project = {key} AND labels in ("{label}")'
|
||||
|
||||
|
||||
def create(conn, label: str, name: str, board_id: int | None = None) -> dict:
|
||||
"""Проект с доской: проект ядра и строка jira_project (прямой путь без POST /projects: init, миграция, тесты).
|
||||
Неверная доска — ProjectError("board_id", …) до записи."""
|
||||
|
||||
@ -94,7 +94,7 @@ def after_result(conn, cmd, status) -> None:
|
||||
# wiki_sync — sync.py (хуки before/abort/after только у него), wiki_fetch — attachments.py
|
||||
HOOKS = CommandHooks(
|
||||
types=frozenset(TYPES), read_types=frozenset(TYPES), validate=validate, check_batch=check_batch,
|
||||
tail=lambda cmds: None, pause=lambda conn, ctype: 0, deliver_params=lambda conn, cmd, files_dir: cmd["params"],
|
||||
tail=lambda conn, cmds, project: None, pause=lambda conn, ctype: 0, deliver_params=lambda conn, cmd, files_dir: cmd["params"],
|
||||
temp_definer=None, temp_keys_used=lambda cmd: [], substitute=lambda ctype, params, mapping: params,
|
||||
drop_unresolved=lambda cmd, unresolved: unresolved, retry_params=lambda cmd: cmd["params"],
|
||||
check_confirmed=lambda conn, cmd: (False, None), on_claim=sync.on_claim, on_failed=on_failed,
|
||||
|
||||
@ -56,7 +56,7 @@ def _stub_hooks() -> CommandHooks:
|
||||
noop = lambda *a, **k: None # noqa: E731
|
||||
return CommandHooks(
|
||||
types=frozenset({"stub_cmd"}), read_types=frozenset({"stub_cmd"}), validate=lambda cmds: cmds,
|
||||
check_batch=lambda conn, cmds, project, files_dir: cmds, tail=lambda cmds: None, pause=lambda conn, t: 0,
|
||||
check_batch=lambda conn, cmds, project, files_dir: cmds, tail=lambda conn, cmds, project: None, pause=lambda conn, t: 0,
|
||||
deliver_params=lambda conn, cmd, files_dir: cmd["params"], temp_definer=None, temp_keys_used=lambda c: [],
|
||||
substitute=lambda t, p, mapping: p, drop_unresolved=lambda c, u: u, retry_params=lambda c: c["params"],
|
||||
check_confirmed=lambda conn, c: (True, None), on_claim=noop, on_failed=noop,
|
||||
|
||||
@ -148,3 +148,11 @@ def test_ensure_legacy_skips_case_variant_and_bad_board(open_db):
|
||||
c2 = open_db()
|
||||
settings.ensure_defaults(c2)
|
||||
assert jira_projects.ensure_legacy(c2, "oops") is True and api.project_rows(c2, ["AWPCA"])[0]["board_id"] is None
|
||||
|
||||
|
||||
def test_project_jql(open_db, add_project):
|
||||
conn = open_db(); settings.ensure_defaults(conn)
|
||||
add_project(conn)
|
||||
assert jira_projects.project_jql(conn, "AWPCA") == 'project = AURORA AND labels in ("AWPCA")' == jira_projects.base_jql(conn)
|
||||
add_project(conn, "C", board_id=None)
|
||||
assert jira_projects.project_jql(conn, "C") == 'project = AURORA AND labels in ("C")'
|
||||
|
||||
@ -1265,3 +1265,29 @@ def test_estimate_sums_module_pauses_replaces_commands_estimate(conn):
|
||||
через pacing -> hooks.pause -> queue.estimate."""
|
||||
settings.set_(conn, "pacing", {"ensure_fields": 120, "ensure_comment": 30, "refresh": 2})
|
||||
assert queue.estimate(conn, ["ensure_fields", "ensure_comment", "refresh"]) == 152
|
||||
|
||||
|
||||
def test_sprint_batch_tail_is_project_jql(conn, add_project): # design #13, R-10
|
||||
add_project(conn); add_project(conn, "C", board_id=None)
|
||||
mirror.apply_state(conn, [{"key": "X", "labels": ["AWPCA"]}, {"key": "Y", "labels": ["AWPCA"]}], [])
|
||||
b = queue.create_batch(conn, "x", "claude", [meta(), {"type": "ensure_rank", "params": {"key": "X", "before": "Y"}}], project="AWPCA")
|
||||
cmds = queue.batch_view(conn, b["id"])["commands"]
|
||||
assert [c["type"] for c in cmds] == ["ensure_sprint_meta", "ensure_rank", "refresh"]
|
||||
assert cmds[-1]["params"] == {"jql": 'project = AURORA AND labels in ("AWPCA")'}
|
||||
|
||||
|
||||
def test_rank_batch_tail_by_keys(conn, add_project):
|
||||
add_project(conn)
|
||||
mirror.apply_state(conn, [{"key": "X", "labels": ["AWPCA"]}], [])
|
||||
b = queue.create_batch(conn, "x", "claude", [{"type": "ensure_rank", "params": {"key": "X", "after": "Z"}}], project="AWPCA")
|
||||
assert queue.batch_view(conn, b["id"])["commands"][-1]["params"] == {"keys": ["X", "Z"]}
|
||||
|
||||
|
||||
def test_retry_sprint_meta_gets_jql_tail(conn, add_project):
|
||||
add_project(conn)
|
||||
b = queue.create_batch(conn, "x", "claude", [meta()], project="AWPCA")
|
||||
cid = queue.batch_view(conn, b["id"])["commands"][0]["id"]
|
||||
r = queue.retry_command(conn, cid, "claude")
|
||||
cmds = queue.batch_view(conn, r["id"])["commands"]
|
||||
assert [c["type"] for c in cmds] == ["ensure_sprint_meta", "refresh"]
|
||||
assert cmds[-1]["params"] == {"jql": 'project = AURORA AND labels in ("AWPCA")'}
|
||||
|
||||
@ -21,7 +21,7 @@ def make_stub(events, refs=()):
|
||||
hooks = CommandHooks(
|
||||
types=frozenset({"w", "r"}), read_types=frozenset({"r"}), validate=_validate,
|
||||
check_batch=lambda conn, cmds, project, files_dir: cmds,
|
||||
tail=lambda cmds: {"type": "r", "params": {}} if any(c["type"] == "w" for c in cmds) else None,
|
||||
tail=lambda conn, cmds, project: {"type": "r", "params": {}} if any(c["type"] == "w" for c in cmds) else None,
|
||||
pause=lambda conn, ctype: 7, deliver_params=lambda conn, cmd, files_dir: cmd["params"],
|
||||
temp_definer=None, temp_keys_used=lambda cmd: [], substitute=lambda t, p, m: p,
|
||||
drop_unresolved=lambda cmd, unresolved: unresolved, retry_params=lambda cmd: cmd["params"],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user