Merge pull request 'Архив OpenSpec: hub-backend-fixes (#129)' (#131) from chore/129-archive-hub-backend-fixes into main
This commit is contained in:
commit
f09d857521
@ -390,3 +390,52 @@ SHALL перевести её в `confirmed_by_state`: `ensure_fields` — вс
|
|||||||
#### Scenario: Перенаправление на другой хост
|
#### Scenario: Перенаправление на другой хост
|
||||||
- **WHEN** Jira на `GET <base_url>/secure/attachment/31337/…` ответила 302 на `https://files.example/…`
|
- **WHEN** Jira на `GET <base_url>/secure/attachment/31337/…` ответила 302 на `https://files.example/…`
|
||||||
- **THEN** агент туда не ходит, команда `failed` «Jira перенаправила скачивание на files.example»
|
- **THEN** агент туда не ходит, команда `failed` «Jira перенаправила скачивание на files.example»
|
||||||
|
|
||||||
|
### Requirement: Нормализация параметров при постановке
|
||||||
|
При постановке пакета (`POST /batches`, MCP `batch_create`, повтор команды) хаб SHALL сохранять параметры команд в
|
||||||
|
каноническом виде. Нормализация SHALL выполняться сразу после проверки типов значений, до остальных проверок:
|
||||||
|
обязательный параметр, ставший пустым после обрезки, даёт 422 «нет <параметр>»; правила типов, временные ключи
|
||||||
|
`new:<n>` и проверки `ensure_attachment` работают по нормализованным значениям; элемент списка, ставший пустым, даёт
|
||||||
|
422 «<параметр> — без пустых элементов». Необязательное поле `ensure_fields` из одних пробелов (`priority`, `duedate`,
|
||||||
|
`planned_start`, `planned_end`, `epic_key`, `assignee`) даёт 422 «<параметр> — из одних пробелов; для очистки — "" или
|
||||||
|
null»: после обрезки оно стало бы пустой строкой, которую агент читает как «очистить поле в Jira» (R-8). Строковые параметры обрезаются по краям, кроме текстовых `summary`,
|
||||||
|
`description`, `body` и `goal`, которые хранятся как заданы. Элементы списков `labels`, `labels_add`, `labels_remove`
|
||||||
|
и `keys`, а также `target` у `ensure_status`, если это список, обрезаются поэлементно. Числа, логические значения и
|
||||||
|
`null` не меняются. `filename` у `ensure_attachment` SHALL дополнительно приводиться к нормальной форме Unicode NFC.
|
||||||
|
Агент SHALL получать параметры в том виде, в каком они сохранены.
|
||||||
|
|
||||||
|
#### Scenario: Ключ с пробелами
|
||||||
|
- **WHEN** `POST /batches` с `ensure_rank {key: "A-1", before: " A-2 "}`
|
||||||
|
- **THEN** пакет создан, параметры команды `{key: "A-1", before: "A-2"}`; агент при `claim` получает `before = "A-2"`
|
||||||
|
|
||||||
|
#### Scenario: Текст не обрезается
|
||||||
|
- **WHEN** `POST /batches` с `ensure_comment {key: " A-1", marker: "Итог ", body: " отступ\n"}`
|
||||||
|
- **THEN** сохранено `key = "A-1"`, `marker = "Итог"`, `body = " отступ\n"`
|
||||||
|
|
||||||
|
#### Scenario: Временный ключ с пробелами
|
||||||
|
- **WHEN** пакет `ensure_issue {temp_key: "new:1", …}`, затем `ensure_fields {key: "A-5", epic_key: " new:1 "}`
|
||||||
|
- **THEN** пакет создан, `epic_key` сохранён как `new:1` и подставляется настоящим ключом после выполнения
|
||||||
|
`ensure_issue`
|
||||||
|
|
||||||
|
#### Scenario: Ключ из пробелов
|
||||||
|
- **WHEN** `POST /batches` с `ensure_status {key: " ", target: "Готово"}`
|
||||||
|
- **THEN** 422 «команда 1 (ensure_status): нет key; ожидаются key, target»
|
||||||
|
|
||||||
|
#### Scenario: Поле из пробелов не очищает Jira
|
||||||
|
- **WHEN** `POST /batches` с `ensure_fields {key: "A-1", assignee: " "}`
|
||||||
|
- **THEN** 422 «команда 1 (ensure_fields): assignee — из одних пробелов; для очистки — "" или null», пакет не создан
|
||||||
|
|
||||||
|
#### Scenario: Имя вложения в NFD
|
||||||
|
- **WHEN** `ensure_attachment` с `filename` «отчёт.pdf» в NFD («е» + U+0308)
|
||||||
|
- **THEN** сохранён `filename` «отчёт.pdf» в NFC; повторная постановка того же файла с именем в NFC даёт ту же строку
|
||||||
|
`filename`
|
||||||
|
|
||||||
|
### Requirement: Идентификаторы из цифр — только ASCII
|
||||||
|
Хаб SHALL считать строку числовым идентификатором (`attachment_id` в `fetch_attachment`, `id` вложения в состоянии
|
||||||
|
агента, ссылка `#<id>` в MCP, версия агента), только если она непуста и состоит из символов `0-9`. Строка из других
|
||||||
|
цифр Unicode (`"²"`, `"١٢"`) — не идентификатор: при постановке 422, в состоянии агента 422 `bad_state`, в MCP —
|
||||||
|
поиск по имени. Ответ 500 MUST NOT возникать.
|
||||||
|
|
||||||
|
#### Scenario: Надстрочная цифра
|
||||||
|
- **WHEN** `POST /batches` с `fetch_attachment {key: "A-1", attachment_id: "²"}`
|
||||||
|
- **THEN** 422 «attachment_id — цифры id вложения из issue», пакет не создан
|
||||||
|
|||||||
@ -212,10 +212,9 @@ SHALL обнулить `issues_count` и `links_count` её `sync_run` (`started
|
|||||||
относительный путь и не-строка при записи — 422; агенту не передаётся), `closed_statuses` (список имён статусов Jira, считающихся закрытыми, по умолчанию
|
относительный путь и не-строка при записи — 422; агенту не передаётся), `closed_statuses` (список имён статусов Jira, считающихся закрытыми, по умолчанию
|
||||||
`["Готово", "Снято"]`; не-список при записи — 422; агенту не передаётся; на существующей базе появляется со значением
|
`["Готово", "Снято"]`; не-список при записи — 422; агенту не передаётся; на существующей базе появляется со значением
|
||||||
по умолчанию при открытии хаба). Базовая JQL вычисляется из проектов, доска — свойство проекта (требование
|
по умолчанию при открытии хаба). Базовая JQL вычисляется из проектов, доска — свойство проекта (требование
|
||||||
«Проекты»); на новой базе ключей `base_jql` и `board_id` нет. Оставшиеся от v3 ключи хаб SHALL NOT использовать, кроме
|
«Проекты»); на новой базе ключей `base_jql` и `board_id` нет. Ключи, которые модуль объявил устаревшими (у jira — `status_labels`, `transitions_map`, оставшиеся от v3
|
||||||
передачи `board_id` агенту как запасной доски для команд без доски в параметрах (поставленных до миграции);
|
`base_jql` и `board_id`), хаб SHALL удалять при каждом заполнении умолчаний, не меняя `settings_version`; агенту они
|
||||||
Ключи, которые модуль объявил устаревшими (у jira — `status_labels`, `transitions_map`), хаб SHALL удалять при каждом
|
не передаются. `GET /settings` SHALL отдавать вычисленную `base_jql` только для чтения, а запись `base_jql` или `board_id` через
|
||||||
заполнении умолчаний, не меняя `settings_version`. `GET /settings` SHALL отдавать вычисленную `base_jql` только для чтения, а запись `base_jql` или `board_id` через
|
|
||||||
`PUT /settings` — 422 с пояснением. Спринты агент читает с досок проектов, отдельной настройки имён спринтов нет. Начальные значения SHALL импортироваться из `admin/jira-sync/config.example.json`
|
`PUT /settings` — 422 с пояснением. Спринты агент читает с досок проектов, отдельной настройки имён спринтов нет. Начальные значения SHALL импортироваться из `admin/jira-sync/config.example.json`
|
||||||
командой `sdlc-hub init`; пароль и логин Jira в настройках MUST NOT присутствовать. `issue_types` — объект
|
командой `sdlc-hub init`; пароль и логин Jira в настройках MUST NOT присутствовать. `issue_types` — объект
|
||||||
`{task, epic, bug}`, у каждого `{name, id}` (`id` может быть `null`); `init` берёт их из ключей конфига
|
`{task, epic, bug}`, у каждого `{name, id}` (`id` может быть `null`); `init` берёт их из ключей конфига
|
||||||
@ -256,6 +255,11 @@ SHALL обнулить `issues_count` и `links_count` её `sync_run` (`started
|
|||||||
- **THEN** `GET /settings` не содержит `status_labels` и `transitions_map`; `GET /agent/settings` агенту `jira` их не
|
- **THEN** `GET /settings` не содержит `status_labels` и `transitions_map`; `GET /agent/settings` агенту `jira` их не
|
||||||
отдаёт
|
отдаёт
|
||||||
|
|
||||||
|
#### Scenario: Ключи v3 в живой базе
|
||||||
|
- **WHEN** в `setting` лежат `base_jql` и `board_id` (база, поднятая с v3), хаб открыл базу
|
||||||
|
- **THEN** ключей `base_jql` и `board_id` в `setting` нет, `settings_version` прежний; `GET /settings` отдаёт вычисленную
|
||||||
|
`base_jql`, `board_id` в ответе нет; `PUT /settings {board_id: 1}` — 422 «доска Jira — свойство проекта»
|
||||||
|
|
||||||
### Requirement: Проекты
|
### Requirement: Проекты
|
||||||
Таблица `project` SHALL хранить проекты моста — подмножества задач Jira-проекта `project_key`, выделенные меткой:
|
Таблица `project` SHALL хранить проекты моста — подмножества задач Jira-проекта `project_key`, выделенные меткой:
|
||||||
`label` (PK, метка Jira), `name`, `created_at`. Метка SHALL быть непустой строкой до 100 символов без пробельных и
|
`label` (PK, метка Jira), `name`, `created_at`. Метка SHALL быть непустой строкой до 100 символов без пробельных и
|
||||||
|
|||||||
@ -790,3 +790,53 @@ from_string, to, to_string}`. До записи хаб SHALL проверить
|
|||||||
#### Scenario: Проект без доски
|
#### Scenario: Проект без доски
|
||||||
- **WHEN** `GET /agile?project=STOCKS_DRRK`, у проекта нет доски
|
- **WHEN** `GET /agile?project=STOCKS_DRRK`, у проекта нет доски
|
||||||
- **THEN** 200, `board = null`, `sprints = []`, `backlog = null`, `board_issues = null`
|
- **THEN** 200, `board = null`, `sprints = []`, `backlog = null`, `board_issues = null`
|
||||||
|
|
||||||
|
### Requirement: Форма тела запроса
|
||||||
|
На тело неверной формы хаб SHALL отвечать 422, а не 500. В `POST /agent/state` поле `issues` SHALL быть списком,
|
||||||
|
каждый его элемент — объектом с непустым строковым `key`. Иначе 422 `bad_state` с номером элемента, и пачка не
|
||||||
|
применяется. В `POST /wiki/sync` поле `project` в теле, если задано, SHALL быть строкой. Иначе 422 `bad_request`
|
||||||
|
«project — строка».
|
||||||
|
|
||||||
|
#### Scenario: Элемент состояния не объект
|
||||||
|
- **WHEN** агент шлёт `POST /agent/state {issues: ["A-1"]}`
|
||||||
|
- **THEN** 422 `bad_state` «issues[0] — объект задачи с key», зеркало не изменилось
|
||||||
|
|
||||||
|
#### Scenario: Проект вики не строкой
|
||||||
|
- **WHEN** `POST /wiki/sync {project: 5}`
|
||||||
|
- **THEN** 422 `bad_request` «project — строка», пакет не создан
|
||||||
|
|
||||||
|
### Requirement: Ошибка доступа к файлу хранилища
|
||||||
|
Если файл хранилища вложений не читается (`PermissionError`, `IsADirectoryError` или другая ошибка ОС), хаб SHALL
|
||||||
|
обрабатывать его как отсутствующий: при `claim` `ensure_attachment` получает `size = null`, `GET /files/<sha>` и
|
||||||
|
скачивание вложения отвечают 404, как для удалённого файла. В лог хаба SHALL писаться предупреждение с путём и
|
||||||
|
ошибкой. `FileNotFoundError` в лог не пишется.
|
||||||
|
|
||||||
|
#### Scenario: Нет прав на файл
|
||||||
|
- **WHEN** у файла `files/<sha>` сняты права на чтение, агент берёт `ensure_attachment` с этим `file`
|
||||||
|
- **THEN** `claim` отвечает 200, у команды `size = null`; в логе — предупреждение с путём
|
||||||
|
|
||||||
|
### Requirement: Пустой SDLC_HUB_HOME
|
||||||
|
Переменная `SDLC_HUB_HOME`, заданная пустой строкой, SHALL трактоваться как незаданная: каталог данных —
|
||||||
|
`~/.local/share/sdlc-hub/`, проверка прежнего каталога выполняется. Хаб MUST NOT писать данные в рабочий каталог
|
||||||
|
процесса из-за пустой переменной.
|
||||||
|
|
||||||
|
#### Scenario: Пустая переменная
|
||||||
|
- **WHEN** `SDLC_HUB_HOME= sdlc-hub tokens`
|
||||||
|
- **THEN** база открыта из `~/.local/share/sdlc-hub/hub.sqlite`; в рабочем каталоге не появился `hub.sqlite`
|
||||||
|
|
||||||
|
### Requirement: Восстановление прерванной публикации агента
|
||||||
|
Перед публикацией версии агента модуля хаб SHALL приводить `release/<модуль>/` к опубликованному состоянию по
|
||||||
|
`manifest.json`. Если `agent.py.prev` есть, а `agent.py` нет или не совпадает по sha256 с манифестом, тогда как
|
||||||
|
`agent.py.prev` совпадает, `agent.py.prev` SHALL возвращаться на место `agent.py`. Если `agent.py` совпадает с
|
||||||
|
манифестом, оставшийся `agent.py.prev` удаляется. В других сочетаниях файлы не трогаются, в лог пишется
|
||||||
|
предупреждение. Годный `agent.py.prev`, совпадающий с манифестом, MUST NOT перезаписываться другим содержимым.
|
||||||
|
|
||||||
|
#### Scenario: Сбой между переименованием и записью
|
||||||
|
- **WHEN** опубликована v5, прошлая публикация упала после переименования: `agent.py` нет, `agent.py.prev` — v5,
|
||||||
|
манифест — v5; новая публикация v6 снова падает при записи
|
||||||
|
- **THEN** в `release/jira/` — `agent.py` v5 и манифест v5, опубликованной остаётся v5
|
||||||
|
|
||||||
|
#### Scenario: Сбой поверх неудачного возврата
|
||||||
|
- **WHEN** `agent.py` — недописанный v6, `agent.py.prev` — v5, манифест — v5; запущена публикация v6
|
||||||
|
- **THEN** перед записью `agent.py` восстановлен из `.prev` (v5); при сбое записи опубликованной остаётся v5, а `.prev`
|
||||||
|
не затёрт недописанным файлом
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user