Спека: пакет C бэкенда хаба hub-backend-fixes (#129)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
1c8d7f4f36
commit
e786428a1d
2
openspec/changes/hub-backend-fixes/.openspec.yaml
Normal file
2
openspec/changes/hub-backend-fixes/.openspec.yaml
Normal file
@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-10-26
|
||||
73
openspec/changes/hub-backend-fixes/design.md
Normal file
73
openspec/changes/hub-backend-fixes/design.md
Normal file
@ -0,0 +1,73 @@
|
||||
## Context
|
||||
|
||||
Пакет C сверки follow-ups 25.10.2026 (sdlc-hub#129). Все правки — в Python-коде хаба, без смены схемы базы и контракта
|
||||
с агентами. Владелец пропустил вопросы brainstorming («не сталкивался и не хочу»): решения ниже приняты агентом и
|
||||
продублированы в «Decisions made without discussion».
|
||||
|
||||
## Decisions
|
||||
|
||||
1. **Нормализация параметров — один шаг в `validate`.** После всех проверок `validate` кладёт в пакет параметры,
|
||||
приведённые функцией `normalize(ctype, params) -> dict` (`modules/jira/commands.py`). Проверки на пустоту
|
||||
(`.strip()`) остаются как есть. Обрезаются строковые параметры всех типов, кроме текстовых `summary`,
|
||||
`description`, `body`, `goal`. Это ключи, `temp_key`, `sprint`, `rename_to`, `move_open_to`, `link_type`,
|
||||
`assignee`, `priority`, даты, `marker`, `attachment_id`, `file`, `filename`, `issue_type`, `target` (строка или
|
||||
элементы списка). Элементы списков `labels`, `labels_add`, `labels_remove`, `keys` тоже обрезаются. Числа,
|
||||
логические значения и `null` не меняются. Нормализация возвращает новый словарь и не меняет вход.
|
||||
2. **`filename` — NFC.** `normalize` приводит `filename` у `ensure_attachment` к `unicodedata.normalize("NFC", …)`.
|
||||
MCP подставляет имя по умолчанию из `Path(path).name` и тоже приводит его к NFC; на macOS имена из пути приходят в
|
||||
NFD. Поиск вложения по имени в MCP-инструменте `attachment` сравнивает имена в NFC с обеих сторон.
|
||||
3. **Проверки `hooks.check_batch` идут по нормализованным параметрам.** `validate` вызывается раньше, поэтому
|
||||
`filename` с пробелами по краям проверяется уже обрезанным.
|
||||
4. **ASCII-цифры.** В `core/http.py` (рядом с `int_group`) появляется функция `is_digits(s) -> bool`: `s` — непустая строка только из
|
||||
`0-9` (`s.isascii() and s.isdigit()`). Модуль jira использует её вместо `str.isdigit()` везде, где за проверкой
|
||||
идёт `int()` или где строка — идентификатор: `hooks.py` (`attachment_id`), `mirror.py` (`check_attachments`),
|
||||
`attachments.py` (сортировка), `fetch.py` (версия агента), `mcp.py` (ссылка `#<id>`). В разборе дат `history.py`
|
||||
и `mcp.py` значение уже прошло форматную проверку, но для единообразия там тоже `is_digits`.
|
||||
5. **Форма элементов состояния.** `mirror.check_issues(issues)` вызывается до `check_comments` и
|
||||
`check_attachments`. Она требует: `issues` — список (иначе сейчас 500 на итерации), каждый элемент — объект с непустым строковым `key`. Иначе
|
||||
`ValueError` с номером элемента. Обработчик `POST /agent/state` уже превращает `ValueError` в 422 `bad_state`,
|
||||
как для комментариев.
|
||||
6. **`POST /wiki/sync`.** Если `project` в теле задан и это не строка, ответ 422 `bad_request` «project — строка»
|
||||
до обращения к проектам.
|
||||
7. **Файлы хранилища.** `files.size` и `files.read` ловят `OSError`. `FileNotFoundError` по-прежнему тихо даёт `None`.
|
||||
Остальные ошибки тоже дают `None`, но пишут в лог предупреждение с путём и ошибкой. Дальше всё идёт по
|
||||
существующему пути «файла нет»: `size = null` при claim, 404 при скачивании.
|
||||
8. **`SDLC_HUB_HOME=""`.** `paths.home()` берёт `os.environ.get("SDLC_HUB_HOME") or <по умолчанию>`. Пустая строка
|
||||
равна незаданной переменной; `home_problem`/`home_warning` уже считают так.
|
||||
9. **Восстановление прерванной публикации.** В начале `release.publish`, до проверки версии, вызывается
|
||||
`_recover(rd)`. Если `agent.py.prev` есть, а `agent.py` нет или его sha256 не совпадает с манифестом, тогда как
|
||||
sha256 `agent.py.prev` совпадает, `agent.py.prev` возвращается на место `agent.py` с предупреждением в лог. Если
|
||||
`agent.py` совпадает с манифестом, а `.prev` лежит, `.prev` удаляется. Во всех остальных случаях ничего не
|
||||
трогается, а в лог пишется предупреждение. Так закрываются T2.2 и M-6: после `_recover` в `agent.py` лежит
|
||||
опубликованная версия, и `os.replace(agent, prev)` не затирает годный `.prev`.
|
||||
10. **Устаревшие ключи настроек.** `base_jql` и `board_id` добавляются в `modules/jira/settings.OBSOLETE`.
|
||||
`ensure_defaults` удаляет их при каждом открытии, не меняя `settings_version`, как `status_labels`. Запись через
|
||||
`PUT /settings` по-прежнему даёт 422 с пояснением. Агенту `board_id` давно не передаётся: его нет в `AGENT_KEYS`.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- Нормализация меняет то, что раньше уходило агенту дословно. Пробелы по краям идентификатора в Jira не значимы,
|
||||
а текстовые поля не трогаются.
|
||||
- `_recover` срабатывает только при совпадении sha256 с манифестом, поэтому неизвестное содержимое не
|
||||
перезаписывается.
|
||||
- Удаление `board_id` из `setting` необратимо, но значение нигде не читается. Копия базы есть в `backups/`.
|
||||
|
||||
## Decisions made without discussion
|
||||
|
||||
- R-1 (proposal, 26.10.2026): O-3 (`check_confirmed` считает обратную связь симметричного типа конфликтом) не
|
||||
входит в пакет и переносится в пакет B. Хаб не знает, какие типы симметричны: это знает агент по
|
||||
`/issueLinkType`. Ручная настройка списка типов в хабе отвергнута, она дублировала бы Jira. Цена ошибки: при
|
||||
потерянном отчёте команда `ensure_link` к «связанной» задаче, связанной в обратную сторону, подтверждается не по
|
||||
состоянию, а довыдачей агенту, и агент отвечает existed. Лишний вызов Jira, данные не страдают.
|
||||
- R-2 (design #1): обрезаются все строковые параметры, кроме `summary`, `description`, `body`, `goal`. Обоснование:
|
||||
пробелы по краям идентификатора всегда ошибка ввода, а в тексте могут быть значимы. Цена ошибки: тема с пробелами
|
||||
по краям уходит в Jira как есть, Jira обрезает её сама.
|
||||
- R-3 (design #2): имя вложения приводится к NFC, а не к NFD. NFC — форма, в которой имена приходят из Jira и
|
||||
набираются с клавиатуры. Цена ошибки: имя вложения в Jira будет в NFC, визуально то же самое.
|
||||
- R-4 (design #7): ошибка доступа к файлу хранилища трактуется как «файла нет», а не как отдельная ошибка API.
|
||||
Обоснование: путь «файла нет» уже понятен агенту и клиентам. Цена ошибки: причина видна только в логе хаба.
|
||||
- R-5 (design #9): `_recover` чинит только состояния, однозначно определяемые по sha256 манифеста. Остальные
|
||||
оставляет как есть с предупреждением в лог. Цена ошибки: в редком сочетании сбоев владелец публикует ещё раз.
|
||||
- R-6 (design #10): `base_jql` и `board_id` удаляются через механизм устаревших ключей, без новой версии схемы.
|
||||
Это тот же путь, что у `status_labels`, и при каждом старте ключи, вставленные старым процессом, убираются снова.
|
||||
Цена ошибки: нет.
|
||||
45
openspec/changes/hub-backend-fixes/proposal.md
Normal file
45
openspec/changes/hub-backend-fixes/proposal.md
Normal file
@ -0,0 +1,45 @@
|
||||
## Why
|
||||
|
||||
25.10.2026 сверка открытых follow-up issue с кодом подтвердила дефекты бэкенда хаба. Один из них уже приводит к
|
||||
неверной команде агенту: ключ с пробелами из `ensure_rank` уходит в Jira как есть. Остальные проявляются при
|
||||
нестандартном входе: на кривой запрос хаб отвечает 500 вместо понятного 422, один и тот же кириллический файл
|
||||
превращается в два вложения, при пустом `SDLC_HUB_HOME` данные пишутся в рабочий каталог процесса, после прерванной
|
||||
публикации агента может потеряться прежняя версия. Владелец с ними не сталкивался и не хочет столкнуться. Это пакет C
|
||||
сверки (sdlc-hub#129).
|
||||
|
||||
## What Changes
|
||||
|
||||
- **Параметры команд** (#111 T-F1, #49 M-4): при постановке пакета хаб приводит параметры к каноническому виду.
|
||||
Идентификаторы (ключи, имена спринтов, тип связи, логин, даты, маркер комментария, id вложения) и элементы списков
|
||||
обрезаются по краям. `filename` у `ensure_attachment` обрезается и приводится к NFC, в том числе имя по умолчанию из
|
||||
пути в MCP. Текст (`summary`, `description`, `body`, `goal`) хранится как задан.
|
||||
- **Цифры — только ASCII** (#86 F.1): `attachment_id`, id вложений в состоянии агента, версия агента — только `0-9`.
|
||||
Сейчас `"²"` проходит `isdigit()` и роняет `int()` с 500.
|
||||
- **Форма тела — 422, не 500** (#42 T2.1, #94 T8.1): элемент `issues` в `POST /agent/state` должен быть объектом со
|
||||
строковым `key`, `project` в `POST /wiki/sync` должен быть строкой.
|
||||
- **Файлы хранилища** (#49 M-3): ошибка доступа к файлу (`PermissionError`, `IsADirectoryError` и любая `OSError`)
|
||||
обрабатывается как «файла нет» с предупреждением в лог, а не 500.
|
||||
- **Пустой `SDLC_HUB_HOME`** (#102 T3.2): трактуется как незаданный, каталог данных по умолчанию.
|
||||
- **Публикация агента** (#39 T2.2, M-6): перед публикацией хаб восстанавливает `agent.py` из `agent.py.prev`, если
|
||||
прошлая публикация прервалась; годный `agent.py.prev` не перезаписывается содержимым, не совпадающим с манифестом.
|
||||
- **Уборка настроек** (#64 «выкатка»): ключи `base_jql` и `board_id` объявлены устаревшими и удаляются из `setting`
|
||||
при открытии хаба, как `status_labels`/`transitions_map`.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- O-3 из закрытой #4 (`check_confirmed` и симметричные типы связей): хаб не знает, какие типы связей симметричны, это
|
||||
знает только агент. Чинится в пакете B: агент передаёт симметричные типы, хаб учитывает их при подтверждении
|
||||
по состоянию. До этого последствие безвредное: вместо подтверждения по состоянию команда ждёт отчёта агента (R-1).
|
||||
- Изменения агентов, веба, MCP-инструментов (кроме имени вложения по умолчанию) и схемы базы.
|
||||
- Прочие маркеры исходных issue остаются в своих issue.
|
||||
|
||||
## Impact
|
||||
|
||||
- Спеки: `commands` (нормализация параметров, цифровые id), `hub-api` (форма тела, файлы хранилища, публикация,
|
||||
каталог данных), `data-model` (требование «Настройки»).
|
||||
- Код: `modules/jira/commands.py`, `modules/jira/hooks.py`, `modules/jira/mirror.py`, `modules/jira/mcp.py`,
|
||||
`modules/jira/fetch.py`, `modules/jira/attachments.py`, `modules/jira/settings.py`, `modules/wiki/api.py`,
|
||||
`core/files.py`, `core/paths.py`, `core/release.py`.
|
||||
- Живая база: при первом старте нового кода из `setting` уйдут `base_jql` и `board_id`. Резервная копия базы
|
||||
делается штатно.
|
||||
- После merge — комментарии в #111, #42, #94, #86, #49, #102, #39, #64 о закрытых маркерах; O-3 — в пакет B.
|
||||
32
openspec/changes/hub-backend-fixes/specs/commands/spec.md
Normal file
32
openspec/changes/hub-backend-fixes/specs/commands/spec.md
Normal file
@ -0,0 +1,32 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Нормализация параметров при постановке
|
||||
При постановке пакета (`POST /batches`, MCP `batch_create`, повтор команды) хаб SHALL сохранять параметры команд в
|
||||
каноническом виде, после всех проверок валидации. Строковые параметры обрезаются по краям, кроме текстовых `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: Имя вложения в 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», пакет не создан
|
||||
56
openspec/changes/hub-backend-fixes/specs/data-model/spec.md
Normal file
56
openspec/changes/hub-backend-fixes/specs/data-model/spec.md
Normal file
@ -0,0 +1,56 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Настройки
|
||||
Таблица `setting` SHALL хранить парами ключ–JSON: `project_key`, `field_ids`, `issue_types`, `priority_map`, `link_types`,
|
||||
`pacing`, `tokens` (агентский и клиентский), `settings_version` (увеличивается при любой
|
||||
правке), `backup_keep`, `auto_refresh_minutes`, `relay_dir` (абсолютный путь папки реле или `null`, по умолчанию `null`;
|
||||
относительный путь и не-строка при записи — 422; агенту не передаётся), `closed_statuses` (список имён статусов Jira, считающихся закрытыми, по умолчанию
|
||||
`["Готово", "Снято"]`; не-список при записи — 422; агенту не передаётся; на существующей базе появляется со значением
|
||||
по умолчанию при открытии хаба). Базовая JQL вычисляется из проектов, доска — свойство проекта (требование
|
||||
«Проекты»); на новой базе ключей `base_jql` и `board_id` нет. Ключи, которые модуль объявил устаревшими (у jira — `status_labels`, `transitions_map`, оставшиеся от v3
|
||||
`base_jql` и `board_id`), хаб SHALL удалять при каждом заполнении умолчаний, не меняя `settings_version`; агенту они
|
||||
не передаются. `GET /settings` SHALL отдавать вычисленную `base_jql` только для чтения, а запись `base_jql` или `board_id` через
|
||||
`PUT /settings` — 422 с пояснением. Спринты агент читает с досок проектов, отдельной настройки имён спринтов нет. Начальные значения SHALL импортироваться из `admin/jira-sync/config.example.json`
|
||||
командой `sdlc-hub init`; пароль и логин Jira в настройках MUST NOT присутствовать. `issue_types` — объект
|
||||
`{task, epic, bug}`, у каждого `{name, id}` (`id` может быть `null`); `init` берёт их из ключей конфига
|
||||
`issue_type_<тип>` и `issue_type_<тип>_id`; `bug` попадает в `issue_types`, только если в конфиге есть хотя бы
|
||||
один из ключей `issue_type_bug`, `issue_type_bug_id` (`task` и `epic` — как раньше, всегда).
|
||||
|
||||
#### Scenario: Правка настроек поднимает версию
|
||||
- **WHEN** через API изменён `pacing`
|
||||
- **THEN** `settings_version` увеличен, следующий `claim` отдаёт агенту новую версию
|
||||
|
||||
#### Scenario: Конфиг без ключей ошибки
|
||||
- **WHEN** `sdlc-hub init --config` с `issue_type_task`, `issue_type_epic`, без `issue_type_bug`
|
||||
- **THEN** `issue_types` содержит `task` и `epic`, `bug` нет; постановка `ensure_issue` с `issue_type = bug` — 422
|
||||
|
||||
#### Scenario: Неверный relay_dir
|
||||
- **WHEN** `PUT /settings` с `relay_dir = "rshb/jira-bridge"`
|
||||
- **THEN** 422, значение не записано, `settings_version` не изменился
|
||||
|
||||
#### Scenario: Закрытые статусы по умолчанию
|
||||
- **WHEN** хаб открыл базу, в которой нет ключа `closed_statuses`
|
||||
- **THEN** `GET /settings` отдаёт `closed_statuses = ["Готово", "Снято"]`; `claim` агенту этого ключа не передаёт
|
||||
|
||||
#### Scenario: Запись базовой JQL
|
||||
- **WHEN** `PUT /settings {base_jql: "project = AURORA"}`
|
||||
- **THEN** 422 «базовая JQL собирается из проектов»; `settings_version` не изменился
|
||||
|
||||
#### Scenario: Создание проекта не трогает версию настроек
|
||||
- **WHEN** создан проект `STOCKS_DRRK`
|
||||
- **THEN** `settings_version` прежний; `GET /settings` отдаёт `base_jql` с обеими метками
|
||||
|
||||
#### Scenario: Ключи вернул процесс прежней версии
|
||||
- **WHEN** база v8, процесс со старым кодом (MCP-сессия до reconnect) вставил `status_labels = {}` и
|
||||
`transitions_map = {}`, затем вызван `ensure_defaults` текущего кода (старт хаба, новая MCP-сессия)
|
||||
- **THEN** ключей в `setting` нет, `settings_version` прежний
|
||||
|
||||
#### Scenario: Ключей статуса по факту нет
|
||||
- **WHEN** хаб открыл базу v8
|
||||
- **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 — свойство проекта»
|
||||
51
openspec/changes/hub-backend-fixes/specs/hub-api/spec.md
Normal file
51
openspec/changes/hub-backend-fixes/specs/hub-api/spec.md
Normal file
@ -0,0 +1,51 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### 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