fix(1c-analyst): поле Description (не Content) + обязательная верификация GET-ом

Критичный баг-фикс по режиму «Пожелания из совещания».

Проблема: сервер Devprom на POST /issue/items молча игнорирует поле
Content, упомянутое в публичной документации /docs/8835.html.
Запись создаётся с пустым телом, HTTP 200, UID возвращается —
никакого сигнала об ошибке. Правильное имя поля для тела пожелания
на эндпоинте /issue/items — Description.

Выявлено при проверке U-6186 (Артур): поле Description пустое,
хотя POST вернул валидный UID. Та же проблема обнаружилась на 6
пожеланиях Тепловин U-6189..U-6194 — все с пустым телом.
Пересоздано как U-6198..U-6203 с полем Description, размер тела
1565–2172 символа, верификация пройдена.

Правки скилла:
- devprom-alm-api.md §0: новый блок «Критично: сервер молча отбрасывает
  незнакомые поля» + обязательная схема контрольного GET с ассертами.
  Список известных тихих ловушек (Content вместо Description, Type.Id=''
  в request/items, Requirement в POST).
- devprom-alm-api.md §2: рецепт с Description + встроенные ассерты.
- devprom-alm-api.md §3: справочник полей — Description с пояснением
  про Content и документацию, требование GET-проверки.
- devprom-alm-api.md appendix: curl-примеры с Description.
- meeting-wishes-extraction.md Шаг 6: Description в инструкции + ссылка
  на секцию «тихое отбрасывание».
- meeting-wishes-extraction.md Шаг 7: жёсткая формулировка
  «Ответ POST — недостаточное подтверждение успеха», ассерты
  на префикс UID, содержимое Description, Priority, Function;
  визуальная проверка в UI.
- meeting-wishes-extraction.md Python-шаблон: Description + ассерты
  на размер тела (>100 символов).
This commit is contained in:
creator
2026-04-22 07:02:53 +00:00
parent dee7d51c19
commit 7365242875
2 changed files with 93 additions and 31 deletions
+46 -9
View File
@@ -36,6 +36,36 @@
- **PATCH не поддерживается** — HTTP 405. Обновление только через PUT.
- **Успешный ответ на создание:** иногда возвращается как объект, иногда — как массив с одним элементом. Учитывать оба случая.
### ⚠ Критично: сервер молча отбрасывает незнакомые поля
Devprom REST API **не возвращает ошибку**, если имя поля в теле POST
не соответствует схеме сущности. Поле просто игнорируется, запись
создаётся с остальными полями, HTTP 200, `UID` возвращается.
**Следствие:** ответ POST нельзя считать подтверждением, что данные
сохранены. Успешный HTTP 200 + валидный JSON с `UID` означают только
что запись появилась, но часть полей может быть потеряна.
**Правило:** после КАЖДОГО POST на `/issue/items` (и любой другой
сущности, где критично содержимое) делать контрольный GET и глазами
или ассертом проверять реально сохранённые значения. Минимум:
```python
code, text = call("GET", f"issue/items/{new_id}")
g = json.loads(text)
assert g["UID"].startswith("U-"), "неверный префикс UID"
assert len(g.get("Description","")) > 100, "тело пожелания пустое"
# опционально: проверить Caption, Priority.Id, Function.Id, Company.Id
```
Известные «тихие ловушки»:
- `Content` вместо `Description` в теле POST на `/issue/items`
поле игнорируется, запись создаётся с пустым телом.
- `Type: {"Id":""}` при POST на `/request/items` → игнорируется,
сервер ставит `Type=397` (Доработка).
- `Requirement` при POST на `/issue/items` → игнорируется
(привязку делать через PUT или в UI).
## 1. Пожелания vs Заявки — КРИТИЧНО
В официальной схеме Devprom это **две разные сущности** с разными
@@ -74,14 +104,20 @@ _Проверено эмпирически:_ `/request/items` при POST **вс
```python
post_body = {
"Caption": "<заголовок>",
"Content": "<html-описание>", # по документации; Description тоже принимается
"Priority": {"Id": "2"}, # 1=Критично, 2=Высокий, 3=Обычный
"Author": {"Id": "<issueauthor.id>"},
"Function": {"Id": "<feature.id>"}, # подсистема (группировка)
"Company": {"Id": "<company.id>"}, # опционально: клиент-источник
"Caption": "<заголовок>",
"Description": "<html-тело>", # ← именно Description, НЕ Content
"Priority": {"Id": "2"}, # 1=Критично, 2=Высокий, 3=Обычный
"Author": {"Id": "<issueauthor.id>"},
"Function": {"Id": "<feature.id>"}, # подсистема (группировка)
"Company": {"Id": "<company.id>"}, # опционально: клиент-источник
}
# POST {BASE}/issue/items → {UID: "U-xxxx", Id: "xxxx", ...}
# ⚠ ОБЯЗАТЕЛЬНЫЙ контрольный GET — сервер может молча отбросить поля
code, text = call("GET", f"issue/items/{new_id}")
g = json.loads(text)
assert g["UID"].startswith("U-"), f"префикс не U-: {g['UID']}"
assert len(g.get("Description","")) > 100, "тело пожелания пустое"
```
Поля `Requirement` и `RequirementDocument` в теле **не передаются**:
@@ -106,7 +142,7 @@ Egress-прокси периодически роняет запросы с HTTP
| Поле | Тип / формат | Обязательность | Комментарий |
|-----------------------|------------------------------|----------------|-------------|
| `Caption` | string | обязательно | Заголовок пожелания |
| `Content` | HTML-string | обязательно | Описание. Разрешены: `<p> <b> <ul> <ol> <li> <a> <blockquote> <code>`. Поле `Description` на входе тоже принимается как алиас. |
| `Description` | HTML-string | обязательно | **Тело пожелания.** Имя поля — `Description`, не `Content`. Эмпирически: `Content` на `/issue/items` при POST молча игнорируется (принимается, но не сохраняется). В публичной документации (`/docs/8835.html`) упомянуто поле `Content` — это относится к `/request/items` и/или документации в целом устаревшее; для `/issue/items` используйте только `Description`. Разрешённые теги HTML: `<p> <b> <ul> <ol> <li> <a> <blockquote> <code>`. **После POST обязательно делать GET и проверять `len(Description) > 100`** — сервер не возвращает ошибку при отброшенном поле. |
| `Priority.Id` | "1" / "2" / "3" | по смыслу | 1=Критично, 2=Высокий, 3=Обычный |
| `Author.Id` | issueauthor.id | обязательно | Автор пожелания. Справочник инстанс-уровня. |
| `Function.Id` | feature.id | рекомендовано | Группировка по подсистемам (создаются через `/feature/items`) |
@@ -255,8 +291,9 @@ curl -X POST \
curl -X POST \
-H "Devprom-Auth-Key: <KEY>" -H "Content-Type: application/json" \
https://<host>/pm/<proj>/api/latest/issue/items \
-d '{"Caption":"Пожелание","Content":"<p>Описание</p>","Priority":{"Id":"2"},"Author":{"Id":"1"},"Function":{"Id":"190"},"Company":{"Id":"146"}}'
-d '{"Caption":"Пожелание","Description":"<p>Тело</p>","Priority":{"Id":"2"},"Author":{"Id":"1"},"Function":{"Id":"190"},"Company":{"Id":"146"}}'
# → UID=U-xxxx, Type=пусто
# ⚠ После создания — GET и проверить len(Description) > 100
```
### Привязка к документу требований (не нужна в режиме «Пожелания из совещания»)
@@ -267,7 +304,7 @@ curl -X POST \
curl -X PUT \
-H "Devprom-Auth-Key: <KEY>" -H "Content-Type: application/json" \
https://<host>/pm/<proj>/api/latest/issue/items/6179 \
-d '{"Caption":"Пожелание","Content":"<p>Описание</p>","Priority":{"Id":"2"},"Author":{"Id":"1"},"Function":{"Id":"190"},"Requirement":{"Id":"4378"}}'
-d '{"Caption":"Пожелание","Description":"<p>Тело</p>","Priority":{"Id":"2"},"Author":{"Id":"1"},"Function":{"Id":"190"},"Requirement":{"Id":"4378"}}'
```
### Удаление
@@ -108,14 +108,20 @@ _«Если бы это пожелание взял в работу разраб
пустыми — пользователь вручную прикрепит их в UI, если/когда
появится профильное требование.
**Тело пожелания отправляется в поле `Description`, НЕ `Content`.**
Публичная документация Devprom (`/docs/8835.html`) упоминает `Content`,
но на эндпоинте `/issue/items` это поле молча игнорируется сервером
при POST. Эмпирически проверено — см. `references/devprom-alm-api.md`,
раздел 0 «Сервер молча отбрасывает незнакомые поля».
```python
post_body = {
"Caption": wish["caption"],
"Content": CUSTOMER_NOTE + wish["description"] + SOURCE_NOTE,
"Priority": {"Id": wish["priority"]}, # "1"/"2"/"3"
"Author": {"Id": AUTHOR_ID},
"Function": {"Id": wish["function_id"]},
"Company": {"Id": COMPANY_ID}, # опционально
"Caption": wish["caption"],
"Description": CUSTOMER_NOTE + wish["body"] + SOURCE_NOTE, # ← Description!
"Priority": {"Id": wish["priority"]}, # "1"/"2"/"3"
"Author": {"Id": AUTHOR_ID},
"Function": {"Id": wish["function_id"]},
"Company": {"Id": COMPANY_ID}, # опционально
}
# POST /issue/items → {UID: "U-xxxx", ...}
```
@@ -123,16 +129,33 @@ post_body = {
Подробный справочник полей и поведение API — в
`references/devprom-alm-api.md`.
### Шаг 7 — Верификация
### Шаг 7 — Верификация по факту (обязательно)
После загрузки:
**Ответ POST — недостаточное подтверждение успеха.** Сервер Devprom
возвращает HTTP 200 и валидный JSON с `UID` даже тогда, когда часть
полей была молча отброшена (например, из-за неправильного имени).
Верификация идёт по результату GET, а не по ответу POST.
- Открыть `/pm/<project>/module/requirements/issues` → все загруженные пожелания должны отображаться одним списком.
- UID у всех — префикс **`U-`**.
- У каждого заполнены `Priority`, `Function`, `Author`. Поля `Requirement`
и `RequirementDocument` пустые — это норма, привязку делает
пользователь вручную в UI.
- Открыть одну карточку случайно — проверить форматирование HTML, работу ссылок (mailto:), наличие цитат.
Для каждого созданного пожелания — контрольный GET и ассерты:
```python
code, text = call("GET", f"issue/items/{rid}")
g = json.loads(text)
assert g["UID"].startswith("U-"), f"неверный префикс: {g['UID']}"
assert len(g.get("Description","")) > 100, "тело пожелания пустое"
assert (g.get("Priority") or {}).get("Id") == expected_priority
assert (g.get("Function") or {}).get("Id") == expected_function_id
```
Дополнительно — после загрузки всей пачки открыть UI-раздел
`/pm/<project>/module/requirements/issues`, убедиться визуально:
- Все загруженные пожелания отображаются одним списком
- UID у всех — префикс `U-`
- Случайно выбранная карточка открывается, форматирование HTML работает,
ссылки (mailto:) кликабельны, цитаты в `<blockquote>` отрисованы
- Поля `Requirement` и `RequirementDocument` пустые — это норма,
привязку делает пользователь вручную в UI
### Шаг 8 — Сводка для пользователя
@@ -172,20 +195,22 @@ def call(method, path, body=None, retries=3):
for wish in WISHES:
post_body = {
"Caption": wish["caption"],
"Content": CUSTOMER_NOTE + wish["description"] + SOURCE_NOTE,
"Priority": {"Id": wish["priority"]},
"Author": {"Id": AUTHOR_ID},
"Function": {"Id": wish["function"]},
"Company": {"Id": COMPANY_ID}, # опционально
"Caption": wish["caption"],
"Description": CUSTOMER_NOTE + wish["description"] + SOURCE_NOTE, # ← Description
"Priority": {"Id": wish["priority"]},
"Author": {"Id": AUTHOR_ID},
"Function": {"Id": wish["function"]},
"Company": {"Id": COMPANY_ID}, # опционально
}
code, text = call("POST", "issue/items", post_body)
r = json.loads(text); r = r[0] if isinstance(r, list) else r
issue_id = r["Id"]
# проверка
# ⚠ ОБЯЗАТЕЛЬНАЯ верификация: сервер молча отбрасывает незнакомые поля.
# Ответа POST недостаточно — смотрим, что реально сохранилось.
_, t = call("GET", f"issue/items/{issue_id}")
g = json.loads(t)
assert g["UID"].startswith("U-"), f"префикс не U-: {g['UID']}"
assert g["UID"].startswith("U-"), f"префикс не U-: {g['UID']}"
assert len(g.get("Description","")) > 100, f"пустое тело: Id={issue_id}"
```
---