146 lines
14 KiB
Markdown
146 lines
14 KiB
Markdown
## MODIFIED Requirements
|
||
|
||
### Requirement: Процесс и доступ
|
||
`jira-hub serve` SHALL запускать один процесс: HTTP на `0.0.0.0:<port>` (по умолчанию 8790), база и настройки в
|
||
`~/.local/share/jira-hub/` (переопределяется `JIRA_HUB_HOME`). Все маршруты API SHALL требовать
|
||
`Authorization: Bearer <token>`: токен агента модуля открывает только `/agent/*` (маршруты ядра — от имени этого агента,
|
||
маршруты модуля со scope `agent` — только агенту своего модуля, см. `modules`, «Агенты модулей»), клиентский — всё
|
||
остальное. `jira-hub tokens` SHALL печатать клиентский токен и токены агентов с именами модулей. Исключение —
|
||
раздача веб-приложения (`GET /app`, `GET /app/…`) и перенаправления `GET /`, `GET /links`: это статические файлы без
|
||
данных, они отдаются без токена. Ответы API — JSON, ошибки — `{error, detail}` с кодами 400 (неверное тело или
|
||
`Content-Length`), 401, 404, 409, 413 (тело больше предела), 422, 500 (внутренняя ошибка); реле общей папки
|
||
дополнительно отвечает 502. Методы, кроме GET, POST, PUT и PATCH, отвечают 501 стандартной страницей `http.server`, не
|
||
JSON.
|
||
|
||
#### Scenario: Чужой токен
|
||
- **WHEN** токеном агента `jira` вызван `GET /issues`
|
||
- **THEN** 401
|
||
|
||
#### Scenario: Неподдерживаемый метод
|
||
- **WHEN** `DELETE /issues/AURORA-1`
|
||
- **THEN** 501, тело не JSON; база не меняется
|
||
|
||
#### Scenario: Страница без токена
|
||
- **WHEN** `GET /app/board` без заголовка `Authorization`, сборка приложения есть
|
||
- **THEN** 200, `Content-Type: text/html; charset=utf-8`, тело — `index.html` сборки; данных зеркала в нём нет
|
||
|
||
#### Scenario: Токены после миграции
|
||
- **WHEN** база v6 с агентским токеном T мигрировала в v7, запущен `jira-hub tokens`
|
||
- **THEN** вывод содержит клиентский токен, `jira: T` и новый токен `wiki`
|
||
|
||
### Requirement: Маршруты проектов
|
||
Хаб SHALL предоставлять под клиентским токеном:
|
||
- `GET /projects` → список проектов по `created_at`, затем `label`; каждый — плоский объект из полей ядра (`label`,
|
||
`name`, `created_at`) и полей всех модулей (`modules`, «Поля проекта от модулей»). Модуль jira даёт `board_id`,
|
||
`board_type`, `issues_count` (задачи зеркала с меткой проекта), модуль wiki — `wiki_root`, `wiki_root_title`,
|
||
`wiki_pages`, `wiki_synced_at`, `wiki_sync_error`.
|
||
- `POST /projects` `{label, name, author?, <поля модулей>}` → 201 `{project, batch_id}`: в одной транзакции проект
|
||
записывается, модули записывают свои поля и ставят свои пакеты; `batch_id` — пакет первого модуля, вернувшего пакет
|
||
(у jira — «Новый проект <метка>» из одного `refresh` по базовой JQL, уже включающей новую метку). Автор пакетов —
|
||
`author` (по умолчанию `api`). Неизвестное поле, неверные поля ядра (см. `data-model`, «Проекты») или модуля — 422 с
|
||
указанием поля; метка, совпадающая с существующей без учёта регистра, — 409. При ошибке ни проекта, ни пакетов нет.
|
||
- `PATCH /projects/{label}` `{<изменяемые поля модулей>, author?}` → 200 `{project, batch_ids}`: модули правят свои поля
|
||
и ставят пакеты в одной транзакции. Неизменяемое поле (метка, название, `board_id`) — 422 «<поле> после создания не
|
||
меняется»; неизвестное поле или неверное значение — 422 с указанием поля; проекта нет — 404.
|
||
|
||
#### Scenario: Новый проект
|
||
- **WHEN** `POST /projects {label: "STOCKS_DRRK", name: "Запасы ДРРК", board_id: 5120, author: "viewer"}`, есть проект `AWPCA`
|
||
- **THEN** 201; `GET /projects` отдаёт два проекта, у `STOCKS_DRRK` `board_type = null`, `wiki_root = null`; в очереди
|
||
пакет автора `viewer` с `refresh {jql: 'project = AURORA AND labels in ("AWPCA", "STOCKS_DRRK")'}`
|
||
|
||
#### Scenario: Проект без доски
|
||
- **WHEN** `POST /projects {label: "STOCKS_DRRK", name: "Запасы ДРРК"}`
|
||
- **THEN** 201, `board_id = null`
|
||
|
||
#### Scenario: Проект с корнем вики
|
||
- **WHEN** `POST /projects {label: "STOCKS_DRRK", name: "Запасы ДРРК", wiki_root: "DOC-001234"}`
|
||
- **THEN** 201; в очереди пакет `refresh` и пакет `wiki_sync {label: "STOCKS_DRRK", root: "DOC-001234"}`; `batch_id` —
|
||
пакет `refresh`
|
||
|
||
#### Scenario: Повтор метки
|
||
- **WHEN** `POST /projects {label: "AWPCA", name: "…"}`, проект `AWPCA` есть
|
||
- **THEN** 409, пакет не поставлен
|
||
|
||
#### Scenario: Без названия
|
||
- **WHEN** `POST /projects {label: "STOCKS_DRRK", name: " "}`
|
||
- **THEN** 422 с указанием поля `name`
|
||
|
||
#### Scenario: Неизвестное поле
|
||
- **WHEN** `POST /projects {label: "X", name: "X", board: 5}`
|
||
- **THEN** 422 с указанием поля `board`, проекта нет
|
||
|
||
#### Scenario: Корень вики у существующего проекта
|
||
- **WHEN** `PATCH /projects/AWPCA {wiki_root: "DOC-000777"}`
|
||
- **THEN** 200, `batch_ids` — один пакет `wiki_sync`; `GET /projects` отдаёт `AWPCA` с `wiki_root = DOC-000777`
|
||
|
||
#### Scenario: Доску не меняют
|
||
- **WHEN** `PATCH /projects/AWPCA {board_id: 1}`
|
||
- **THEN** 422 «board_id после создания не меняется», доска прежняя
|
||
|
||
### Requirement: Публикация версии агента
|
||
`jira-hub agent publish [--module <модуль>] PATH` SHALL публиковать файл агента модуля (по умолчанию `jira`;
|
||
неизвестный модуль или модуль без команд — код 2 и список модулей с агентами в stderr): текст в UTF-8 не больше 1 МБ,
|
||
ровно одна строка `AGENT_VERSION = <целое>`, версия больше опубликованной для этого модуля. Файл SHALL записываться в
|
||
`<JIRA_HUB_HOME>/release/<модуль>/agent.py` (каталоги 0700), затем `manifest.json` `{version, sha256, size,
|
||
published_at}`; оба — через временный файл с `fsync` и переименование, манифест последним. Прежний `agent.py` на время
|
||
публикации SHALL переименовываться в `agent.py.prev` и удаляться после записи манифеста; если запись нового файла или
|
||
манифеста не удалась (`OSError`), хаб SHALL вернуть `agent.py.prev` на место `agent.py`, удалить временные файлы и
|
||
завершить команду сообщением «не удалось записать публикацию: <ошибка>» и кодом 1 — опубликованной остаётся прежняя
|
||
версия. Команда SHALL работать без запущенного `serve` и печатать «опубликована v<N>, sha256 <первые 12 символов>…,
|
||
<size> байт»; при ошибке — сообщение и код 1, опубликованное не меняется. При старте хаб SHALL один раз перенести
|
||
прежнюю публикацию `release/agent.py` и `release/manifest.json` в `release/jira/`, если `release/jira/` ещё нет; ошибка
|
||
переноса — предупреждение в лог, хаб стартует. Хаб SHALL читать манифест модуля агента при каждом `claim`,
|
||
`GET /agent/release` и `GET /agent`; манифест годен, только если это объект, `version` — целое число (не логическое) не
|
||
меньше 1, `sha256` — строка из 64 символов `[0-9a-f]`; негодный или нечитаемый манифест означает «публикации нет» и
|
||
предупреждение в лог — одно на каждый новый текст ошибки. Файл `agent.py`, не декодируемый как UTF-8 при совпавшем
|
||
sha256, означает «публикации нет» (`GET /agent/release` — 404 `no_release`). `GET /agent?module=<модуль>` (без
|
||
параметра — `jira`) и каждый элемент `GET /agents` SHALL дополняться полями `release` (`{version, sha256,
|
||
published_at}` или `null`), `update_state`, `launcher_version`, `agent_reported` (был ли `claim` этого агента с момента
|
||
запуска хаба) и `update_hint`: «агент запущен без run.py — обновится только вручную» (опубликована версия выше версии
|
||
агента, агент онлайн, `agent_reported = true`, `launcher_version = null`), «откат с v<to> на v<from>: <error>»
|
||
(`rolled_back`), «v<to> отвергнута: <error>» (`rejected`), иначе `null`.
|
||
|
||
#### Scenario: Публикация новой версии
|
||
- **WHEN** опубликована v5, `jira-hub agent publish jira-agent/src/agent.py` с `AGENT_VERSION = 6`
|
||
- **THEN** в `release/jira/` — новый `agent.py` и манифест с `version = 6`, `agent.py.prev` нет; вывод «опубликована v6,
|
||
sha256 …»; код 0
|
||
|
||
#### Scenario: Публикация eva-agent
|
||
- **WHEN** `jira-hub agent publish --module wiki eva-agent/src/agent.py` с `AGENT_VERSION = 1`, опубликована jira v14
|
||
- **THEN** в `release/wiki/` — v1; `claim` агента `wiki` получает `agent_release {version: 1, …}`, `claim` агента `jira` —
|
||
`{version: 14, …}`
|
||
|
||
#### Scenario: Неизвестный модуль
|
||
- **WHEN** `jira-hub agent publish --module nope agent.py`
|
||
- **THEN** код 2, в stderr «неизвестный модуль nope» и список модулей с агентами; `release/` не изменён
|
||
|
||
#### Scenario: Версия не выше опубликованной
|
||
- **WHEN** опубликована v6, публикуется файл с `AGENT_VERSION = 6`
|
||
- **THEN** «опубликована v6, новая должна быть больше», код 1; `release/` не изменён
|
||
|
||
#### Scenario: Нет строки версии
|
||
- **WHEN** публикуется файл без строки `AGENT_VERSION = <целое>` или с двумя такими строками
|
||
- **THEN** сообщение об ошибке, код 1; `release/` не изменён
|
||
|
||
#### Scenario: Диск полон при записи манифеста
|
||
- **WHEN** опубликована v8, публикуется v9, запись `manifest.json` завершилась `OSError`
|
||
- **THEN** «не удалось записать публикацию: …», код 1, без трейсбека; `agent.py` и манифест — прежние v8,
|
||
`GET /agent/release` отдаёт v8; временных файлов и `agent.py.prev` в `release/jira/` нет
|
||
|
||
#### Scenario: Негодный манифест
|
||
- **WHEN** в `manifest.json` `version = 6.9` (или `true`, или `sha256` короче 64 символов), хаб получает 100 `claim`
|
||
- **THEN** в `claim` `agent_release = null`, `GET /agent/release` — 404; в логе одно предупреждение о манифесте
|
||
|
||
#### Scenario: Агент без запускателя
|
||
- **WHEN** опубликована v6, агент v5 онлайн, в последнем `claim` `launcher_version = null`
|
||
- **THEN** `GET /agent` → `agent_reported = true`, `update_hint` = «агент запущен без run.py — обновится только вручную»
|
||
|
||
#### Scenario: Хаб перезапущен, агент ещё не выходил на связь
|
||
- **WHEN** опубликована v10, агент v9 под запускателем, хаб перезапущен, `claim` после запуска ещё не было
|
||
- **THEN** `GET /agent` → `agent_reported = false`, `launcher_version = null`, `update_hint = null`; после первого
|
||
`claim` — `agent_reported = true`, `launcher_version` из `claim`
|
||
|
||
#### Scenario: Перенос прежней публикации
|
||
- **WHEN** хаб с `release/agent.py` v14 и манифестом без `release/jira/` запущен на новом коде
|
||
- **THEN** публикация v14 лежит в `release/jira/`, `claim` агента `jira` получает `agent_release {version: 14, …}`
|