e2e спринтов и бэклога, тестовый хаб с досками, документация (#108)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
611ed830c4
commit
fe1d5926a5
@ -101,6 +101,11 @@
|
||||
страницы модуля — `/app/<модуль>/…` (`/app/jira/links`, `/app/jira/issues[/{key}]`, `/app/jira/feed`, `/app/wiki`); прежние `/app/board`, `/app/jira/board`,
|
||||
`/app/issues`, `/app/issues/{key}`, `/app/feed` перенаправляются с сохранением `search` и `replace`. Пункты меню могут зависеть от проекта (`NavItem.when`, `visibleNav` в `shell/nav.tsx`): пока проект не определён, условный пункт скрыт; «не найдено» перечисляет только безусловные.
|
||||
|
||||
Экраны доски модуля jira — `web/src/modules/jira/agile/`: `model` (даты Jira, «осталось N дн.», склонение, состояние данных
|
||||
`boardState`), `columns` (раскладка задач по колонкам доски, «Без колонки»), `AgileGate` (тип доски проекта, загрузка, ошибка, три
|
||||
состояния данных), `BoardColumns` и страницы `BacklogPage` (`/jira/backlog`), `SprintPage` (`/jira/sprint`), `KanbanPage`
|
||||
(`/jira/kanban`). Данные — один запрос `GET /agile?project=<метка>` (`modules/jira/agile.py`); страницы только смотрят.
|
||||
|
||||
Модуль `wiki` (`web/src/modules/wiki/index.tsx`): страница «Вики» (`/app/wiki`): дерево (`Tree`) с поиском (`Search`), страница
|
||||
(`WikiPage`, `PageView` — `react-markdown` + `remark-gfm`, без `rehype-raw`; ссылки `page:` ведут на страницу вики, `attachment:`
|
||||
скачивают файл через `download.ts`, `urlTransform.ts` пропускает только эти схемы и http(s)), форма корня без привязки (`RootForm`),
|
||||
|
||||
@ -26,7 +26,7 @@ SDLC-хаб с модулями jira и wiki. Мост Jira для АРМ КА:
|
||||
- `uv sync` — установка окружения.
|
||||
- `uv run sdlc-hub init --config PATH [--force]` — настройки проекта из `config.example.json`, создание токенов.
|
||||
- `uv run sdlc-hub serve [--host] [--port]` — локальный запуск хаба.
|
||||
- Веб-приложение — `http://127.0.0.1:8790/app/` («Обзор» — `/app/?project=<метка>`, «Связи» (доска связей) — `/app/jira/links?project=<метка>`, задачи — `/app/jira/issues?project=<метка>`, лента изменений — `/app/jira/feed?project=<метка>`, вики — `/app/wiki?project=<метка>`; вход по клиентскому токену); `/` и `/links` перенаправляют на «Обзор», прежние `/app/board`, `/app/jira/board`, `/app/issues`, `/app/feed` — на страницы модуля Jira; пункты меню могут зависеть от проекта (`NavItem.when`).
|
||||
- Веб-приложение — `http://127.0.0.1:8790/app/` («Обзор» — `/app/?project=<метка>`, «Связи» (доска связей) — `/app/jira/links?project=<метка>`, задачи — `/app/jira/issues?project=<метка>`, «Бэклог» — `/app/jira/backlog?project=<метка>`, «Спринт» — `/app/jira/sprint?project=<метка>`, «Kanban» — `/app/jira/kanban?project=<метка>`, лента изменений — `/app/jira/feed?project=<метка>`, вики — `/app/wiki?project=<метка>`; вход по клиентскому токену); `/` и `/links` перенаправляют на «Обзор», прежние `/app/board`, `/app/jira/board`, `/app/issues`, `/app/feed` — на страницы модуля Jira; пункты меню могут зависеть от проекта (`NavItem.when`).
|
||||
- `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 sdlc-hub import FOLDER` — импорт CSV-пакета `admin/jira-sync`; `uv run sdlc-hub mcp <модуль> [--project <метка>]` — MCP модуля по stdio (модуль обязателен: `jira` — сервер `sdlc-jira`, `wiki` — сервер `sdlc-wiki`; с привязкой сессии к проекту); `uv run sdlc-hub tokens` — показать токены (клиентский и по агенту на модуль).
|
||||
- `uv run sdlc-hub agent publish [--module jira|wiki] PATH` — опубликовать версию агента модуля для самообновления (только вверх; по умолчанию `jira`; публикация лежит в `release/<модуль>/`).
|
||||
|
||||
14
README.md
14
README.md
@ -262,6 +262,15 @@ make web
|
||||
переводят её в новую схему, а хаб на прежнем коде с такой базой не работает. Старый фронт с новым API совместим, сборка может подождать. Откат на копию v6 из `backups/` —
|
||||
вместе с возвратом `release/jira/*` в `release/` (прежняя раскладка), иначе старый код не найдёт публикацию агента.
|
||||
|
||||
Экраны доски (только просмотр: ни перетаскивания, ни кнопок спринтов, ни смены статуса или ранга): «Бэклог»
|
||||
`/app/jira/backlog?project=<метка>` (активные и будущие спринты, бэклог, «Прошлые спринты (N)»), «Спринт»
|
||||
`/app/jira/sprint?project=<метка>[&sprint=<id>]` (колонки доски активного спринта; устаревший `sprint` — первый активный),
|
||||
«Kanban» `/app/jira/kanban?project=<метка>` (колонки доски, «Бэклог (N)» при включённом бэклоге Jira, выполненное за 14 дней).
|
||||
Пункты меню зависят от типа доски проекта: у scrum-проекта «Бэклог» и «Спринт», у kanban-проекта «Kanban»; пока тип не известен,
|
||||
пунктов нет. Адрес страницы чужого типа показывает «У проекта нет scrum-доски» / «У проекта нет kanban-доски» со ссылкой
|
||||
«Связи». Три состояния данных: «Нужен агент v15 и обновление из Jira» (доска ещё не прочитана), «Не удалось прочитать настройки
|
||||
доски из Jira» (колонок нет), сами данные. Карточка задачи открывается окном поверх экрана.
|
||||
|
||||
Проект выбирается выпадающим списком «Проект» в верхней панели (метка и название); пункт «+ Новый проект…» открывает окно
|
||||
(метка, название, необязательно номер доски Jira): после создания приложение переходит в новый проект и показывает ход
|
||||
пакета обновления. Текущий проект — параметр `project` в адресе; без него приложение подставляет последний выбранный
|
||||
@ -349,6 +358,11 @@ make web
|
||||
|
||||
## Эксплуатация
|
||||
|
||||
- Выкатка спринтов и бэклога (порядок обязателен): `git pull` → `launchctl kickstart -k gui/$(id -u)/ru.aepif.sdlc-hub`
|
||||
(владелец; миграция схемы v9) → `make web` → `/mcp` reconnect → `agent publish` агента v15 (`uv run sdlc-hub agent publish
|
||||
../jira-agent/src/agent.py`) → проверка полей Jira банка (ранг, оценка, спринты) → `field_ids.story_points` в настройках,
|
||||
если агент поле оценки не нашёл. `min_agent_version` поднимает владелец после выкатки v15 и после того, как `agent_status`
|
||||
покажет новую версию.
|
||||
- Резервные копии: перед первой записывающей командой пакета хаб копирует базу в `backups/<дата-время>-<batch>.sqlite`
|
||||
и хранит последние `backup_keep` копий (по умолчанию 30).
|
||||
- Файлы вложений: `files/<sha256>`, одна копия на содержимое, в резервные копии базы не входят. Раз в сутки (первый
|
||||
|
||||
81
web/e2e/agile.spec.ts
Normal file
81
web/e2e/agile.spec.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { login } from "./hub.ts";
|
||||
|
||||
// экраны доски: «Бэклог», «Спринт», «Kanban» (spec web-app «Спринты и бэклог»); данные — сид тестового хаба (R-27)
|
||||
const side = (page: import("@playwright/test").Page) => page.getByRole("complementary", { name: "Панель приложения" });
|
||||
|
||||
test("«Бэклог» AWPCA: секции, «Прошлые спринты (1)», щелчок открывает карточку", async ({ page }) => {
|
||||
await login(page, "/app/jira/backlog?project=AWPCA");
|
||||
await expect(page.getByRole("heading", { name: "AWPCA_Спринт 2" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "AWPCA_Спринт 3" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Бэклог", exact: true, level: 2 })).toBeVisible();
|
||||
const past = page.getByText("Прошлые спринты (1)");
|
||||
await expect(past).toBeVisible();
|
||||
await past.click();
|
||||
await page.getByText("AWPCA_Спринт 1").first().click();
|
||||
const row = page.locator(".agile-row").filter({ hasText: "AURORA-9001" }).last();
|
||||
await expect(row).toBeVisible();
|
||||
await row.click();
|
||||
await expect(page).toHaveURL(/\/app\/jira\/issues\/AURORA-9001/);
|
||||
await expect(page.getByRole("dialog")).toBeVisible();
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(page).toHaveURL(/\/app\/jira\/backlog/);
|
||||
await expect(page.getByRole("dialog")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("«Спринт» AWPCA: колонки доски, «осталось 7 дн.», карточка окном", async ({ page }) => {
|
||||
await login(page, "/app/jira/sprint?project=AWPCA");
|
||||
await expect(page.getByRole("heading", { name: "AWPCA_Спринт 2" })).toBeVisible();
|
||||
await expect(page.getByText("осталось 7 дн.")).toBeVisible();
|
||||
for (const name of ["К выполнению", "В работе", "Готово"]) {
|
||||
await expect(page.locator(".agile-col__name", { hasText: name })).toBeVisible();
|
||||
}
|
||||
await page.locator(".agile-card").filter({ hasText: "AURORA-9001" }).click();
|
||||
await expect(page).toHaveURL(/\/app\/jira\/issues\/AURORA-9001/);
|
||||
await expect(page.getByRole("dialog")).toBeVisible();
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(page).toHaveURL(/\/app\/jira\/sprint/);
|
||||
});
|
||||
|
||||
test("«Kanban»: «Бэклог (1)», колонки «В работе» и «Готово», подпись «за 14 дней»", async ({ page }) => {
|
||||
await login(page, "/app/jira/kanban?project=KANBAN_E2E");
|
||||
await expect(page.getByText("Бэклог (1)")).toBeVisible();
|
||||
await expect(page.locator(".agile-col__name", { hasText: "В работе" })).toBeVisible();
|
||||
await expect(page.locator(".agile-col__name", { hasText: "Готово" })).toBeVisible();
|
||||
await expect(page.locator(".agile-col__name", { hasText: "Бэклог" })).toHaveCount(0);
|
||||
await expect(page.getByText("за 14 дней")).toBeVisible();
|
||||
await expect(page.locator(".agile-card").filter({ hasText: "AURORA-9102" })).toBeVisible();
|
||||
await expect(page.locator(".agile-card").filter({ hasText: "AURORA-9103" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("без scrum-доски: /app/jira/backlog?project=KANBAN_E2E — «У проекта нет scrum-доски», ссылка «Связи»", async ({ page }) => {
|
||||
await login(page, "/app/jira/backlog?project=KANBAN_E2E");
|
||||
await expect(page.getByText("У проекта нет scrum-доски")).toBeVisible();
|
||||
await page.getByRole("link", { name: "Связи", exact: true }).last().click();
|
||||
await expect(page).toHaveURL(/\/app\/jira\/links\?project=KANBAN_E2E/);
|
||||
});
|
||||
|
||||
test("меню по проекту: у AWPCA «Бэклог» и «Спринт», у KANBAN_E2E — «Kanban»", async ({ page }) => {
|
||||
await login(page, "/app/jira/links?project=AWPCA");
|
||||
await expect(side(page).getByRole("link", { name: "Бэклог" })).toBeVisible();
|
||||
await expect(side(page).getByRole("link", { name: "Спринт" })).toBeVisible();
|
||||
await expect(side(page).getByRole("link", { name: "Kanban" })).toHaveCount(0);
|
||||
await page.goto("/app/jira/links?project=KANBAN_E2E");
|
||||
await expect(side(page).getByRole("link", { name: "Kanban" })).toBeVisible();
|
||||
await expect(side(page).getByRole("link", { name: "Бэклог" })).toHaveCount(0);
|
||||
await expect(side(page).getByRole("link", { name: "Спринт" })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("900 px: колонки «Спринта» прокручиваются внутри доски, страница без горизонтальной прокрутки", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 900, height: 900 });
|
||||
await login(page, "/app/jira/sprint?project=AWPCA");
|
||||
await expect(page.locator(".agile-col").first()).toBeVisible();
|
||||
const m = await page.evaluate(() => {
|
||||
const d = document.documentElement, c = document.querySelector<HTMLElement>(".agile-columns")!;
|
||||
return { doc: [d.scrollWidth, d.clientWidth], cols: [c.scrollWidth, c.clientWidth],
|
||||
gutter: c.getBoundingClientRect().left - document.querySelector(".agile")!.getBoundingClientRect().left };
|
||||
});
|
||||
expect(m.gutter, "колонки не получают второй отступ внутри страницы (T10.1)").toBe(0);
|
||||
expect(m.doc[0]).toBeLessThanOrEqual(m.doc[1]);
|
||||
expect(m.cols[0]).toBeGreaterThan(m.cols[1]);
|
||||
});
|
||||
@ -3,10 +3,11 @@
|
||||
Дополнения только для тестового хаба (описание, комментарии, команды) — из seed-extras.json рядом со скриптом."""
|
||||
import json
|
||||
import sys
|
||||
from datetime import date, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
from sdlc_hub.core import db, paths, settings
|
||||
from sdlc_hub.modules.jira import mirror
|
||||
from sdlc_hub.modules.jira import mirror, projects as jira_projects
|
||||
from sdlc_hub.modules.wiki import paths as wiki_paths, search as wiki_search, sync as wiki_sync
|
||||
|
||||
snap = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
||||
@ -25,7 +26,35 @@ def own_labels(i):
|
||||
issues = [{k: v for k, v in i.items() if k not in ("work_kind", "neighbor")} | {"id": i["key"]}
|
||||
| {"labels": own_labels(i)} | extra_issues.get(i["key"], {}) for i in snap["issues"]]
|
||||
links = [{"source_key": l["source"], "target_key": l["target"], "link_type": l["link_type"]} for l in snap["links"]]
|
||||
mirror.apply_state(conn, issues, links, neighbor_keys=[i["key"] for i in snap["issues"] if i["neighbor"]])
|
||||
# доски для экранов «Бэклог», «Спринт», «Kanban» (R-27): AWPCA — scrum 4216, KANBAN_E2E — kanban 5120
|
||||
SPRINTS = {"AWPCA_Спринт 1": 101, "AWPCA_Спринт 2": 102, "AWPCA_Спринт 3": 103}
|
||||
for n, i in enumerate(issues, 1):
|
||||
i["rank"] = f"0|i{n:04d}:"
|
||||
sid = SPRINTS.get(i.get("sprint"))
|
||||
i["sprint_ids"] = [] if sid is None else [101, 102] if i["key"] == "AURORA-9001" else [sid]
|
||||
if n % 2:
|
||||
i["story_points"] = float(n % 5 + 1)
|
||||
today = date.today()
|
||||
iso = lambda d, hhmm: f"{d.isoformat()}T{hhmm}:00.000+03:00"
|
||||
now_iso = iso(today, "12:00")
|
||||
issues += [{"key": k, "id": k, "summary": f"Задача канбана {k[-4:]}", "issue_type": "Задача", "status": st, "priority": "Средний",
|
||||
"labels": ["KANBAN_E2E"], "rank": f"0|k{n:04d}:", "sprint_ids": [], **({"jira_updated": now_iso} if st == "Готово" else {})}
|
||||
for n, (k, st) in enumerate([("AURORA-9101", "Backlog"), ("AURORA-9102", "В работе"), ("AURORA-9103", "Готово")], 1)]
|
||||
jira_projects.create(conn, "KANBAN_E2E", "Канбан e2e", 5120)
|
||||
boards = [
|
||||
{"id": 4216, "type": "scrum", "kanban_backlog": None,
|
||||
"columns": [{"name": "К выполнению", "statuses": ["Backlog", "К выполнению", "Ожидание", "Ожидание разработки"]},
|
||||
{"name": "В работе", "statuses": ["В работе"]}, {"name": "Готово", "statuses": ["Готово"]}],
|
||||
"sprints": [{"id": 101, "name": "AWPCA_Спринт 1", "state": "closed", "start_date": iso(today - timedelta(days=21), "09:00"),
|
||||
"end_date": iso(today - timedelta(days=8), "09:00"), "complete_date": iso(today - timedelta(days=8), "18:00"), "goal": None},
|
||||
{"id": 102, "name": "AWPCA_Спринт 2", "state": "active", "start_date": iso(today - timedelta(days=7), "09:00"),
|
||||
"end_date": iso(today + timedelta(days=7), "12:00"), "complete_date": None, "goal": "Тестовая цель спринта"},
|
||||
{"id": 103, "name": "AWPCA_Спринт 3", "state": "future", "start_date": None, "end_date": None, "complete_date": None, "goal": None}]},
|
||||
{"id": 5120, "type": "kanban", "kanban_backlog": True,
|
||||
"columns": [{"name": "Бэклог", "statuses": ["Backlog"]}, {"name": "В работе", "statuses": ["В работе"]},
|
||||
{"name": "Готово", "statuses": ["Готово"]}]},
|
||||
]
|
||||
mirror.apply_state(conn, issues, links, neighbor_keys=[i["key"] for i in snap["issues"] if i["neighbor"]], boards=boards)
|
||||
for i in snap["issues"]:
|
||||
note = {k: i[k] for k in ("work_kind",) if i.get(k)}
|
||||
if note:
|
||||
|
||||
@ -36,7 +36,7 @@
|
||||
.agile-row__assignee{flex-basis:100%}
|
||||
}
|
||||
/* «Спринт»: страница во всю ширину, колонки прокручиваются внутри контейнера */
|
||||
.agile--wide{padding-inline:var(--space-6);min-width:0}
|
||||
.agile--wide{min-width:0}
|
||||
.agile-columns{display:flex;align-items:flex-start;gap:var(--space-3);overflow-x:auto;padding-bottom:var(--space-2);min-width:0}
|
||||
.agile-col{flex:0 0 280px;min-width:0;display:flex;flex-direction:column;gap:var(--space-2);
|
||||
background:var(--color-bg-subtle);border:1px solid var(--color-border-default);border-radius:var(--radius-xl);padding:var(--space-3)}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user