From ec4fb8b1e74de96a23f1564eb8815f1471025462 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Mon, 19 Oct 2026 22:06:41 +0400 Subject: [PATCH] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=84=D0=B8=D0=BD=D0=B0=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D0=BE=D0=B3=D0=BE=20=D1=80=D0=B5=D0=B2=D1=8C=D1=8E:=20?= =?UTF-8?q?=D1=82=D0=B8=D0=BF=20=D0=B4=D0=BE=D1=81=D0=BA=D0=B8=20=D0=BD?= =?UTF-8?q?=D0=BE=D0=B2=D0=BE=D0=B3=D0=BE=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA?= =?UTF-8?q?=D1=82=D0=B0,=20=D0=BD=D0=BE=D1=80=D0=BC=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20labels=5Fadd,=20=D0=B4=D0=BE?= =?UTF-8?q?=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=86=D0=B8=D1=8F=20?= =?UTF-8?q?(#61)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- ARCHITECTURE.md | 6 +++--- CLAUDE.md | 4 ++-- README.md | 9 +++++++-- openspec/changes/projects/design.md | 14 +++++++++++++- src/jira_hub/api.py | 4 ++++ src/jira_hub/projects.py | 16 ++++++++++++---- src/jira_hub/queue.py | 8 +++++++- tests/test_api.py | 6 ++++++ tests/test_projects.py | 25 +++++++++++++++++++++++++ tests/test_queue.py | 6 ++++++ web/e2e/projects.spec.ts | 2 +- 11 files changed, 86 insertions(+), 14 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 87600b0..4292119 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -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/` (каталог — параметр, `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` и своих копий не держит. diff --git a/CLAUDE.md b/CLAUDE.md index 6d8c44a..0352b92 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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/`, каталог — параметр `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` — опубликовать версию агента для самообновления (только вверх). diff --git a/README.md b/README.md index 692cce4..173de8a 100644 --- a/README.md +++ b/README.md @@ -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` (тестовый хаб diff --git a/openspec/changes/projects/design.md b/openspec/changes/projects/design.md index 87b28ab..55ba5e3 100644 --- a/openspec/changes/projects/design.md +++ b/openspec/changes/projects/design.md @@ -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` не нужен. + Цена ошибки: нет (поведение и порядок те же). diff --git a/src/jira_hub/api.py b/src/jira_hub/api.py index 2292b57..95362d0 100644 --- a/src/jira_hub/api.py +++ b/src/jira_hub/api.py @@ -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")) diff --git a/src/jira_hub/projects.py b/src/jira_hub/projects.py index a231d32..9c6461d 100644 --- a/src/jira_hub/projects.py +++ b/src/jira_hub/projects.py @@ -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: diff --git a/src/jira_hub/queue.py b/src/jira_hub/queue.py index e1343e0..a407b70 100644 --- a/src/jira_hub/queue.py +++ b/src/jira_hub/queue.py @@ -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}) diff --git a/tests/test_api.py b/tests/test_api.py index 3bfe694..47d0f8a 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -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" diff --git a/tests/test_projects.py b/tests/test_projects.py index 80251e2..d53b632 100644 --- a/tests/test_projects.py +++ b/tests/test_projects.py @@ -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: diff --git a/tests/test_queue.py b/tests/test_queue.py index a61b1f3..f4227fe 100644 --- a/tests/test_queue.py +++ b/tests/test_queue.py @@ -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"), # «Задача другого проекта» diff --git a/web/e2e/projects.spec.ts b/web/e2e/projects.spec.ts index 9671c8c..6a06741 100644 --- a/web/e2e/projects.spec.ts +++ b/web/e2e/projects.spec.ts @@ -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("выполнен"); });