Исправления финального ревью: тип доски нового проекта, нормализация labels_add, документация (#61)
I-1: boards() отдаёт type=null, если тип неизвестен у любого проекта доски. M-2: labels_add ensure_fields нормализуется к метке проекта. M-3: light_list вместо list_projects в _assign_boards. M-4/T5.4: пустой project — 422. M-1, M-5, T10.1-T10.7: README, ARCHITECTURE, CLAUDE.md, design.md (49-51), e2e. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
9c2c4d09e0
commit
ec4fb8b1e7
@ -15,11 +15,11 @@
|
||||
| `queue` | пакеты, проверки постановки (`_check_setup`: `bug` без настройки, файл вложения; `_check_project`: проверки пакета проекта — метка, чужие метки, задачи вне проекта; `_assign_boards`: `board_id` проекта у `ensure_sprint`), выдача (`claim`, `size` у `ensure_attachment` и `boards` у `refresh` — `deliver_params`), отчёты, отмена, повтор, автообновление | `commands`, `mirror`, `settings`, `files`, `projects`, `db` |
|
||||
| `backup` | копии файла базы, ротация | `paths` |
|
||||
| `files` | хранилище файлов вложений `<данные>/files/<sha256>` (каталог — параметр, `Hub.files_dir`): `put` (атомарно, одна копия на содержимое, mtime при повторе, предел 10 МБ, пустой — отказ), `path`/`exists`/`size`/`read` (только sha из 64 hex нижнего регистра), `sweep` (старше 30 дней, кроме файлов `ensure_attachment` в `queued`/`claimed`) | `paths` |
|
||||
| `api` | HTTP-сервер, маршруты, авторизация по токенам, фоновый `tick` (автообновление и уборка `files/` раз в сутки); маршруты файлов `POST /files`, `GET /agent/files/{sha256}` (каталог `Hub.files_dir`, 0700 при старте); раздача веб-приложения `/app…` (каталог `web/app` пакета, без токена) и перенаправление `/`, `/links`; снимок `GET /board` | `queue`, `mirror`, `settings`, `backup`, `files`, `relay`, `release` |
|
||||
| `api` | HTTP-сервер, маршруты, авторизация по токенам, фоновый `tick` (автообновление и уборка `files/` раз в сутки); маршруты файлов `POST /files`, `GET /agent/files/{sha256}` (каталог `Hub.files_dir`, 0700 при старте); раздача веб-приложения `/app…` (каталог `web/app` пакета, без токена) и перенаправление `/`, `/links`; снимок `GET /board`; маршруты `/projects` | `queue`, `mirror`, `projects`, `settings`, `backup`, `files`, `relay`, `release` |
|
||||
| `relay` | реле общей папки (настройка `relay_dir`): поток в `serve`, файлы `req/` → свой HTTP (адрес `serve`, для `0.0.0.0` — `127.0.0.1`) → `resp/`; агент с `hub_url = folder:<путь>` ходит через него; HTTP-обработчики о нём не знают | `settings` |
|
||||
| `release` | опубликованная версия агента: `release/agent.py` + `manifest.json` в каталоге данных, `publish` (только вверх; запись с fsync, прежний файл на время записи — `agent.py.prev`, при сбое возвращается), `current` (проверка манифеста, предупреждение один раз на ошибку), `load` (с проверкой sha256 и UTF-8), подсказка `hint` для `agent_status` | `paths` |
|
||||
| `client` | HTTP-клиент хаба | `db`, `paths`, `settings` (только для `from_home`) |
|
||||
| `mcp_server` | инструменты MCP по stdio; привязка сессии к проекту (`jira-hub mcp --project <метка>`), инструменты `projects` и `project_create` (фрагмент `.mcp.json`); `batch_create` читает файлы `ensure_attachment.path` и загружает их `POST /files` до постановки пакета | `client`, `files` (только `MAX_SIZE`) |
|
||||
| `mcp_server` | инструменты MCP по stdio; привязка сессии к проекту (`jira-hub mcp --project <метка>`), инструменты `projects` и `project_create` (фрагмент `.mcp.json`); `batch_create` читает файлы `ensure_attachment.path` и загружает их `POST /files` до постановки пакета | `client`, `commands`, `files` (только `MAX_SIZE`) |
|
||||
| `importer` | разовый импорт CSV-пакета в заметки, сверка с зеркалом | `mirror`, `settings` |
|
||||
| `cli` | `init`, `serve`, `import`, `mcp`, `tokens`, `agent publish` | `api`, `mcp_server`, `importer`, `release`, `settings`, `db`, `paths` |
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
- Зеркало пишет только `mirror.apply_state` по запросу агента (и заметки через `PUT /issues/{key}/note`); из очереди и API задач напрямую не правят.
|
||||
- Хаб не знает пароль Jira и не обращается к Jira; `init` не переносит `auth`, `base_url`, `verify_ssl`, `ca_bundle`.
|
||||
- `queue` не создаёт команд сам: повтор после failed — новый пакет. Выданная без отчёта команда довыдаётся при следующем `claim` (до 3 выдач, затем failed).
|
||||
- `mcp_server` не импортирует `queue`/`mirror`/`db` напрямую и ходит только через `client`; из `files` берёт только предел
|
||||
- `mcp_server` не импортирует `queue`/`mirror`/`db`/`projects` напрямую и ходит только через `client`; из `files` берёт только предел
|
||||
`MAX_SIZE` (файлы в хранилище кладёт хаб по `POST /files`).
|
||||
- `commands`, `mirror`, `queue` не знают про HTTP; HTTP живёт только в `api`, `client` и `relay` (реле — клиент своего же HTTP).
|
||||
- Веб-приложение (`web/`) ходит в хаб только через HTTP API (`web/src/api/client.ts`); словари `kind_labels`, `link_types` и набор закрытых статусов `closed_statuses` берёт из `GET /settings` и своих копий не держит.
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
|
||||
## Архитектура
|
||||
- `src/jira_hub/` — весь код пакета, без разделения на ядро и адаптеры; чистая логика (`commands`, `mirror`, `queue`, `settings`) принимает соединение SQLite параметром.
|
||||
- `db` (схема, миграции) ← `settings`, `mirror`, `commands` ← `queue` ← `api` (HTTP, маршруты в `ROUTES`, фоновый `tick`, реле общей папки `relay` (поток в `serve`, настройка `relay_dir`)) ← `cli`.
|
||||
- `db` (схема, миграции) ← `settings`, `mirror`, `commands`, `projects` ← `queue` ← `api` (HTTP, маршруты в `ROUTES`, фоновый `tick`, реле общей папки `relay` (поток в `serve`, настройка `relay_dir`)) ← `cli`.
|
||||
- `files` (хранилище вложений `files/<sha256>`, каталог — параметр `Hub.files_dir`) ← `queue`, `api`; `mcp_server` берёт из него только предел `MAX_SIZE`.
|
||||
- `client` (HTTP-клиент хаба) ← `mcp_server` (stdio, инструменты MCP); `importer` → `mirror`/`settings`; `backup` вызывается из `api` и из `db` (копия перед миграцией схемы).
|
||||
- Подробная карта модулей и границ — `ARCHITECTURE.md`.
|
||||
@ -24,7 +24,7 @@
|
||||
- `uv sync` — установка окружения.
|
||||
- `uv run jira-hub init --config PATH [--force]` — настройки проекта из `config.example.json`, создание токенов.
|
||||
- `uv run jira-hub serve [--host] [--port]` — локальный запуск хаба.
|
||||
- Веб-приложение — `http://127.0.0.1:8790/app/` (доска — `/app/board`, задачи — `/app/issues`; вход по клиентскому токену); `/` и `/links` перенаправляют на доску.
|
||||
- Веб-приложение — `http://127.0.0.1:8790/app/` (доска — `/app/board?project=<метка>`, задачи — `/app/issues?project=<метка>`; вход по клиентскому токену); `/` и `/links` перенаправляют на доску.
|
||||
- `make web` — сборка приложения (после `git pull` на Mac mini, до перезапуска хаба); `make check` — pytest + lint и тесты фронта; `cd web && npm run dev` — разработка с прокси на тестовый хаб `bash web/scripts/test-hub.sh` (:8791); `cd web && npm run e2e` — Playwright; `cd web && npm run golden` — перегенерировать эталоны доски из `links.html@3931a88` (только при смене фикстур).
|
||||
- `uv run jira-hub import FOLDER` — импорт CSV-пакета `admin/jira-sync`; `uv run jira-hub mcp [--project <метка>]` — MCP по stdio (с привязкой сессии к проекту); `uv run jira-hub tokens` — показать токены.
|
||||
- `uv run jira-hub agent publish PATH` — опубликовать версию агента для самообновления (только вверх).
|
||||
|
||||
@ -173,10 +173,10 @@ uv run jira-hub agent publish ../jira-agent/src/agent.py
|
||||
| `GET /agent/known-keys` | агент | ключи зеркала |
|
||||
| `GET /agent/release` | агент | опубликованная версия агента `{version, sha256, content}`; 404 `no_release` |
|
||||
| `GET /agent/files/{sha256}` | агент | файл вложения `{file, size, content}` (`content` — base64); 404 `no_file` |
|
||||
| `GET /projects`, `POST /projects` | клиент | проекты `{projects: […]}` (метка, название, `board_id`, `board_type`, число задач) / завести `{label, name, board_id?, author?}` → 201 `{project, batch_id}` и пакет «Новый проект <метка>» (`refresh` по общей JQL); 409 `project_exists`, 422 `bad_project` |
|
||||
| `GET /projects`, `POST /projects` | клиент | проекты `{projects: […]}` (метка, название, `board_id`, `board_type`, число задач) / завести `{label, name, board_id?, author?}` → 201 `{project, batch_id}` и пакет «Новый проект <метка>» (`refresh` по общей JQL); 409 `project_exists`, 422 `bad_project`, 422 `bad_batch` (постановка `refresh` отказана) |
|
||||
| `GET /issues` | клиент | фильтры `project` (метка; неизвестная — 404), `sprint`, `assignee`, `work_kind`, `fact_status`, `status`, `label`, `epic`, `neighbor`, `q`, `hide_closed` (`1` — без задач в статусах из настройки `closed_statuses`, по умолчанию «Готово», «Снято»), `limit`, `offset` |
|
||||
| `GET /issues/{key}` | клиент | задача целиком |
|
||||
| `GET /board` | клиент | `?project=<метка>` (неизвестная — 404): снимок для доски — задачи проекта и соседи по связям, с `fact_status`/`work_kind`, все связи, `last_sync` — последний успешный полный прогон |
|
||||
| `GET /board` | клиент | `?project=<метка>` (неизвестная — 404): снимок для доски — свои задачи проекта, соседи (связи в обе стороны) и эпики своих задач, с `fact_status`/`work_kind`; связи — те, у которых хотя бы один конец — своя задача проекта (без `project` — все задачи и связи), `last_sync` — последний успешный полный прогон |
|
||||
| `GET /`, `GET /links` | — | перенаправление на `/app/board` |
|
||||
| `PUT /issues/{key}/note` | клиент | заметки (частично) |
|
||||
| `POST /batches`, `GET /batches`, `GET /batches/{id}` | клиент | пакеты команд; `project` в теле `POST /batches` — пакет проекта (проверки метки, ограничение задач проектом), `?project=` у `GET /batches` — только пакеты проекта |
|
||||
@ -218,6 +218,11 @@ launchctl kickstart -k gui/$(id -u)/ru.aepif.jira-hub
|
||||
исправляет только владелец, прямо в базе хаба: `UPDATE project SET board_id = <номер>, board_type = NULL WHERE label = '<метка>'`
|
||||
(тип доски определит следующий `refresh`).
|
||||
|
||||
После перезапуска хаба: нажать «Обновить из Jira», затем `projects` в MCP должен показать у AWPCA тип доски `scrum`
|
||||
(до этого `ensure_sprint` AWPCA отвечает 422 «тип доски ещё не известен»). Между `git pull` и перезапуском хаба не
|
||||
открывать новые сессии MCP: новый MCP/CLI мигрирует базу под работающим старым хабом (миграция v4 ему не вредит, но
|
||||
лучше без неё).
|
||||
|
||||
Без `make web` хаб на `/app…` отвечает 503 с подсказкой. Шрифты (IBM Plex) входят в сборку: приложение
|
||||
работает без доступа в интернет. Раскладка доски и вход сохраняются: приложение
|
||||
читает те же ключи `localStorage`, что прежняя страница. Разработка: `bash web/scripts/test-hub.sh` (тестовый хаб
|
||||
|
||||
@ -230,7 +230,8 @@
|
||||
`batch_status/wait/cancel` по id — без ограничения.** Обоснование: список не засоряется чужими пакетами, а явный id —
|
||||
осознанный запрос. Цена ошибки: фильтр списка.
|
||||
14. 2026-10-19 — **миграция v4 не удаляет `base_jql` и `board_id` из `setting`; `board_id` остаётся запасной доской
|
||||
агента; удаление — follow-up.** Обоснование: базу мигрирует любой процесс новой версии (MCP, CLI), возможно — под
|
||||
агента; удаление — follow-up. `GET/PUT /settings` по-прежнему отдаёт `board_id` как есть (запасная доска агента), а
|
||||
`base_jql` — вычисляемый из меток проектов.** Обоснование: базу мигрирует любой процесс новой версии (MCP, CLI), возможно — под
|
||||
работающим старым хабом (находка builder check). Цена ошибки: два мёртвых ключа до follow-up.
|
||||
15. 2026-10-19 — **миграция переписывает `params.jql` у `refresh` в `queued`/`claimed` со старой базовой JQL.**
|
||||
Обоснование: иначе прогон из очереди не признается полным. Цена ошибки: одна строка миграции.
|
||||
@ -344,3 +345,14 @@
|
||||
48. 2026-10-19 — **эталон `hub-empty.expected.json` не перегенерируется (его пишет прежняя страница, где `hasSprint` всегда
|
||||
`true`): тест сравнивает с ним, подменив `hasSprint` на `false`.** Обоснование: решение 12 меняет смысл `hasSprint`;
|
||||
эталон — снимок прежней страницы. Цена ошибки: эталон расходится с прежней страницей в одном поле.
|
||||
49. 2026-10-19 — **`boards()` отдаёт `type = null`, если тип неизвестен хотя бы у одного проекта доски (иначе — общий
|
||||
известный тип; при расхождении — первый по порядку проектов).** Обоснование: спека «board_type только по результату
|
||||
refresh»; агент типы известных досок не перезапрашивает, поэтому новый проект на уже известной доске оставался
|
||||
с `board_type = NULL` навсегда, и его `ensure_sprint` получал 422; `record_board_types` проставляет тип всем проектам
|
||||
доски. Цена ошибки: проект на известной доске без спринтов до ручной правки базы.
|
||||
50. 2026-10-19 — **в пакете проекта метки `labels_add` у `ensure_fields`, равные P без учёта регистра, заменяются на P
|
||||
(повтор P убирается), как в `ensure_issue`.** Обоснование: единообразие с `ensure_issue`; иначе в Jira уходит
|
||||
вариант метки в чужом регистре. Цена ошибки: лишняя метка-двойник у задачи.
|
||||
51. 2026-10-19 — **`queue._assign_boards` берёт проекты из `projects.light_list` (label, board_id, board_type) без
|
||||
`issues_count`.** Обоснование: подсчёт задач по всему зеркалу при каждой постановке `ensure_sprint` не нужен.
|
||||
Цена ошибки: нет (поведение и порядок те же).
|
||||
|
||||
@ -288,6 +288,8 @@ def h_batches_post(hub, conn, m, q, body):
|
||||
raise ApiError(422, "bad_batch", "commands — список команд")
|
||||
if body.get("project") is not None and not isinstance(body["project"], str):
|
||||
raise ApiError(422, "bad_batch", "project — строка")
|
||||
if body.get("project") == "":
|
||||
raise ApiError(422, "bad_batch", "project: пустая строка")
|
||||
try:
|
||||
return 201, queue.create_batch(conn, body["title"], body.get("author") or "api", body.get("commands"),
|
||||
files_dir=hub.files_dir, project=body.get("project"))
|
||||
@ -334,6 +336,8 @@ def h_command_retry(hub, conn, m, q, body):
|
||||
raise ApiError(422, "bad_retry", "author — строка")
|
||||
if body.get("project") is not None and not isinstance(body["project"], str):
|
||||
raise ApiError(422, "bad_retry", "project — строка")
|
||||
if body.get("project") == "":
|
||||
raise ApiError(422, "bad_retry", "project: пустая строка")
|
||||
try:
|
||||
return 201, queue.retry_command(conn, _int(m), body.get("author") or "api", files_dir=hub.files_dir,
|
||||
project=body.get("project"))
|
||||
|
||||
@ -74,6 +74,11 @@ def list_projects(conn) -> list[dict]:
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
def light_list(conn) -> list[dict]:
|
||||
"""Проекты без issues_count (label, board_id, board_type), порядок как у list_projects."""
|
||||
return [dict(r) for r in conn.execute("SELECT label, board_id, board_type FROM project ORDER BY created_at, label")]
|
||||
|
||||
|
||||
def labels(conn) -> list[str]:
|
||||
return [r["label"] for r in conn.execute("SELECT label FROM project ORDER BY created_at, label")]
|
||||
|
||||
@ -88,12 +93,15 @@ def base_jql(conn) -> str | None:
|
||||
|
||||
|
||||
def boards(conn) -> list[dict]:
|
||||
"""Доски проектов без повторов (порядок первого проекта); type — первый известный среди проектов доски."""
|
||||
"""Доски проектов без повторов (порядок первого проекта). type — None, если тип неизвестен хотя бы у одного
|
||||
проекта доски (агент тогда запросит его, record_board_types проставит всем); иначе первый известный."""
|
||||
out: dict[int, str | None] = {}
|
||||
unknown: set[int] = set()
|
||||
for r in conn.execute("SELECT board_id, board_type FROM project WHERE board_id IS NOT NULL ORDER BY created_at, label"):
|
||||
if out.get(r["board_id"]) is None:
|
||||
out[r["board_id"]] = r["board_type"]
|
||||
return [{"id": i, "type": t} for i, t in out.items()]
|
||||
out.setdefault(r["board_id"], r["board_type"])
|
||||
if r["board_type"] is None:
|
||||
unknown.add(r["board_id"])
|
||||
return [{"id": i, "type": None if i in unknown else t} for i, t in out.items()]
|
||||
|
||||
|
||||
def record_board_types(conn, found) -> int:
|
||||
|
||||
@ -74,7 +74,7 @@ def _assign_boards(conn, cmds) -> None:
|
||||
"""Доска проекта задачи в params ensure_sprint (design #7): метки временного ключа — из его ensure_issue в пакете,
|
||||
иначе из зеркала; 422 — задача вне проектов, разные доски, нет доски, канбан, тип ещё не известен."""
|
||||
temp_labels = {c["params"]["temp_key"]: c["params"].get("labels") or [] for c in cmds if c["type"] == "ensure_issue"}
|
||||
found = projects.list_projects(conn)
|
||||
found = projects.light_list(conn)
|
||||
for i, c in enumerate(cmds, 1):
|
||||
if c["type"] != "ensure_sprint":
|
||||
continue
|
||||
@ -127,9 +127,15 @@ def _check_project(conn, cmds, project) -> list[dict]:
|
||||
if t == "ensure_fields":
|
||||
if project.casefold() in {l.casefold() for l in p.get("labels_remove") or []}:
|
||||
raise C.CommandError(f"команда {i}: снять метку своего проекта {project} нельзя")
|
||||
added = []
|
||||
for l in p.get("labels_add") or []:
|
||||
if l.casefold() in others:
|
||||
raise C.CommandError(f"команда {i}: метка другого проекта: {l}")
|
||||
l = project if l.casefold() == project.casefold() else l
|
||||
if l not in added:
|
||||
added.append(l)
|
||||
if "labels_add" in p:
|
||||
p["labels_add"] = added
|
||||
if not own(p["key"]):
|
||||
raise C.CommandError(f"команда {i}: {p['key']} не в проекте {project}")
|
||||
out.append({**c, "params": p})
|
||||
|
||||
@ -894,6 +894,9 @@ def test_project_batches_api(hub, add_project): # #61, T
|
||||
with pytest.raises(HubError) as e:
|
||||
c.post("/batches", {"title": "x", "project": 5, "commands": cmd("A-2")})
|
||||
assert e.value.status == 422
|
||||
with pytest.raises(HubError) as e:
|
||||
c.post("/batches", {"title": "x", "project": "", "commands": cmd("A-2")})
|
||||
assert e.value.status == 422 and "project: пустая строка" in e.value.detail
|
||||
mine = c.post("/batches", {"title": "x", "project": "STOCKS_DRRK", "commands": cmd("A-2")})
|
||||
c.post("/batches", {"title": "y", "commands": cmd("A-1")})
|
||||
assert [b["id"] for b in c.get("/batches", project="STOCKS_DRRK")["batches"]] == [mine["id"]]
|
||||
@ -907,6 +910,9 @@ def test_project_batches_api(hub, add_project): # #61, T
|
||||
with pytest.raises(HubError) as e:
|
||||
c.post(f"/commands/{cid}/retry", {"project": 5})
|
||||
assert e.value.status == 422
|
||||
with pytest.raises(HubError) as e:
|
||||
c.post(f"/commands/{cid}/retry", {"project": ""})
|
||||
assert e.value.status == 422 and "project: пустая строка" in e.value.detail
|
||||
assert len(c.get("/batches", limit=50)["batches"]) == before
|
||||
r = c.post(f"/commands/{cid}/retry", {})
|
||||
assert c.get(f"/batches/{r['id']}")["project"] == "AWPCA"
|
||||
|
||||
@ -10,6 +10,31 @@ def conn(open_db):
|
||||
return c
|
||||
|
||||
|
||||
def test_boards_type_unknown_if_any_project_unknown(conn): # I-1: новый проект на известной доске
|
||||
projects.create(conn, "AWPCA", "a", 4216)
|
||||
projects.record_board_types(conn, {"4216": "scrum"})
|
||||
projects.create(conn, "NEW", "n", 4216)
|
||||
assert projects.get(conn, "NEW")["board_type"] is None
|
||||
assert projects.boards(conn) == [{"id": 4216, "type": None}]
|
||||
projects.record_board_types(conn, {"4216": "scrum"})
|
||||
assert projects.boards(conn) == [{"id": 4216, "type": "scrum"}]
|
||||
assert [p["board_type"] for p in projects.list_projects(conn)] == ["scrum", "scrum"]
|
||||
|
||||
|
||||
def test_boards_diverged_types_first_by_order(conn):
|
||||
projects.create(conn, "A", "a", 7); projects.create(conn, "B", "b", 7)
|
||||
conn.execute("UPDATE project SET board_type = 'kanban' WHERE label = 'B'")
|
||||
conn.execute("UPDATE project SET board_type = 'scrum' WHERE label = 'A'")
|
||||
assert projects.boards(conn) == [{"id": 7, "type": "scrum"}]
|
||||
|
||||
|
||||
def test_light_list_same_order_no_count(conn):
|
||||
projects.create(conn, "B", "b", 1); projects.create(conn, "A", "a")
|
||||
got = projects.light_list(conn)
|
||||
assert [p["label"] for p in got] == [p["label"] for p in projects.list_projects(conn)]
|
||||
assert all("issues_count" not in p and set(p) == {"label", "board_id", "board_type"} for p in got)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("label", ["", "STOCKS DRRK", "A\tB", "A,B", 'A"B', "A'B", "A(B", "A)B", "X" * 101, 5, None])
|
||||
def test_bad_label(conn, label):
|
||||
with pytest.raises(projects.ProjectError) as e:
|
||||
|
||||
@ -701,6 +701,12 @@ def test_project_label_case_variant_replaced(conn, add_project): # сво
|
||||
assert queue.batch_view(conn, b["id"])["commands"][0]["params"]["labels"] == [S] # без повтора P
|
||||
|
||||
|
||||
def test_project_ensure_fields_labels_add_case_normalized(conn, add_project): # M-2
|
||||
two(conn, add_project)
|
||||
b = queue.create_batch(conn, "x", "claude", [fields("AURORA-1", labels_add=["stocks_drrk", "kind-dev", S])], project=S)
|
||||
assert queue.batch_view(conn, b["id"])["commands"][0]["params"]["labels_add"] == [S, "kind-dev"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("cmd, msg", [
|
||||
(new_issue(["awpca"]), "команда 1: метка другого проекта: awpca"), # «Чужая метка проекта»
|
||||
(fields("AURORA-1538"), "команда 1: AURORA-1538 не в проекте STOCKS_DRRK"), # «Задача другого проекта»
|
||||
|
||||
@ -28,8 +28,8 @@ test.describe.serial("проекты", () => {
|
||||
await d.getByLabel("Название").fill("Запасы ДРРК");
|
||||
await d.getByRole("button", { name: "Создать" }).click();
|
||||
await expect(page).toHaveURL(/\?project=STOCKS_DRRK/);
|
||||
await expect(page.locator(".board .card[data-key]")).toHaveCount(0);
|
||||
await expect(page.getByRole("status")).toContainText(/пакет #\d+/);
|
||||
await expect(page.locator(".board .card[data-key]")).toHaveCount(0);
|
||||
await agentServeRefresh(); // иначе пакет держит «Обновить из Jira» в других файлах на общем тестовом хабе
|
||||
await expect(page.getByRole("status")).toContainText("выполнен");
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user