9.3 KiB
MODIFIED Requirements
Requirement: Журнал на диске и восстановление
Агент SHALL вести journal.jsonl (только дозапись) рядом с собой: строки claimed {command_id, at} (сразу после
получения команды, до паузы), started {command_id, at}, finished {command_id, status, result, error, jira_calls, rate_limited, at}, acked {command_id, at}. При старте агент SHALL: отправить хабу все finished без acked; для
claimed/started без finished — запросить команду у хаба по id и, если она в хабе всё ещё claimed, выполнить её
заново по правилу «проверь, потом сделай» с паузой pause_seconds из ответа хаба, не записывая claimed повторно.
Команда python3 agent.py prune SHALL удалять из журнала записи с acked старше 30 дней; она SHALL брать ту же
блокировку <журнал>.lock, что run, и при занятой блокировке завершаться с кодом 1 и сообщением «агент запущен»,
не меняя журнал.
Scenario: Падение после ответа Jira
- WHEN агент упал между
POST /issueи записьюfinished, затем перезапущен - THEN агент находит в журнале
startedбезfinished, запрашивает команду у хаба, находит созданную задачу поиском дубля и отчитываетсяdoneс её ключом; второй задачи нет
Scenario: Восстановление без лишней записи
- WHEN в журнале
claimed 40безfinished, хаб отдаёт команду 40 вclaimedсpause_seconds = 30 - THEN в журнал дописываются
started 40,finished 40,acked 40; второй строкиclaimed 40нет; перед выполнением агент выдерживает 30 с от последнего вызова Jira
Scenario: prune при работающем агенте
- WHEN
agent.py runработает, и запущенagent.py pruneс тем же журналом - THEN
pruneпишет «агент запущен…» и завершается с кодом 1; журнал не изменён
Requirement: Ограничение темпа и 429
Агент SHALL помнить время последнего вызова Jira (любого) и не начинать команду раньше pause_seconds после него. При
старте агент SHALL принять за время последнего вызова Jira самое позднее at в журнале; если оно в будущем (часы
сдвинулись назад) — текущий момент (пауза перед первой командой — полная pause_seconds); при пустом журнале паузы
перед первой командой нет. Оценка из журнала служит только паузе: в claim агент SHALL передавать last_jira_call_at
только после настоящего вызова Jira в текущем процессе, до него — null. На HTTP 429 и 503 агент SHALL ждать
Retry-After (иначе 60 с × номер попытки) и повторять тот же запрос до 5 раз, считая попытки в rate_limited; после
5 неудач — failed. Пауза между записями внутри одной команды — по зазору типа команды.
Scenario: 429 с Retry-After
- WHEN Jira ответила 429 с
Retry-After: 90 - THEN агент ждёт 90 с, повторяет тот же запрос; в отчёте
rate_limited = 1
Scenario: Перезапуск не сбивает темп
- WHEN последняя запись журнала
finishedсделана 10 с назад, агент перезапущен и получил команду сpause_seconds = 30 - THEN агент ждёт около 20 с перед первым вызовом Jira по команде
Scenario: Время журнала в будущем
- WHEN последняя запись журнала на 2 часа позже текущего времени, агент перезапущен и получил команду с
pause_seconds = 30 - THEN агент ждёт 30 с перед первым вызовом Jira; в логе — что время журнала в будущем
Scenario: После перезапуска хаб не получает оценку из журнала
- WHEN агент перезапущен с непустым журналом и отправляет первый
claim - THEN
last_jira_call_atвclaim—null; после первого вызова Jira следующийclaimнесёт его время
Requirement: Доставка и запуск
README агента SHALL описывать: копирование agent.py и agent.example.json через общую папку, заполнение agent.json
(включая выбор транспорта в hub_url), check, запуск в терминале и через nohup с логом, опциональный launchd-plist,
prune (только при остановленном агенте, иначе откажет). Версия агента SHALL быть в константе файла (4 — со сравнением
текстов без различия переводов строк и уточнённым восстановлением темпа) и в каждом отчёте; хаб предупреждает в
agent_status, если версия ниже минимальной из настроек.
Scenario: Устаревший агент
- WHEN агент версии 1 выходит на связь, а в настройках
min_agent_version = 2 - THEN
agent_status.warning= «обновите агента», команды выдаются по-прежнему
Requirement: Транспорт через общую папку
При hub_url = "folder:<путь>" агент SHALL обращаться к хабу теми же запросами, что по HTTP, через файлы в <путь>:
на каждый вызов записать req/<id>.json = {id, method, path, body, authorization, deadline} (id — uuid4().hex,
authorization — Bearer <hub_token>, deadline — момент UTC = сейчас + таймаут вызова; для claim таймаут не меньше
45 с) через временный файл с именем на точку и переименование, затем опрашивать resp/<id>.json каждые 0,1 с.
Полученный ответ агент SHALL удалить; status 2xx — вернуть разобранное тело, иной — выбросить HttpError(status, body)
так же, как HTTP-клиент. Если к deadline ответа нет, агент SHALL удалить свой req/<id>.json (если он ещё там) и
выбросить ConnectionError. Нечитаемый ответ — не JSON-объект, status не целое число или body не строка и не null (null и отсутствие — пустое тело) — SHALL
перечитываться до 2 с, затем ConnectionError. Чужие файлы в resp/ агент MUST NOT трогать. Цикл выполнения, журнал,
повторы отчёта и восстановление SHALL работать без изменений.
Scenario: Ответ хаба через папку
- WHEN агент вызвал
settings, реле положилоresp/<id>.jsonсоstatus = 200 - THEN агент получает настройки, файл ответа удалён
Scenario: Реле не отвечает
- WHEN агент вызвал
claim, а ответа нет 45 с - THEN
req/<id>.jsonудалён агентом, выброшенConnectionError; цикл пишет «хаб недоступен» и повторяетclaimчерезpoll_seconds, как при недоступном хабе по сети
Scenario: Код ошибки хаба через папку
- WHEN агент отправил
resultпо команде, которой в хабе нет, реле положило ответ соstatus = 404 - THEN агент получает
HttpError(404)и считает команду закрытой, как по HTTP
Scenario: Ответ с неверными типами
- WHEN в
resp/<id>.jsonлежит{"id": …, "status": "ok", "body": 5} - THEN агент выбрасывает
ConnectionError(«нечитаемый ответ»), процесс не падает; цикл повторяет вызов как при недоступном хабе