Агент v16: оценка через estimation доски (#53)

ensure_fields со story_points и board_id читает и пишет оценку через
/rest/agile/1.0/issue/{key}/estimation?boardId=… — поле оценки в Jira банка не на экране
редактирования, PUT /issue его отклоняет. Остальные поля — как раньше; без board_id — путь v15.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-25 21:49:05 +04:00
parent 3350d0066a
commit cb8203f0e1
9 changed files with 102 additions and 15 deletions

View File

@ -11,7 +11,7 @@ Jira и хаба. Спека: `../sdlc-hub/openspec/changes/bridge-core/`, пл
| HTTP | `Http`, `HttpError`, `JiraClient`, `HubClient`, `multipart_file`, `wire_filename` | запросы; `Http.send` — запрос с готовым телом (JSON-вызов `call` идёт через него); `JiraClient` считает вызовы, повторяет 429/503 по `Retry-After` (до 5 запросов, не более 4 ожиданий), хранит время последнего вызова; `JiraClient.upload` — вложение `multipart/form-data` (таймаут не меньше 120 с); `JiraClient.download` (через `Http.fetch_bytes`) — содержимое вложения байтами, не больше предела (`TooBig`), перенаправление на другой адрес — `RedirectAway` без перехода (`_SameOriginRedirect`); `HubClient.file` — файл вложения у хаба (base64 в JSON), `HubClient.upload_file` — скачанный файл хабу (`POST /agent/files`) | `urllib` | | HTTP | `Http`, `HttpError`, `JiraClient`, `HubClient`, `multipart_file`, `wire_filename` | запросы; `Http.send` — запрос с готовым телом (JSON-вызов `call` идёт через него); `JiraClient` считает вызовы, повторяет 429/503 по `Retry-After` (до 5 запросов, не более 4 ожиданий), хранит время последнего вызова; `JiraClient.upload` — вложение `multipart/form-data` (таймаут не меньше 120 с); `JiraClient.download` (через `Http.fetch_bytes`) — содержимое вложения байтами, не больше предела (`TooBig`), перенаправление на другой адрес — `RedirectAway` без перехода (`_SameOriginRedirect`); `HubClient.file` — файл вложения у хаба (base64 в JSON), `HubClient.upload_file` — скачанный файл хабу (`POST /agent/files`) | `urllib` |
| Папка | `FolderTransport` | транспорт `folder:` к хабу через общую папку: `req/<id>.json` → реле хаба → `resp/<id>.json`; ошибки как у `Http`; `HubClient` делегирует ему `call` (спека `sdlc-hub/openspec/changes/folder-transport/`) | `os`, `uuid` | | Папка | `FolderTransport` | транспорт `folder:` к хабу через общую папку: `req/<id>.json` → реле хаба → `resp/<id>.json`; ошибки как у `Http`; `HubClient` делегирует ему `call` (спека `sdlc-hub/openspec/changes/folder-transport/`) | `os`, `uuid` |
| Журнал | `Journal`, `_parse_at` | дозапись `journal.jsonl`: `started` → `finished` → `acked`; восстановление; `prune` (из CLI — под блокировкой журнала) | файловая система | | Журнал | `Journal`, `_parse_at` | дозапись `journal.jsonl`: `started` → `finished` → `acked`; восстановление; `prune` (из CLI — под блокировкой журнала) | файловая система |
| Исполнитель | `Executor` и `do_<тип>` | по одному методу на тип команды: читает Jira, пишет только разницу; `refresh` читает и отправляет состояние в хаб, затем (v13, параметр `history`) журнал изменений Jira — `refresh_history`: изменившиеся с `history.since` задачи этого прогона и стартовая выборка `history.jql`, `search` с `expand=changelog`, `POST /agent/history` пачками по 500; ответ не 2xx Jira или хаба — `history.error`/`backfill_error`, `refresh` остаётся `done`; `ensure_attachment` берёт файл вложения у хаба (`hub_file`: sha256 и размер, повреждение — `HubError` с повтором); `fetch_attachment` (v14) читает вложение в Jira, качает содержимое по адресу из `base_url` (предел `MAX_ATTACHMENT`, 10 МБ) и отдаёт хабу; в состоянии `refresh` у задачи — `attachments` (список без содержимого, `attachments_of`); доски — из параметров команд (`ensure_sprint.board_id`, `refresh.boards`, тип доски — `board_type`), `board_id` настроек — запасной; v15: поля ранга, спринта и оценки — `jira_fields` по `field_list` (`GET /field` один раз на команду; перекрытие `field_ids`), доски — `board_state` (спринты через `all_sprints`), новые `do_ensure_sprint_meta`, `do_ensure_sprint_state`, `do_ensure_rank`, `story_points` в `do_ensure_fields` | `JiraClient`, `HubClient` (`refresh` и файл в `ensure_attachment`), настройки хаба | | Исполнитель | `Executor` и `do_<тип>` | по одному методу на тип команды: читает Jira, пишет только разницу; `refresh` читает и отправляет состояние в хаб, затем (v13, параметр `history`) журнал изменений Jira — `refresh_history`: изменившиеся с `history.since` задачи этого прогона и стартовая выборка `history.jql`, `search` с `expand=changelog`, `POST /agent/history` пачками по 500; ответ не 2xx Jira или хаба — `history.error`/`backfill_error`, `refresh` остаётся `done`; `ensure_attachment` берёт файл вложения у хаба (`hub_file`: sha256 и размер, повреждение — `HubError` с повтором); `fetch_attachment` (v14) читает вложение в Jira, качает содержимое по адресу из `base_url` (предел `MAX_ATTACHMENT`, 10 МБ) и отдаёт хабу; в состоянии `refresh` у задачи — `attachments` (список без содержимого, `attachments_of`); доски — из параметров команд (`ensure_sprint.board_id`, `refresh.boards`, тип доски — `board_type`), `board_id` настроек — запасной; v15: поля ранга, спринта и оценки — `jira_fields` по `field_list` (`GET /field` один раз на команду; перекрытие `field_ids`), доски — `board_state` (спринты через `all_sprints`), новые `do_ensure_sprint_meta`, `do_ensure_sprint_state`, `do_ensure_rank`, `story_points` в `do_ensure_fields`; v16: оценка с `board_id` — через `/rest/agile/1.0/issue/{key}/estimation` доски, остальные поля — `ensure_issue_fields` | `JiraClient`, `HubClient` (`refresh` и файл в `ensure_attachment`), настройки хаба |
| Самообновление | `read_update_state`, `write_update_state`, `Updater`, `launcher_version` | `update.json`; после `claim` без команды — проверка `agent_release`, скачивание, sha256, компиляция, `selftest`, `staged`; подтверждение `ok` новой версией; без запускателя — только подсказка в лог | `HubClient`, `subprocess` | | Самообновление | `read_update_state`, `write_update_state`, `Updater`, `launcher_version` | `update.json`; после `claim` без команды — проверка `agent_release`, скачивание, sha256, компиляция, `selftest`, `staged`; подтверждение `ok` новой версией; без запускателя — только подсказка в лог | `HubClient`, `subprocess` |
| Цикл | `Runner` | claim, пауза до команды, журнал, выполнение, отчёт до подтверждения, восстановление при старте; после `claim` без команды спрашивает `Updater`, готова версия — выход с `EXIT_UPDATE` (75) | `HubClient`, `JiraClient`, `Journal`, `Executor` | | Цикл | `Runner` | claim, пауза до команды, журнал, выполнение, отчёт до подтверждения, восстановление при старте; после `claim` без команды спрашивает `Updater`, готова версия — выход с `EXIT_UPDATE` (75) | `HubClient`, `JiraClient`, `Journal`, `Executor` |
| CLI | `cmd_run`, `cmd_check`, `cmd_selftest`, `cmd_prune`, `main` | команды `run`, `check`, `selftest`, `prune` (`run` и `prune` берут `acquire_lock` на `<журнал>.lock`); обработка сигналов | `Runner`, `Journal` | | CLI | `cmd_run`, `cmd_check`, `cmd_selftest`, `cmd_prune`, `main` | команды `run`, `check`, `selftest`, `prune` (`run` и `prune` берут `acquire_lock` на `<журнал>.lock`); обработка сигналов | `Runner`, `Journal` |

View File

@ -48,7 +48,7 @@
## Первая подсистема ## Первая подсистема
Исполнитель команд и цикл агента (спека `agent/spec.md`, `commands/spec.md`) — реализованы целиком: все типы команд, Исполнитель команд и цикл агента (спека `agent/spec.md`, `commands/spec.md`) — реализованы целиком: все типы команд,
журнал, восстановление после перезапуска, CLI `run|check|selftest|prune`, самообновление через `run.py`. журнал, восстановление после перезапуска, CLI `run|check|selftest|prune`, самообновление через `run.py`.
Последний change — `../sdlc-hub/openspec/changes/sprints-backlog/` (агент v15: спринты, ранг, оценки; решения агента — `openspec/changes/agent-v15-sprints/design.md`), план — `docs/superpowers/plans/2026-10-24-sprints-backlog-agent.md`. Последний change — `../sdlc-hub/openspec/changes/sp-estimation-board/` (агент v16: оценка через доску; решения агента — `openspec/changes/agent-v16-sp-estimation/design.md`), план — `docs/superpowers/plans/2026-10-25-sp-estimation-agent.md`.
План и спека живут в `sdlc-hub`: `openspec/changes/bridge-core/` и `docs/superpowers/plans/2026-10-11-bridge-core-hub.md`; план агента лежит здесь: `docs/superpowers/plans/2026-10-11-bridge-core-agent.md`. План и спека живут в `sdlc-hub`: `openspec/changes/bridge-core/` и `docs/superpowers/plans/2026-10-11-bridge-core-hub.md`; план агента лежит здесь: `docs/superpowers/plans/2026-10-11-bridge-core-agent.md`.
## Выкатка новой версии ## Выкатка новой версии

View File

@ -168,7 +168,7 @@ python3 agent.py prune
| Тип | Что делает | | Тип | Что делает |
|---|---| |---|---|
| `ensure_issue` | ищет задачу своей темы (`reporter = currentUser()`, за 7 дней), иначе создаёт с типом из `issue_types` хаба (`task`, `epic`, `bug`; нет настройки типа — `failed` «нет настройки issue_types.<тип>» без вызова Jira); возвращает `key` | | `ensure_issue` | ищет задачу своей темы (`reporter = currentUser()`, за 7 дней), иначе создаёт с типом из `issue_types` хаба (`task`, `epic`, `bug`; нет настройки типа — `failed` «нет настройки issue_types.<тип>» без вызова Jira); возвращает `key` |
| `ensure_fields` | читает задачу и пишет только отличающиеся поля; с v15 — `story_points` (оценка; поле ищется по `GET /field`, перекрытие — `field_ids.story_points`; не найдено — `failed`, `null` — очистка) | | `ensure_fields` | читает задачу и пишет только отличающиеся поля; с v15 — `story_points` (оценка; поле ищется по `GET /field`, перекрытие — `field_ids.story_points`; не найдено — `failed`, `null` — очистка); с v16 при `board_id` в команде (хаб ставит доску проекта) оценка читается и пишется через `/rest/agile/1.0/issue/{key}/estimation?boardId=…` — как в бэклоге Scrum, без экрана редактирования; `GET /issue` — только если есть другие поля |
| `ensure_comment` | комментарий с маркером в начале: обновляет, если текст изменился, иначе создаёт | | `ensure_comment` | комментарий с маркером в начале: обновляет, если текст изменился, иначе создаёт |
| `ensure_status` | идёт по пути статусов, на каждом шаге выбирает переход; нет перехода — `failed` со списком доступных | | `ensure_status` | идёт по пути статусов, на каждом шаге выбирает переход; нет перехода — `failed` со списком доступных |
| `ensure_link` | создаёт связь; конфликт без `replace` — `failed`; с `replace` сначала удаляет лишнее | | `ensure_link` | создаёт связь; конфликт без `replace` — `failed`; с `replace` сначала удаляет лишнее |
@ -215,6 +215,8 @@ python3 agent.py prune
Версия 15 — спринты, ранг и оценки: команды `ensure_sprint_meta`, `ensure_sprint_state`, `ensure_rank`, `story_points` Версия 15 — спринты, ранг и оценки: команды `ensure_sprint_meta`, `ensure_sprint_state`, `ensure_rank`, `story_points`
в `ensure_fields`; в `refresh` — `rank`, `story_points`, `sprint_ids` и данные досок `boards`. Поля ищутся по `GET /field` в `ensure_fields`; в `refresh` — `rank`, `story_points`, `sprint_ids` и данные досок `boards`. Поля ищутся по `GET /field`
(ранг и спринт — по схеме `schema.custom`, оценка — по имени «Story Points»), перекрытие — `field_ids.rank`/`sprint`/`story_points`. (ранг и спринт — по схеме `schema.custom`, оценка — по имени «Story Points»), перекрытие — `field_ids.rank`/`sprint`/`story_points`.
Версия 16 — оценка из `ensure_fields` с `board_id` через `/rest/agile/1.0/issue/{key}/estimation` доски: в Jira банка поле
оценки не на экране редактирования, `PUT /issue` его отклоняет (#53).
Перед выкаткой проверить поля на Jira банка: имена и id могут отличаться. Агент v14 на новые типы отвечает Перед выкаткой проверить поля на Jira банка: имена и id могут отличаться. Агент v14 на новые типы отвечает
«неизвестный тип команды»; `min_agent_version` поднимает владелец после выкатки. «неизвестный тип команды»; `min_agent_version` поднимает владелец после выкатки.
Если хаб назначил `min_agent_version` выше, он пишет в `agent_status.warning`: «обновите агента»; команды Если хаб назначил `min_agent_version` выше, он пишет в `agent_status.warning`: «обновите агента»; команды

View File

@ -33,7 +33,7 @@ import uuid
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Any, Callable, Dict, List, Optional, Tuple from typing import Any, Callable, Dict, List, Optional, Tuple
AGENT_VERSION = 15 AGENT_VERSION = 16
EXIT_LOCKED = 73 # «журнал занят другим процессом»: для run.py это не сбой версии (design 8) EXIT_LOCKED = 73 # «журнал занят другим процессом»: для run.py это не сбой версии (design 8)
EXIT_UPDATE = 75 # «обновление готово»: run.py подменит agent.py на agent.py.new EXIT_UPDATE = 75 # «обновление готово»: run.py подменит agent.py на agent.py.new
HERE = os.path.dirname(os.path.abspath(__file__)) HERE = os.path.dirname(os.path.abspath(__file__))
@ -874,14 +874,30 @@ class Executor:
def do_ensure_fields(self, p: dict, cmd: dict) -> dict: def do_ensure_fields(self, p: dict, cmd: dict) -> dict:
key = p["key"] key = p["key"]
sp_fid = None sp_fid = None
# С доской оценка — через estimation доски (v16): поля может не быть на экране редактирования (#53)
by_board = "story_points" in p and bool(p.get("board_id"))
if "story_points" in p: if "story_points" in p:
want_sp = p["story_points"]
if want_sp is not None and (isinstance(want_sp, bool) or not isinstance(want_sp, (int, float)) or not want_sp >= 0):
return self._out("failed", error="story_points — число ≥ 0 или null")
if "story_points" in p and not by_board:
# Настроенный id берётся без GET /field (как в refresh он не нужен); иначе — поиск в jira_fields. # Настроенный id берётся без GET /field (как в refresh он не нужен); иначе — поиск в jira_fields.
sp_fid = (self.s.get("field_ids") or {}).get("story_points") or self.jira_fields()["story_points"] sp_fid = (self.s.get("field_ids") or {}).get("story_points") or self.jira_fields()["story_points"]
if not sp_fid: if not sp_fid:
raise ConfigGap("поле оценки не найдено в Jira: задайте field_ids.story_points") raise ConfigGap("поле оценки не найдено в Jira: задайте field_ids.story_points")
want_sp = p["story_points"] changed: List[str] = []
if want_sp is not None and (isinstance(want_sp, bool) or not isinstance(want_sp, (int, float)) or not want_sp >= 0): if set(p) - {"key", "board_id", "story_points"} or not by_board:
return self._out("failed", error="story_points — число ≥ 0 или null") changed = self.ensure_issue_fields(key, p, sp_fid)
if by_board:
path = AGILE + "/issue/%s/estimation" % key
if not _same_points(self.jira.get(path, boardId=p["board_id"]).get("value"), p["story_points"]):
self.jira.call("PUT", path, {"value": p["story_points"]}, params={"boardId": p["board_id"]})
changed.append("story_points")
return self._out("done", {"changed": changed})
def ensure_issue_fields(self, key: str, p: dict, sp_fid: Optional[str]) -> List[str]:
"""Поля ensure_fields через GET/PUT /issue: только отличающиеся; оценка — здесь лишь при sp_fid (путь v15).
Возвращает имена записанных полей."""
cur = self.jira.get(API + "/issue/%s" % key)["fields"] cur = self.jira.get(API + "/issue/%s" % key)["fields"]
fields, changed = {}, [] fields, changed = {}, []
if "summary" in p and (cur.get("summary") or "") != (p["summary"] or ""): if "summary" in p and (cur.get("summary") or "") != (p["summary"] or ""):
@ -923,10 +939,9 @@ class Executor:
if sp_fid and not _same_points(cur.get(sp_fid), p["story_points"]): if sp_fid and not _same_points(cur.get(sp_fid), p["story_points"]):
fields[sp_fid] = p["story_points"] fields[sp_fid] = p["story_points"]
changed.append("story_points") changed.append("story_points")
if not fields: if fields:
return self._out("done", {"changed": []}) self.jira.put(API + "/issue/%s" % key, {"fields": fields})
self.jira.put(API + "/issue/%s" % key, {"fields": fields}) return changed
return self._out("done", {"changed": changed})
# --- ensure_comment -------------------------------------------------------- # --- ensure_comment --------------------------------------------------------
def do_ensure_comment(self, p: dict, cmd: dict) -> dict: def do_ensure_comment(self, p: dict, cmd: dict) -> dict:

View File

@ -86,6 +86,7 @@ class FakeJira:
self.sprint_seq = 500 # id следующего созданного спринта self.sprint_seq = 500 # id следующего созданного спринта
self.sprint_posts = [] # (путь, тело) каждой записи спринта: POST /sprint и POST /sprint/{id} self.sprint_posts = [] # (путь, тело) каждой записи спринта: POST /sprint и POST /sprint/{id}
self.rank_bodies = [] # тела PUT /rest/agile/1.0/issue/rank self.rank_bodies = [] # тела PUT /rest/agile/1.0/issue/rank
self.estimation_bodies = [] # тела PUT /rest/agile/1.0/issue/{key}/estimation
self.rank_errors = {} # ключ → текст ошибки ранжирования: ответ 207, ранг задачи не меняется self.rank_errors = {} # ключ → текст ошибки ранжирования: ответ 207, ранг задачи не меняется
self.seq, self.link_seq, self.comment_seq = 1000, 100, 10 self.seq, self.link_seq, self.comment_seq = 1000, 100, 10
self.id_seq = 10000 self.id_seq = 10000
@ -557,6 +558,17 @@ class FakeJira:
return 204, None return 204, None
if p == "/rest/agile/1.0/issue/rank" and method == "PUT": if p == "/rest/agile/1.0/issue/rank" and method == "PUT":
return self._rank(body) return self._rank(body)
m = re.match(r"^/rest/agile/1\.0/issue/([^/]+)/estimation$", p)
if m: # оценка задачи на доске boardId (поле оценки доски — POINTS_FIELD)
if m.group(1) not in self.issues:
return 404, {"errorMessages": ["Issue Does Not Exist"]}
if int(q.get("boardId") or 0) not in self.boards:
return 400, {"errorMessages": ["The board %s does not exist" % q.get("boardId")]}
issue = self.issues[m.group(1)]
if method == "PUT":
self.estimation_bodies.append(body)
issue[POINTS_FIELD] = body.get("value")
return 200, {"fieldId": POINTS_FIELD, "value": issue.get(POINTS_FIELD)}
m = re.match(r"^/rest/agile/1\.0/issue/([^/]+)$", p) m = re.match(r"^/rest/agile/1\.0/issue/([^/]+)$", p)
if m: if m:
name = self.issues[m.group(1)]["sprint"] name = self.issues[m.group(1)]["sprint"]

View File

@ -294,5 +294,63 @@ class ExecutorFieldsTest(unittest.TestCase):
self.assertEqual(self.fake.writes(), []) self.assertEqual(self.fake.writes(), [])
# --- story_points через доску (#53) ------------------------------------------
def points(self, key, value):
self.fake.add(key, **({} if value is None else {"customfield_10106": value}))
def test_points_by_board_set(self): # сценарий «Оценка не на экране редактирования»
self.points("AURORA-1704", 1.0)
out = self.run_cmd("ensure_fields", key="AURORA-1704", story_points=2, board_id=4216)
self.assertEqual((out["status"], out["result"], out["jira_calls"]), ("done", {"changed": ["story_points"]}, 2))
self.assertEqual(self.fake.estimation_bodies, [{"value": 2}])
self.assertEqual(self.fake.issues["AURORA-1704"]["customfield_10106"], 2)
self.assertEqual(self.fake.queries[-1], {"boardId": "4216"})
for call in (("GET", "/rest/api/2/issue/AURORA-1704"), ("PUT", "/rest/api/2/issue/AURORA-1704"),
("GET", "/rest/api/2/field")):
self.assertNotIn(call, self.fake.requests)
def test_points_by_board_same(self): # сценарий «Оценка уже стоит»
self.points("AURORA-1704", 2.0)
out = self.run_cmd("ensure_fields", key="AURORA-1704", story_points=2, board_id=4216)
self.assertEqual((out["result"], out["jira_calls"]), ({"changed": []}, 1))
self.assertEqual(self.fake.estimation_bodies, [])
def test_points_by_board_clear(self):
self.points("AURORA-1", 1.0)
out = self.run_cmd("ensure_fields", key="AURORA-1", story_points=None, board_id=4216)
self.assertEqual(out["result"], {"changed": ["story_points"]})
self.assertEqual(self.fake.estimation_bodies, [{"value": None}])
again = self.run_cmd("ensure_fields", key="AURORA-1", story_points=None, board_id=4216)
self.assertEqual((again["result"], again["jira_calls"]), ({"changed": []}, 1))
def test_points_by_board_with_fields(self):
self.points("AURORA-1", None)
out = self.run_cmd("ensure_fields", key="AURORA-1", duedate="2026-10-30", story_points=3, board_id=4216)
self.assertEqual(out["result"], {"changed": ["duedate", "story_points"]})
writes = [r for r in self.fake.requests if r[0] == "PUT"]
self.assertEqual(writes, [("PUT", "/rest/api/2/issue/AURORA-1"), ("PUT", "/rest/agile/1.0/issue/AURORA-1/estimation")])
self.assertEqual(self.fake.issues["AURORA-1"]["duedate"], "2026-10-30")
self.assertEqual(self.fake.issues["AURORA-1"]["customfield_10106"], 3)
def test_points_by_board_jira_error(self):
self.points("AURORA-1", None)
out = self.run_cmd("ensure_fields", key="AURORA-1", duedate="2026-10-30", story_points=3, board_id=9999)
self.assertEqual(out["status"], "failed")
self.assertIn("9999", out["error"])
self.assertEqual(self.fake.issues["AURORA-1"]["duedate"], "2026-10-30")
def test_points_by_board_no_field_lookup(self):
self.points("AURORA-1", None)
self.fake.fields = []
self.ex.s["field_ids"] = {k: v for k, v in SETTINGS["field_ids"].items() if k != "story_points"}
out = self.run_cmd("ensure_fields", key="AURORA-1", story_points=3, board_id=4216)
self.assertEqual((out["status"], out["result"]), ("done", {"changed": ["story_points"]}))
def test_points_by_board_bad_value(self):
self.points("AURORA-1", None)
out = self.run_cmd("ensure_fields", key="AURORA-1", story_points="3", board_id=4216)
self.assertEqual((out["status"], out["error"]), ("failed", "story_points — число ≥ 0 или null"))
self.assertEqual(self.fake.requests, [])
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -197,7 +197,7 @@ class FolderIntegrationTest(unittest.TestCase):
return jira_cfg(self.fake, hub_url="folder:" + self.root, hub_token=self.hub.token) return jira_cfg(self.fake, hub_url="folder:" + self.root, hub_token=self.hub.token)
def test_version(self): def test_version(self):
self.assertEqual(agent.AGENT_VERSION, 15) self.assertEqual(agent.AGENT_VERSION, 16)
def test_runner_through_folder(self): def test_runner_through_folder(self):
self.fake.add("AURORA-1", duedate="2026-10-20") self.fake.add("AURORA-1", duedate="2026-10-20")

View File

@ -310,7 +310,7 @@ class RefreshHistoryTest(Base):
self.assertEqual(self.changelog_queries(), []) self.assertEqual(self.changelog_queries(), [])
def test_version(self): def test_version(self):
self.assertEqual(agent.AGENT_VERSION, 15) self.assertEqual(agent.AGENT_VERSION, 16)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -263,8 +263,8 @@ class RefreshTest(unittest.TestCase):
self.assertEqual(out["status"], "done") self.assertEqual(out["status"], "done")
self.assertEqual(out["result"]["missing"], ["GONE-1"]) self.assertEqual(out["result"]["missing"], ["GONE-1"])
def test_agent_version_15(self): def test_agent_version_16(self):
self.assertEqual(agent.AGENT_VERSION, 15) self.assertEqual(agent.AGENT_VERSION, 16)
self.fake.add("AURORA-1") self.fake.add("AURORA-1")
self.ex.run({"id": 19, "type": "refresh", "params": {"keys": ["AURORA-1"]}}) self.ex.run({"id": 19, "type": "refresh", "params": {"keys": ["AURORA-1"]}})
self.assertEqual(self.hub.states[0]["agent_version"], 15) self.assertEqual(self.hub.states[0]["agent_version"], 15)