Спринты и бэклог в модуле jira (#108) #109
@ -18,7 +18,7 @@
|
||||
| `http` | общее для обработчиков HTTP: `ApiError`, `Raw`, разбор параметров, `mapped`, `not_found` | `errors`, `projects` |
|
||||
| `modules` | контракт модуля: `Module` и `CommandHooks` (только структуры) | stdlib |
|
||||
| `registry` | реестр модулей: ленивая сборка, `modules()`, `module(name)`, `module_of(тип_команды)`, `agent_modules()`, `DEFAULT_AGENT`, проверка конфликтов (типы команд, маршруты, ключи настроек, поля проекта) | `modules`; модули — ленивым импортом внутри функции |
|
||||
| `db` | `connect`, схема ядра (`setting`, `batch`, `command`, `project`, `agent_status`) и схемы модулей из `Module.schema`, миграции по `schema_version` (v8; копия базы перед миграцией; история шагов `MIGRATIONS`) | `backup`, `paths`; модули — только внутри шагов истории миграций (ленивый импорт) |
|
||||
| `db` | `connect`, схема ядра (`setting`, `batch`, `command`, `project`, `agent_status`) и схемы модулей из `Module.schema`, миграции по `schema_version` (v9; копия базы перед миграцией; история шагов `MIGRATIONS`) | `backup`, `paths`; модули — только внутри шагов истории миграций (ленивый импорт) |
|
||||
| `settings` | настройки (`setting`), токены (клиентский + по агенту на модуль: `setting.tokens = {client, agents: {<модуль>: <токен>}}`), умолчания модулей (`ensure_defaults`) | `registry` (база — через `conn` вызывающего) |
|
||||
| `projects` | проекты-метки: таблица `project` (`label`, `name`, `created_at`), проверка метки, создание и правка (`PATCH`) с полями модулей, список = поля ядра + `project_view` модулей | `db`, `registry` |
|
||||
| `queue` | механика очереди: пакеты, выдача (`claim`), отчёты, отмена, повтор, временные ключи `new:<n>`; смысл типов команд берёт у модуля через `CommandHooks` (`registry.module_of`) | `backup`, `db`, `registry`, `settings`, `errors` |
|
||||
@ -37,7 +37,7 @@
|
||||
|
||||
| Модуль | Ответственность | Зависит от |
|
||||
|---|---|---|
|
||||
| `schema` | таблицы модуля: `issue`, `issue_link`, `issue_comment`, `issue_attachment`, `attachment_file`, `issue_change`, `issue_note`, `sync_run` и др. | stdlib |
|
||||
| `schema` | таблицы модуля: `issue`, `issue_link`, `issue_comment`, `issue_attachment`, `attachment_file`, `issue_change`, `issue_note`, `sync_run`, `jira_board`, `jira_sprint`, `issue_sprint` и др. | stdlib |
|
||||
| `settings` | умолчания настроек модуля, ключи для агента, проверка значений, темп (`pacing`), перенос из `config.example.json` (`init_from_config`) | `core.settings`, `core.projects`, `projects` |
|
||||
| `mirror` | зеркало задач, связей, комментариев и вложений; пишет его только `apply_state` (с проверками `check_comments`, `check_attachments`), разрез по проекту, заметки, фильтры, снимок доски `board`, карточка `get_issue` | `core.db`, `history`, `attachments` |
|
||||
| `history` | журнал изменений `issue_change`: время Jira → UTC (`jira_utc`), проверка и запись пачек агента, `delivery_param` для выдачи `refresh`, лента `feed`, история задачи | `core.db`, `core.settings`, `projects` |
|
||||
@ -152,10 +152,11 @@ golden-эталоны `__fixtures__/`), `issues/` (список, карточк
|
||||
- `commands`, `mirror`, `queue` не знают про HTTP; HTTP живёт только в `core/api`, `modules/*/api`, `client` и `relay` (реле — клиент своего же HTTP).
|
||||
- Веб-приложение (`web/`) ходит в хаб только через HTTP API (`web/src/api/client.ts`); словари `kind_labels`, `link_types` и набор закрытых статусов `closed_statuses` берёт из `GET /settings` и своих копий не держит. Модуль веба не импортирует другой модуль; общее — в `shell`, `ui`, `api`, `projects`.
|
||||
|
||||
## Выкатка и откат версии с новой схемой (v7, v8)
|
||||
## Выкатка и откат версии с новой схемой (v7, v8, v9)
|
||||
- Порядок: `git pull` → сразу `launchctl kickstart -k gui/$(id -u)/ru.aepif.sdlc-hub` (владелец) → `make web` → `/mcp` reconnect. Старый фронт с новым API работает (поля `board_id` и прочие на месте), поэтому сборку можно отложить.
|
||||
- До kickstart не запускать MCP и CLI `sdlc-hub` (в том числе `tokens`): `HubClient.from_home()` и CLI открывают базу через `db.connect` и мигрируют её, а хаб на прежнем коде с такой базой не работает (v7 — `agent_status`, токены; v8 — нет колонки `issue_note.fact_status`: 500 на `GET /issues`, `/board`, запись заметки).
|
||||
- После kickstart MCP-сессии со старым кодом до reconnect открывают базу своими умолчаниями: ключи, удалённые миграцией, они могут вернуть — их убирает `ensure_defaults` нового кода (`Module.settings_obsolete`).
|
||||
- v9 (sprints-backlog): колонки `issue.rank`, `issue.story_points` и таблицы `jira_board`, `jira_sprint`, `issue_sprint` добавляются миграцией; прежний код с базой v9 работает — новые колонки и таблицы он не читает, поэтому откат кода делается без возврата копии базы.
|
||||
- Откат на копию v6 из `backups/` (`*schema-v6*`): вместе с базой вернуть публикацию агента `release/jira/*` в `release/` (прежняя раскладка `release/agent.py`, `manifest.json`); иначе старый код увидит «публикации нет» и самообновление агента молчит.
|
||||
|
||||
## Открытые вопросы
|
||||
|
||||
@ -173,3 +173,49 @@ kanban) — свои задачи в статусах остальных кол
|
||||
- 24.10.2026 — R-28 (план агента R-64, R-65): поле Jira не найдено — ключ у задач не передаётся (хаб сохраняет прежнее),
|
||||
а не `null`; ошибка чтения списка спринтов — доска не входит в `boards` (иначе стёрлась бы история спринтов).
|
||||
Цена ошибки — нет.
|
||||
- 24.10.2026 — R-13 (контракт ядра): `CommandHooks.tail` получает `(conn, cmds, project)` вместо `(cmds)`: хвост
|
||||
пакета со спринтовыми командами — `refresh` по JQL проекта пакета (design #13), а без соединения и проекта хук JQL
|
||||
не построит. Правится ядро (`core/modules.py`, `core/queue.py`), wiki (`tail=lambda conn, cmds, project: None`) и
|
||||
две заглушки в тестах. Цена ошибки — вернуть сигнатуру и строить хвост в `check_batch` (дописывая `refresh` в конец
|
||||
пакета).
|
||||
- 24.10.2026 — R-14 (форма `boards`): у доски в `boards` поле `sprints` отсутствует или `null` — спринты доски в
|
||||
зеркале не меняются (агент не смог их прочитать, agent spec «прежние правила для спринтов»); `[]` — у доски спринтов
|
||||
нет, строки удаляются. `columns` отсутствует или `null` — пишется `NULL`. `boards: null` в пачке — как отсутствие.
|
||||
Цена ошибки — устаревшие спринты на экране до следующего `refresh` с `sprints: []`.
|
||||
- 24.10.2026 — R-15 (проверки постановки): хаб проверяет `start_date`/`end_date` (`ГГГГ-ММ-ДД`, настоящая дата, начало
|
||||
не позже конца), непустые `sprint`/`rename_to`, `state` ∈ {`active`, `closed`}, `move_open_to` (обязателен при
|
||||
`closed`, 422 при `active`, не равен `sprint`), у `ensure_rank` ровно одно из `before`/`after` и не сама задача,
|
||||
`story_points` — конечное число ≥ 0 или `null`. Цена ошибки — правка проверок.
|
||||
- 24.10.2026 — R-16 (подтверждение по зеркалу): `ensure_fields` со `story_points` подтверждается, только если
|
||||
`issue.story_points == params.story_points` точно (`null` = `null`, `0` ≠ `null`); прочие поля — как раньше. Цена
|
||||
ошибки — лишняя довыдача.
|
||||
- 24.10.2026 — R-17 (`GET /agile`): `board_type = null` → `sprints: []`, `backlog: null`, `board_issues: null`;
|
||||
scrum-бэклог исключает задачи в активном/будущем спринте любой доски из `jira_sprint` (спринт, которого нет в
|
||||
`jira_sprint`, не учитывается); kanban: выполненная задача без `jira_updated` не показывается; `kanban_backlog` в
|
||||
ответе — `true`/ `false`/`null`; суммы `points`/`points_done` — сумма чисел (без оценок — `0`). Цена ошибки — правка
|
||||
выборки.
|
||||
- 24.10.2026 — R-18 (Kanban с бэклогом): при `kanban_backlog = true` первая колонка доски не рисуется — её задачи в
|
||||
«Бэклог (N)» (как kanban-бэклог Jira). Цена ошибки — вернуть колонку.
|
||||
- 24.10.2026 — R-19 (три состояния): «Нужен агент v15 и обновление из Jira» — `board = null` или `board.fetched_at =
|
||||
null`; «Не удалось прочитать настройки доски из Jira» — `fetched_at` есть, `columns = null` (design #23 вместо
|
||||
противоречивой фразы web-app «`board.columns = null` или ответ без доски»). Цена ошибки — правка условия.
|
||||
- 24.10.2026 — R-20 (кнопка «Обновить»): на экранах — существующая `RefreshButton` («Обновить из Jira», в шапке и в
|
||||
сообщениях трёх состояний). Правило «кнопок нет, кроме раскрытия секций и карточки» к ней не относится: она читает
|
||||
Jira, а не пишет. Цена ошибки — убрать кнопку из шапки.
|
||||
- 24.10.2026 — R-21 (переименование): «Связи» — подпись пункта меню, заголовок вкладки и ссылка в карточке jira на
|
||||
«Обзоре»; заголовок `h1` страницы остаётся «Доска связей» (спека говорит о меню и вкладке). Цена ошибки — правка
|
||||
`BoardHeader` и e2e `geometry.spec.ts`.
|
||||
- 24.10.2026 — R-22 (смена проекта): фраза «Проект в приложении» («смена проекта на странице, которой нет у нового
|
||||
проекта, открывает «Обзор»») не меняется в коде: других условных страниц нет, а для экранов доски действует design
|
||||
#21 (адрес остаётся, сообщение «У проекта нет …-доски») — `Layout.select` уже сохраняет раздел. Цена ошибки —
|
||||
переход на «Обзор» для новых страниц.
|
||||
- 24.10.2026 — R-23 (NotFound): «не найдено» перечисляет только безусловные пункты (без `when`). Цена ошибки — нет.
|
||||
- 24.10.2026 — R-24 (склонение): «N задач» — с русским склонением (1 задача, 2 задачи, 5 задач). Цена ошибки — нет.
|
||||
- 24.10.2026 — R-25 (тексты MCP): шапки таблиц и отказы `sprints`/`sprint`/`backlog` — по задаче 7. Цена ошибки —
|
||||
правка текста.
|
||||
- 24.10.2026 — R-26 (даты): хаб сортирует спринты по `history.jira_utc(дата)`; веб перед `new Date` приводит `±HHMM` к
|
||||
`±HH:MM`. Цена ошибки — нет.
|
||||
- 24.10.2026 — R-27 (e2e): тестовый хаб засевает второй проект `KANBAN_E2E` (доска 5120, kanban, задачи
|
||||
AURORA-9101…9103); доска AWPCA — scrum со спринтами 101 (закрыт), 102 (активный), 103 (будущий). Цена ошибки —
|
||||
правка сида.
|
||||
- 24.10.2026 — R-29: трейлер коммитов — Co-Authored-By: Claude Sonnet 5.5 (инструкция сессии точнее перечня плана). Цена ошибки — переписать трейлеры, ничего не пушилось.
|
||||
|
||||
@ -8,7 +8,7 @@ from pathlib import Path
|
||||
|
||||
from . import backup, paths
|
||||
|
||||
SCHEMA_VERSION = 8
|
||||
SCHEMA_VERSION = 9
|
||||
|
||||
CORE_SCHEMA = """
|
||||
CREATE TABLE IF NOT EXISTS setting (
|
||||
@ -116,11 +116,11 @@ def schema() -> str:
|
||||
return CORE_SCHEMA + "".join(m.schema for m in registry.modules())
|
||||
|
||||
|
||||
def _add_column(table: str, column: str):
|
||||
def _add_column(table: str, column: str, decl: str = "TEXT"):
|
||||
"""Шаг миграции: добавить колонку, если её нет. Базы v1/v2 получают issue_comment из схемы уже с новой колонкой."""
|
||||
def step(conn) -> None:
|
||||
if column not in {r["name"] for r in conn.execute(f"PRAGMA table_info({table})")}:
|
||||
conn.execute(f"ALTER TABLE {table} ADD COLUMN {column} TEXT")
|
||||
conn.execute(f"ALTER TABLE {table} ADD COLUMN {column} {decl}")
|
||||
return step
|
||||
|
||||
|
||||
@ -136,6 +136,7 @@ MIGRATIONS = {
|
||||
6: [_tokens_v7, _agent_status_v7,
|
||||
_project_fields_v7], # v6 → v7: токены и статус по агентам модулей (wiki-module, решение 15)
|
||||
7: [_note_fields_v8], # v7 → v8: без статуса по факту и его настроек (fact-status-removal)
|
||||
8: [_add_column("issue", "rank"), _add_column("issue", "story_points", "REAL")], # v8 → v9: ранг и оценка задачи; таблицы досок и спринтов создаются схемой (sprints-backlog)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -17,6 +17,8 @@ CREATE TABLE IF NOT EXISTS issue (
|
||||
epic_key TEXT,
|
||||
labels TEXT NOT NULL DEFAULT '[]',
|
||||
sprint TEXT,
|
||||
rank TEXT,
|
||||
story_points REAL,
|
||||
created TEXT,
|
||||
jira_updated TEXT,
|
||||
fetched_at TEXT NOT NULL,
|
||||
@ -102,4 +104,28 @@ CREATE TABLE IF NOT EXISTS jira_project (
|
||||
board_id INTEGER,
|
||||
board_type TEXT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS jira_board (
|
||||
board_id INTEGER PRIMARY KEY,
|
||||
type TEXT,
|
||||
columns TEXT,
|
||||
kanban_backlog INTEGER,
|
||||
fetched_at TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS jira_sprint (
|
||||
sprint_id INTEGER PRIMARY KEY,
|
||||
board_id INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
start_date TEXT,
|
||||
end_date TEXT,
|
||||
complete_date TEXT,
|
||||
goal TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS ix_jira_sprint_board ON jira_sprint(board_id);
|
||||
CREATE TABLE IF NOT EXISTS issue_sprint (
|
||||
key TEXT NOT NULL,
|
||||
sprint_id INTEGER NOT NULL,
|
||||
PRIMARY KEY (key, sprint_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS ix_issue_sprint_sprint ON issue_sprint(sprint_id);
|
||||
"""
|
||||
|
||||
29
tests/fixtures/baseline/schema.expected.json
vendored
29
tests/fixtures/baseline/schema.expected.json
vendored
@ -25,6 +25,16 @@
|
||||
"ix_issue_link_target",
|
||||
"CREATE INDEX ix_issue_link_target ON issue_link(target_key)"
|
||||
],
|
||||
[
|
||||
"index",
|
||||
"ix_issue_sprint_sprint",
|
||||
"CREATE INDEX ix_issue_sprint_sprint ON issue_sprint(sprint_id)"
|
||||
],
|
||||
[
|
||||
"index",
|
||||
"ix_jira_sprint_board",
|
||||
"CREATE INDEX ix_jira_sprint_board ON jira_sprint(board_id)"
|
||||
],
|
||||
[
|
||||
"table",
|
||||
"agent_status",
|
||||
@ -48,7 +58,7 @@
|
||||
[
|
||||
"table",
|
||||
"issue",
|
||||
"CREATE TABLE issue (\n key TEXT PRIMARY KEY,\n id TEXT,\n issue_type TEXT,\n summary TEXT,\n description TEXT,\n status TEXT,\n priority TEXT,\n assignee TEXT,\n reporter TEXT,\n duedate TEXT,\n planned_start TEXT,\n planned_end TEXT,\n epic_key TEXT,\n labels TEXT NOT NULL DEFAULT '[]',\n sprint TEXT,\n created TEXT,\n jira_updated TEXT,\n fetched_at TEXT NOT NULL,\n neighbor INTEGER NOT NULL DEFAULT 0\n)"
|
||||
"CREATE TABLE issue (\n key TEXT PRIMARY KEY,\n id TEXT,\n issue_type TEXT,\n summary TEXT,\n description TEXT,\n status TEXT,\n priority TEXT,\n assignee TEXT,\n reporter TEXT,\n duedate TEXT,\n planned_start TEXT,\n planned_end TEXT,\n epic_key TEXT,\n labels TEXT NOT NULL DEFAULT '[]',\n sprint TEXT,\n rank TEXT,\n story_points REAL,\n created TEXT,\n jira_updated TEXT,\n fetched_at TEXT NOT NULL,\n neighbor INTEGER NOT NULL DEFAULT 0\n)"
|
||||
],
|
||||
[
|
||||
"table",
|
||||
@ -75,11 +85,26 @@
|
||||
"issue_note",
|
||||
"CREATE TABLE issue_note (\n key TEXT PRIMARY KEY,\n work_kind TEXT,\n done_text TEXT,\n rest_text TEXT,\n owner_note TEXT,\n updated_at TEXT,\n updated_by TEXT\n)"
|
||||
],
|
||||
[
|
||||
"table",
|
||||
"issue_sprint",
|
||||
"CREATE TABLE issue_sprint (\n key TEXT NOT NULL,\n sprint_id INTEGER NOT NULL,\n PRIMARY KEY (key, sprint_id)\n)"
|
||||
],
|
||||
[
|
||||
"table",
|
||||
"jira_board",
|
||||
"CREATE TABLE jira_board (\n board_id INTEGER PRIMARY KEY,\n type TEXT,\n columns TEXT,\n kanban_backlog INTEGER,\n fetched_at TEXT NOT NULL\n)"
|
||||
],
|
||||
[
|
||||
"table",
|
||||
"jira_project",
|
||||
"CREATE TABLE jira_project (\n label TEXT PRIMARY KEY REFERENCES project(label),\n board_id INTEGER,\n board_type TEXT\n)"
|
||||
],
|
||||
[
|
||||
"table",
|
||||
"jira_sprint",
|
||||
"CREATE TABLE jira_sprint (\n sprint_id INTEGER PRIMARY KEY,\n board_id INTEGER NOT NULL,\n name TEXT NOT NULL,\n state TEXT NOT NULL,\n start_date TEXT,\n end_date TEXT,\n complete_date TEXT,\n goal TEXT\n)"
|
||||
],
|
||||
[
|
||||
"table",
|
||||
"project",
|
||||
@ -116,5 +141,5 @@
|
||||
"CREATE TABLE wiki_sync_stage (\n command_id INTEGER NOT NULL,\n code TEXT NOT NULL,\n payload TEXT NOT NULL,\n PRIMARY KEY (command_id, code)\n)"
|
||||
]
|
||||
],
|
||||
"schema_version": 8
|
||||
"schema_version": 9
|
||||
}
|
||||
|
||||
@ -19,6 +19,8 @@ def test_connect_creates_schema(open_db):
|
||||
assert "created_at" in {r["name"] for r in conn.execute("PRAGMA table_info(issue_comment)")}
|
||||
assert "history_at" in {r["name"] for r in conn.execute("PRAGMA table_info(sync_run)")}
|
||||
assert "project" in {r["name"] for r in conn.execute("PRAGMA table_info(batch)")}
|
||||
assert {"jira_board", "jira_sprint", "issue_sprint"} <= names
|
||||
assert {"rank", "story_points"} <= {r["name"] for r in conn.execute("PRAGMA table_info(issue)")}
|
||||
assert conn.execute("SELECT value FROM setting WHERE key = 'schema_version'").fetchone()["value"] == str(db.SCHEMA_VERSION)
|
||||
assert conn.execute("SELECT COUNT(*) AS n FROM agent_status").fetchone()["n"] == 2
|
||||
cols = {r["name"] for r in conn.execute("PRAGMA table_info(command)")}
|
||||
@ -402,7 +404,7 @@ def test_migrates_v7_drops_fact_status(tmp_path):
|
||||
path = tmp_path / "hub.sqlite"
|
||||
_v7_database(path)
|
||||
conn = db.connect(path)
|
||||
assert db._schema_version(conn) == 8
|
||||
assert db._schema_version(conn) == db.SCHEMA_VERSION
|
||||
assert _note_after_v8(conn, "A-1") == "Аналитика"
|
||||
row = conn.execute("SELECT updated_at, updated_by FROM issue_note WHERE key = 'A-1'").fetchone()
|
||||
assert (row["updated_at"], row["updated_by"]) == ("2026-10-20T00:00:00+00:00", "import")
|
||||
@ -416,7 +418,7 @@ def test_migrates_v7_without_keys_keeps_settings_version(tmp_path):
|
||||
path = tmp_path / "hub.sqlite"
|
||||
_v7_database(path, with_keys=False)
|
||||
conn = db.connect(path)
|
||||
assert db._schema_version(conn) == 8 and settings.get(conn, "settings_version") == 12
|
||||
assert db._schema_version(conn) == db.SCHEMA_VERSION and settings.get(conn, "settings_version") == 12
|
||||
conn.close()
|
||||
|
||||
|
||||
@ -428,7 +430,7 @@ def test_migrates_v7_without_settings_version(tmp_path):
|
||||
raw.commit()
|
||||
raw.close()
|
||||
conn = db.connect(path)
|
||||
assert db._schema_version(conn) == 8
|
||||
assert db._schema_version(conn) == db.SCHEMA_VERSION
|
||||
assert conn.execute("SELECT COUNT(*) AS n FROM setting WHERE key = 'settings_version'").fetchone()["n"] == 0
|
||||
conn.close()
|
||||
|
||||
@ -457,3 +459,36 @@ def test_obsolete_keys_removed_by_ensure_defaults(tmp_path):
|
||||
assert {"status_labels", "transitions_map"}.isdisjoint(settings.get_all(conn))
|
||||
assert settings.get(conn, "settings_version") == version
|
||||
conn.close()
|
||||
|
||||
|
||||
def _v8_database(path):
|
||||
"""База v8: задача X со спринтом «Спринт 2», без колонок rank/story_points и таблиц досок (sprints-backlog)."""
|
||||
conn = db.connect(path)
|
||||
settings.ensure_defaults(conn)
|
||||
conn.execute("INSERT INTO issue(key, summary, sprint, labels, fetched_at) VALUES ('X', 'т', 'Спринт 2', '[]', ?)", (db.now(),))
|
||||
for col in ("rank", "story_points"):
|
||||
conn.execute(f"ALTER TABLE issue DROP COLUMN {col}")
|
||||
for t in ("jira_board", "jira_sprint", "issue_sprint"):
|
||||
conn.execute(f"DROP TABLE {t}")
|
||||
conn.execute("UPDATE setting SET value = '8' WHERE key = 'schema_version'")
|
||||
conn.close()
|
||||
|
||||
|
||||
def test_migrates_v8_adds_agile_columns(tmp_path): # сценарий «Миграция базы v8»
|
||||
path = tmp_path / "hub.sqlite"; _v8_database(path)
|
||||
conn = db.connect(path)
|
||||
assert db._schema_version(conn) == 9
|
||||
row = conn.execute("SELECT rank, story_points, sprint FROM issue WHERE key = 'X'").fetchone()
|
||||
assert (row["rank"], row["story_points"], row["sprint"]) == (None, None, "Спринт 2")
|
||||
types = {r["name"]: r["type"] for r in conn.execute("PRAGMA table_info(issue)")}
|
||||
assert types["rank"] == "TEXT" and types["story_points"] == "REAL"
|
||||
for t in ("jira_board", "jira_sprint", "issue_sprint"):
|
||||
assert conn.execute(f"SELECT COUNT(*) AS n FROM {t}").fetchone()["n"] == 0
|
||||
assert len(list((tmp_path / "backups").glob("*-schema-v8.sqlite"))) == 1
|
||||
conn.close()
|
||||
|
||||
|
||||
def test_v9_reconnect_is_stable(tmp_path): # повторный connect не мигрирует и не копирует
|
||||
path = tmp_path / "hub.sqlite"; _v8_database(path)
|
||||
db.connect(path).close(); db.connect(path).close()
|
||||
assert len(list((tmp_path / "backups").glob("*-schema-v*.sqlite"))) == 1
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user