Фикстура open_db в tests/conftest.py, фикстуры conn вместо хелперов fresh()/_mirror(); filterwarnings: error::ResourceWarning и error::pytest.PytestUnraisableExceptionWarning (решение 14, R-5). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
be50525ed6
commit
ea37d54946
@ -48,4 +48,3 @@
|
||||
## Открытые вопросы
|
||||
- Правки из UI в Jira (спринт, срок, связи, исполнитель) — change `viewer-edit`; страницы очереди, агента и настроек — позже.
|
||||
- Автоподстройка темпа по фактам 429 не реализована: хаб только показывает их.
|
||||
- Незакрытые SQLite-соединения и сокеты в ряде тестов (ResourceWarning при `-W error::ResourceWarning`).
|
||||
|
||||
@ -31,6 +31,7 @@
|
||||
## Тесты
|
||||
- Golden-тесты: `tests/fixtures/<name>.*` + эталон `<name>.expected.*`. Внешние сервисы в тестах мокаются.
|
||||
- SQLite в памяти или во временном `JIRA_HUB_HOME` (`tmp_path`); API поднимается на порту 0 в отдельном потоке. Jira и агент в тестах не участвуют.
|
||||
- Соединение SQLite в тесте — через фикстуру `open_db` (`tests/conftest.py`) или фикстуру `conn` модуля, иначе с явным `close()`: `pyproject.toml` (`filterwarnings`) делает незакрытое соединение ошибкой набора.
|
||||
- Фронт: golden-эталоны доски — `web/src/board/__fixtures__/*.expected.json` (их пишет `web/scripts/golden-from-links.ts` из старой страницы); тесты рядом с кодом `*.test.ts(x)`; e2e — `web/e2e/` на тестовом хабе.
|
||||
|
||||
## Первая подсистема
|
||||
|
||||
@ -20,3 +20,6 @@ packages = ["src/jira_hub"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
# незакрытое соединение sqlite — ошибка набора: ResourceWarning из __del__ pytest выдаёт как
|
||||
# PytestUnraisableExceptionWarning, поэтому нужны оба фильтра (hub-hygiene-d, решение 14)
|
||||
filterwarnings = ["error::ResourceWarning", "error::pytest.PytestUnraisableExceptionWarning"]
|
||||
|
||||
20
tests/conftest.py
Normal file
20
tests/conftest.py
Normal file
@ -0,0 +1,20 @@
|
||||
"""Общие фикстуры тестов. Соединения SQLite, открытые тестом, закрываются после него — в тестах нет
|
||||
ResourceWarning «unclosed database» (hub-hygiene-d, решение 14; M-9/O-4 #4)."""
|
||||
import pytest
|
||||
|
||||
from jira_hub import db
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def open_db():
|
||||
"""Фабрика соединений: open_db() — база в памяти, open_db(path) — файл. Все закрываются после теста."""
|
||||
opened = []
|
||||
|
||||
def make(path=":memory:"):
|
||||
conn = db.connect(path)
|
||||
opened.append(conn)
|
||||
return conn
|
||||
|
||||
yield make
|
||||
for conn in opened:
|
||||
conn.close()
|
||||
@ -1,6 +1,6 @@
|
||||
import pytest
|
||||
|
||||
from jira_hub import commands, db, mirror
|
||||
from jira_hub import commands, mirror
|
||||
from jira_hub.commands import CommandError
|
||||
|
||||
NOW = "strftime('%Y-%m-%dT%H:%M:%S.000+0300', 'now')"
|
||||
@ -48,15 +48,17 @@ def test_substitute_and_estimate():
|
||||
assert commands.estimate(["ensure_fields", "ensure_comment", "refresh"], {"ensure_fields": 120, "ensure_comment": 30, "refresh": 2}) == 152
|
||||
|
||||
|
||||
def _mirror():
|
||||
conn = db.connect(":memory:")
|
||||
mirror.apply_state(conn, [
|
||||
@pytest.fixture
|
||||
def conn(open_db):
|
||||
"""Зеркало в памяти: A-1 (Готово, Спринт 2, метки AWPCA и kind-dev) и A-2, связь A-1 → A-2; закрывается после теста."""
|
||||
c = open_db()
|
||||
mirror.apply_state(c, [
|
||||
{"key": "A-1", "summary": "Тема", "status": "Готово", "priority": "Высокий",
|
||||
"duedate": "2026-10-23", "labels": ["AWPCA", "kind-dev"], "sprint": "Спринт 2",
|
||||
"created": "2026-10-10T10:00:00.000+0300", "assignee": "ivanov", "reporter": "petrov"},
|
||||
{"key": "A-2", "summary": "Вторая", "status": "Разработка", "created": "2026-10-10T11:00:00.000+0300"},
|
||||
], [{"source_key": "A-1", "target_key": "A-2", "link_type": "ASUPR_Связанные"}])
|
||||
return conn
|
||||
return c
|
||||
|
||||
|
||||
LT = {"связанные": "ASUPR_Связанные", "блокирует начало": "ASUPR_Блокер_Начала"}
|
||||
@ -78,27 +80,24 @@ LT = {"связанные": "ASUPR_Связанные", "блокирует на
|
||||
({"type": "refresh", "params": {"keys": ["A-1"]}}, False),
|
||||
({"type": "ensure_fields", "params": {"key": "A-404", "duedate": "2026-10-23"}}, False),
|
||||
])
|
||||
def test_check_confirmed(cmd, expected):
|
||||
ok, _ = commands.check_confirmed(_mirror(), cmd, LT)
|
||||
def test_check_confirmed(conn, cmd, expected):
|
||||
ok, _ = commands.check_confirmed(conn, cmd, LT)
|
||||
assert ok is expected
|
||||
|
||||
|
||||
def test_ensure_link_reverse_direction_with_both_keys_in_mirror():
|
||||
def test_ensure_link_reverse_direction_with_both_keys_in_mirror(conn):
|
||||
# A-1 -> A-2 есть, спрашиваем A-2 -> A-1 при обоих ключах в зеркале: обратная связь — конфликт
|
||||
conn = _mirror()
|
||||
ok, _ = commands.check_confirmed(conn, {"type": "ensure_link", "params": {"source": "A-2", "target": "A-1", "link_type": "связанные"}}, LT)
|
||||
assert ok is False
|
||||
|
||||
|
||||
def test_ensure_link_conflict_other_type_blocks_confirmation():
|
||||
conn = _mirror()
|
||||
def test_ensure_link_conflict_other_type_blocks_confirmation(conn):
|
||||
conn.execute("INSERT INTO issue_link(source_key, target_key, link_type) VALUES ('A-1', 'A-2', 'ASUPR_Блокер_Начала')")
|
||||
ok, _ = commands.check_confirmed(conn, {"type": "ensure_link", "params": {"source": "A-1", "target": "A-2", "link_type": "связанные"}}, LT)
|
||||
assert ok is False
|
||||
|
||||
|
||||
def test_ensure_link_conflict_reverse_blocks_confirmation():
|
||||
conn = _mirror()
|
||||
def test_ensure_link_conflict_reverse_blocks_confirmation(conn):
|
||||
conn.execute("INSERT INTO issue_link(source_key, target_key, link_type) VALUES ('A-2', 'A-1', 'ASUPR_Связанные')")
|
||||
ok, _ = commands.check_confirmed(conn, {"type": "ensure_link", "params": {"source": "A-1", "target": "A-2", "link_type": "связанные"}}, LT)
|
||||
assert ok is False
|
||||
@ -108,9 +107,8 @@ def _ensure_issue(summary):
|
||||
return {"type": "ensure_issue", "params": {"temp_key": "new:1", "issue_type": "task", "summary": summary}}
|
||||
|
||||
|
||||
def test_ensure_issue_never_confirmed_by_state():
|
||||
def test_ensure_issue_never_confirmed_by_state(conn):
|
||||
"""bridge-reliability, решение 4: потерянный отчёт ensure_issue закрывается довыдачей, не сверкой с зеркалом."""
|
||||
conn = _mirror()
|
||||
conn.execute(f"UPDATE issue SET created = {NOW} WHERE key = 'A-1'")
|
||||
assert commands.check_confirmed(conn, _ensure_issue("Тема"), LT) == (False, None)
|
||||
|
||||
|
||||
@ -7,8 +7,8 @@ import threading
|
||||
from jira_hub import backup, db, paths, queue, settings
|
||||
|
||||
|
||||
def test_connect_creates_schema():
|
||||
conn = db.connect(":memory:")
|
||||
def test_connect_creates_schema(open_db):
|
||||
conn = open_db()
|
||||
names = {r["name"] for r in conn.execute("SELECT name FROM sqlite_master WHERE type = 'table'")}
|
||||
assert {"setting", "issue", "issue_link", "issue_note", "batch", "command", "sync_run", "agent_status"} <= names
|
||||
assert conn.execute("SELECT value FROM setting WHERE key = 'schema_version'").fetchone()["value"] == "2"
|
||||
@ -59,10 +59,11 @@ def test_connect_is_idempotent(tmp_path):
|
||||
db.connect(path).close()
|
||||
conn = db.connect(path)
|
||||
assert conn.execute("SELECT COUNT(*) AS n FROM agent_status").fetchone()["n"] == 1
|
||||
conn.close()
|
||||
|
||||
|
||||
def test_transaction_rolls_back_on_error(tmp_path):
|
||||
conn = db.connect(tmp_path / "hub.sqlite")
|
||||
def test_transaction_rolls_back_on_error(tmp_path, open_db):
|
||||
conn = open_db(tmp_path / "hub.sqlite")
|
||||
try:
|
||||
with db.transaction(conn):
|
||||
conn.execute("INSERT INTO batch(title, author, created_at) VALUES ('x', 'y', 'z')")
|
||||
@ -72,8 +73,8 @@ def test_transaction_rolls_back_on_error(tmp_path):
|
||||
assert conn.execute("SELECT COUNT(*) AS n FROM batch").fetchone()["n"] == 0
|
||||
|
||||
|
||||
def test_nested_transaction_joins_outer(tmp_path):
|
||||
conn = db.connect(tmp_path / "hub.sqlite")
|
||||
def test_nested_transaction_joins_outer(tmp_path, open_db):
|
||||
conn = open_db(tmp_path / "hub.sqlite")
|
||||
try:
|
||||
with db.transaction(conn):
|
||||
with db.transaction(conn):
|
||||
@ -86,8 +87,8 @@ def test_nested_transaction_joins_outer(tmp_path):
|
||||
assert conn.execute("SELECT COUNT(*) AS n FROM batch").fetchone()["n"] == 0 # откат внешней откатил и вложенную
|
||||
|
||||
|
||||
def test_nested_transaction_error_propagates_to_outer(tmp_path):
|
||||
conn = db.connect(tmp_path / "hub.sqlite")
|
||||
def test_nested_transaction_error_propagates_to_outer(tmp_path, open_db):
|
||||
conn = open_db(tmp_path / "hub.sqlite")
|
||||
try:
|
||||
with db.transaction(conn):
|
||||
conn.execute("INSERT INTO batch(title, author, created_at) VALUES ('a', 'y', 'z')")
|
||||
@ -103,8 +104,8 @@ def test_parse_and_utcnow():
|
||||
assert db.parse(db.now()) <= db.utcnow()
|
||||
|
||||
|
||||
def test_nested_transaction_rollback_is_local(tmp_path):
|
||||
conn = db.connect(tmp_path / "hub.sqlite")
|
||||
def test_nested_transaction_rollback_is_local(tmp_path, open_db):
|
||||
conn = open_db(tmp_path / "hub.sqlite")
|
||||
with db.transaction(conn):
|
||||
conn.execute("INSERT INTO batch(title, author, created_at) VALUES ('outer', 'y', 'z')")
|
||||
try:
|
||||
|
||||
@ -1,6 +1,16 @@
|
||||
import csv
|
||||
|
||||
from jira_hub import db, importer, mirror, settings
|
||||
import pytest
|
||||
|
||||
from jira_hub import importer, mirror, settings
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def conn(open_db):
|
||||
"""База в памяти с настройками по умолчанию; закрывается после теста."""
|
||||
c = open_db()
|
||||
settings.ensure_defaults(c)
|
||||
return c
|
||||
|
||||
|
||||
def write_csv(path, header, rows):
|
||||
@ -29,10 +39,8 @@ def make_package(folder):
|
||||
["new:3", "связанные", "AURORA-1533", "z"]])
|
||||
|
||||
|
||||
def test_import_notes_and_report(tmp_path):
|
||||
def test_import_notes_and_report(conn, tmp_path):
|
||||
make_package(tmp_path)
|
||||
conn = db.connect(":memory:")
|
||||
settings.ensure_defaults(conn)
|
||||
settings.set_(conn, "link_types", {"блокирует начало": "ASUPR_Блокер_Начала", "связанные": "ASUPR_Связанные"})
|
||||
mirror.apply_state(conn, [{"key": "AURORA-1533", "summary": "т", "labels": []}, {"key": "AURORA-1586", "summary": "т", "labels": []}],
|
||||
[{"source_key": "AURORA-1586", "target_key": "AURORA-1533", "link_type": "ASUPR_Связанные"}])
|
||||
@ -60,10 +68,8 @@ def write_tasks(folder, comment):
|
||||
[["AURORA-1533", "Тест ОСВ", "тестирование", "Ожидание разработки", comment, "", "Прогнать", "Спринт 2"]])
|
||||
|
||||
|
||||
def test_reimport_keeps_hub_edit_when_cell_empty(tmp_path):
|
||||
def test_reimport_keeps_hub_edit_when_cell_empty(conn, tmp_path):
|
||||
write_tasks(tmp_path, "Актуализация 10.10.2026")
|
||||
conn = db.connect(":memory:")
|
||||
settings.ensure_defaults(conn)
|
||||
importer.import_package(conn, tmp_path)
|
||||
mirror.set_note(conn, "AURORA-1533", {"owner_note": "правка в хабе"}, "hub")
|
||||
write_tasks(tmp_path, "") # пустая ячейка в CSV
|
||||
@ -73,20 +79,16 @@ def test_reimport_keeps_hub_edit_when_cell_empty(tmp_path):
|
||||
assert n["rest_text"] == "Прогнать"
|
||||
|
||||
|
||||
def test_reimport_updates_nonempty_cell(tmp_path):
|
||||
def test_reimport_updates_nonempty_cell(conn, tmp_path):
|
||||
write_tasks(tmp_path, "Первая версия")
|
||||
conn = db.connect(":memory:")
|
||||
settings.ensure_defaults(conn)
|
||||
importer.import_package(conn, tmp_path)
|
||||
write_tasks(tmp_path, "Вторая версия")
|
||||
importer.import_package(conn, tmp_path)
|
||||
assert mirror.get_note(conn, "AURORA-1533")["owner_note"] == "Вторая версия"
|
||||
|
||||
|
||||
def test_import_does_not_touch_issue_tables(tmp_path):
|
||||
def test_import_does_not_touch_issue_tables(conn, tmp_path):
|
||||
make_package(tmp_path)
|
||||
conn = db.connect(":memory:")
|
||||
settings.ensure_defaults(conn)
|
||||
mirror.apply_state(conn, [{"key": "AURORA-1533", "summary": "т", "labels": []}],
|
||||
[{"source_key": "AURORA-1533", "target_key": "AURORA-1416", "link_type": "x"}])
|
||||
count = lambda t: conn.execute(f"SELECT COUNT(*) AS n FROM {t}").fetchone()["n"]
|
||||
@ -95,20 +97,16 @@ def test_import_does_not_touch_issue_tables(tmp_path):
|
||||
assert (count("issue"), count("issue_link")) == before
|
||||
|
||||
|
||||
def test_missing_csv_files_give_empty_report(tmp_path):
|
||||
conn = db.connect(":memory:")
|
||||
settings.ensure_defaults(conn)
|
||||
def test_missing_csv_files_give_empty_report(conn, tmp_path):
|
||||
rep = importer.import_package(conn, tmp_path)
|
||||
assert rep == {"notes": 0, "expected_links": 0, "missing_keys": [], "missing_links": [], "reversed_links": []}
|
||||
|
||||
|
||||
def test_reversed_link_reported_separately(tmp_path):
|
||||
def test_reversed_link_reported_separately(conn, tmp_path):
|
||||
make_package(tmp_path)
|
||||
write_csv(tmp_path / "links.csv", ["Откуда", "Тип", "Куда", "Пояснение (внутр.)"],
|
||||
[["AURORA-1533", "связанные", "AURORA-1586", "в Jira стоит наоборот"],
|
||||
["AURORA-1533", "блокирует начало", "AURORA-1416", "нет совсем"]])
|
||||
conn = db.connect(":memory:")
|
||||
settings.ensure_defaults(conn)
|
||||
settings.set_(conn, "link_types", {"блокирует начало": "ASUPR_Блокер_Начала", "связанные": "ASUPR_Связанные"})
|
||||
mirror.apply_state(conn, [{"key": "AURORA-1533", "summary": "т", "labels": []}, {"key": "AURORA-1586", "summary": "т", "labels": []}],
|
||||
[{"source_key": "AURORA-1586", "target_key": "AURORA-1533", "link_type": "ASUPR_Связанные"}])
|
||||
|
||||
@ -3,6 +3,12 @@ import pytest
|
||||
from jira_hub import db, mirror
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def conn(open_db):
|
||||
"""Пустое зеркало в памяти; закрывается после теста."""
|
||||
return open_db()
|
||||
|
||||
|
||||
def issue(key, **over):
|
||||
base = {"key": key, "id": "1", "issue_type": "Задача", "summary": f"Тема {key}", "description": "h3. Контекст",
|
||||
"status": "Ожидание разработки", "priority": "Средний", "assignee": None, "reporter": "me",
|
||||
@ -13,8 +19,7 @@ def issue(key, **over):
|
||||
return base
|
||||
|
||||
|
||||
def test_apply_state_upserts_and_replaces_links():
|
||||
conn = db.connect(":memory:")
|
||||
def test_apply_state_upserts_and_replaces_links(conn):
|
||||
links = [{"source_key": "A-1", "target_key": "A-2", "link_type": "ASUPR_Блокер_Начала"}]
|
||||
out = mirror.apply_state(conn, [issue("A-1"), issue("A-2")], links)
|
||||
assert out == {"issues": 2, "links": 1}
|
||||
@ -26,16 +31,14 @@ def test_apply_state_upserts_and_replaces_links():
|
||||
assert mirror.get_issue(conn, "A-2")["links_in"] == [{"source_key": "A-1", "link_type": "ASUPR_Связанные"}]
|
||||
|
||||
|
||||
def test_links_from_other_source_survive():
|
||||
conn = db.connect(":memory:")
|
||||
def test_links_from_other_source_survive(conn):
|
||||
mirror.apply_state(conn, [issue("A-1"), issue("A-2")],
|
||||
[{"source_key": "A-2", "target_key": "A-1", "link_type": "ASUPR_Связанные"}])
|
||||
mirror.apply_state(conn, [issue("A-1")], []) # пришла только A-1 без связей
|
||||
assert mirror.get_issue(conn, "A-1")["links_in"] == [{"source_key": "A-2", "link_type": "ASUPR_Связанные"}]
|
||||
|
||||
|
||||
def test_apply_state_marks_neighbors():
|
||||
conn = db.connect(":memory:")
|
||||
def test_apply_state_marks_neighbors(conn):
|
||||
mirror.apply_state(conn, [issue("A-1"), issue("B-9")], [], neighbor_keys={"B-9"})
|
||||
assert mirror.get_issue(conn, "B-9")["neighbor"] is True
|
||||
assert mirror.get_issue(conn, "A-1")["neighbor"] is False
|
||||
@ -44,8 +47,7 @@ def test_apply_state_marks_neighbors():
|
||||
assert mirror.known_keys(conn) == ["A-1", "B-9"]
|
||||
|
||||
|
||||
def test_partial_load_never_lowers_neighbor():
|
||||
conn = db.connect(":memory:")
|
||||
def test_partial_load_never_lowers_neighbor(conn):
|
||||
mirror.apply_state(conn, [issue("B-9")], [], neighbor_keys={"B-9"})
|
||||
mirror.apply_state(conn, [issue("B-9", summary="новая")], [], authoritative=False) # refresh {keys}
|
||||
assert mirror.get_issue(conn, "B-9")["neighbor"] is True
|
||||
@ -53,8 +55,7 @@ def test_partial_load_never_lowers_neighbor():
|
||||
assert mirror.get_issue(conn, "B-9")["neighbor"] is False
|
||||
|
||||
|
||||
def test_note_survives_refresh_and_renames():
|
||||
conn = db.connect(":memory:")
|
||||
def test_note_survives_refresh_and_renames(conn):
|
||||
mirror.set_note(conn, "new:7", {"fact_status": "В работе", "work_kind": "разработка"}, "import")
|
||||
mirror.rename_note(conn, "new:7", "A-5")
|
||||
mirror.apply_state(conn, [issue("A-5")], [])
|
||||
@ -65,8 +66,7 @@ def test_note_survives_refresh_and_renames():
|
||||
mirror.set_note(conn, "A-5", {"summary": "нельзя"}, "x")
|
||||
|
||||
|
||||
def test_list_issues_filters():
|
||||
conn = db.connect(":memory:")
|
||||
def test_list_issues_filters(conn):
|
||||
mirror.apply_state(conn, [issue("A-1", sprint="Спринт 2", assignee="ivanov", duedate="2026-10-20"),
|
||||
issue("A-2", sprint="Спринт 1", labels=["AWPCA", "fact-done"]),
|
||||
issue("A-3", sprint="Спринт 2", duedate=None)], [])
|
||||
@ -80,31 +80,27 @@ def test_list_issues_filters():
|
||||
assert len(mirror.list_issues(conn, None, limit=1)) == 1
|
||||
|
||||
|
||||
def test_rename_note_same_key_keeps_note():
|
||||
conn = db.connect(":memory:")
|
||||
def test_rename_note_same_key_keeps_note(conn):
|
||||
mirror.apply_state(conn, [issue("A-1")], [])
|
||||
mirror.set_note(conn, "A-1", {"fact_status": "В работе"}, "x")
|
||||
mirror.rename_note(conn, "A-1", "A-1")
|
||||
assert mirror.get_note(conn, "A-1")["fact_status"] == "В работе"
|
||||
|
||||
|
||||
def test_rename_note_missing_is_noop():
|
||||
conn = db.connect(":memory:")
|
||||
def test_rename_note_missing_is_noop(conn):
|
||||
mirror.rename_note(conn, "new:404", "A-9") # заметки нет — не падает
|
||||
assert mirror.get_note(conn, "new:404") is None
|
||||
assert mirror.get_note(conn, "A-9") is None
|
||||
|
||||
|
||||
def test_set_note_partial_keeps_other_fields():
|
||||
conn = db.connect(":memory:")
|
||||
def test_set_note_partial_keeps_other_fields(conn):
|
||||
mirror.set_note(conn, "A-1", {"work_kind": "разработка", "done_text": "сделано"}, "x")
|
||||
got = mirror.set_note(conn, "A-1", {"fact_status": "Готово"}, "y")
|
||||
assert got["work_kind"] == "разработка" and got["done_text"] == "сделано"
|
||||
assert got["fact_status"] == "Готово" and got["updated_by"] == "y"
|
||||
|
||||
|
||||
def test_rename_note_merges_with_existing():
|
||||
conn = db.connect(":memory:")
|
||||
def test_rename_note_merges_with_existing(conn):
|
||||
mirror.set_note(conn, "A-5", {"owner_note": "спросить ИБ", "work_kind": "Старое"}, "human")
|
||||
mirror.set_note(conn, "new:5", {"work_kind": "Аналитика", "fact_status": "В работе"}, "import")
|
||||
mirror.rename_note(conn, "new:5", "A-5")
|
||||
@ -116,8 +112,7 @@ def test_rename_note_merges_with_existing():
|
||||
assert mirror.get_note(conn, "new:5") is None
|
||||
|
||||
|
||||
def test_rename_note_keeps_newer_real_key_stamp():
|
||||
conn = db.connect(":memory:")
|
||||
def test_rename_note_keeps_newer_real_key_stamp(conn):
|
||||
mirror.set_note(conn, "new:6", {"fact_status": "В работе"}, "import")
|
||||
mirror.set_note(conn, "A-6", {"owner_note": "позже"}, "human")
|
||||
conn.execute("UPDATE issue_note SET updated_at = '2020-01-01T00:00:00+00:00' WHERE key = 'new:6'")
|
||||
@ -128,8 +123,7 @@ def test_rename_note_keeps_newer_real_key_stamp():
|
||||
assert note["fact_status"] == "В работе" and note["owner_note"] == "позже"
|
||||
|
||||
|
||||
def test_rename_note_empty_string_does_not_overwrite():
|
||||
conn = db.connect(":memory:")
|
||||
def test_rename_note_empty_string_does_not_overwrite(conn):
|
||||
mirror.set_note(conn, "A-6", {"rest_text": "доделать отчёт"}, "human")
|
||||
mirror.set_note(conn, "new:6", {"rest_text": "", "done_text": "сделано"}, "import")
|
||||
mirror.rename_note(conn, "new:6", "A-6")
|
||||
@ -151,8 +145,7 @@ def _sync_run(conn, started, finished, kind, query, status="done", count=5):
|
||||
(started, finished, kind, query, count, command_id))
|
||||
|
||||
|
||||
def test_board_snapshot_raw_mirror_data():
|
||||
conn = db.connect(":memory:")
|
||||
def test_board_snapshot_raw_mirror_data(conn):
|
||||
mirror.apply_state(conn, [issue("AURORA-1351", issue_type="Фича", epic_key=None),
|
||||
issue("AURORA-1620", labels=["kind-dev"], epic_key="AURORA-1351"),
|
||||
issue("AURORA-1621", epic_key="AURORA-1351")],
|
||||
@ -171,8 +164,7 @@ def test_board_snapshot_raw_mirror_data():
|
||||
assert snap["last_sync"] is None and snap["generated_at"]
|
||||
|
||||
|
||||
def test_board_last_sync_is_last_successful_full_refresh():
|
||||
conn = db.connect(":memory:")
|
||||
def test_board_last_sync_is_last_successful_full_refresh(conn):
|
||||
_sync_run(conn, "2026-10-14T09:59:00+00:00", "2026-10-14T10:00:00+00:00", "jql", BASE, count=198)
|
||||
_sync_run(conn, "2026-10-14T10:59:00+00:00", "2026-10-14T11:00:00+00:00", "keys", "AURORA-1620", count=1)
|
||||
_sync_run(conn, "2026-10-14T11:30:00+00:00", "2026-10-14T11:31:00+00:00", "jql", "key = AURORA-1", count=1)
|
||||
@ -182,9 +174,8 @@ def test_board_last_sync_is_last_successful_full_refresh():
|
||||
"finished_at": "2026-10-14T10:00:00+00:00", "issues_count": 198}
|
||||
|
||||
|
||||
def test_board_inside_open_transaction_keeps_it_open():
|
||||
def test_board_inside_open_transaction_keeps_it_open(conn):
|
||||
"""M-7 #20: вызов внутри уже открытой транзакции не открывает и не закрывает свою."""
|
||||
conn = db.connect(":memory:")
|
||||
mirror.apply_state(conn, [issue("AURORA-1620")], [])
|
||||
with db.transaction(conn):
|
||||
assert conn.in_transaction
|
||||
|
||||
@ -7,19 +7,20 @@ from jira_hub.queue import ClaimedError
|
||||
LT = {"связанные": "ASUPR_Связанные"}
|
||||
|
||||
|
||||
def fresh():
|
||||
conn = db.connect(":memory:")
|
||||
settings.ensure_defaults(conn)
|
||||
settings.set_(conn, "link_types", LT)
|
||||
return conn
|
||||
@pytest.fixture
|
||||
def conn(open_db):
|
||||
"""База очереди в памяти с настройками по умолчанию и типами связей LT; закрывается после теста."""
|
||||
c = open_db()
|
||||
settings.ensure_defaults(c)
|
||||
settings.set_(c, "link_types", LT)
|
||||
return c
|
||||
|
||||
|
||||
def fields(key, **extra):
|
||||
return {"type": "ensure_fields", "params": {"key": key, "duedate": "2026-10-23", **extra}}
|
||||
|
||||
|
||||
def test_create_batch_adds_tail_refresh_and_estimate():
|
||||
conn = fresh()
|
||||
def test_create_batch_adds_tail_refresh_and_estimate(conn):
|
||||
b = queue.create_batch(conn, "срок", "claude", [fields("A-1"), {"type": "ensure_comment", "params": {"key": "A-2", "marker": "m", "body": "b"}}])
|
||||
view = queue.batch_view(conn, b["id"])
|
||||
assert [c["type"] for c in view["commands"]] == ["ensure_fields", "ensure_comment", "refresh"]
|
||||
@ -28,16 +29,14 @@ def test_create_batch_adds_tail_refresh_and_estimate():
|
||||
assert view["status"] == "queued" and view["author"] == "claude"
|
||||
|
||||
|
||||
def test_refresh_only_batch_has_no_tail():
|
||||
conn = fresh()
|
||||
def test_refresh_only_batch_has_no_tail(conn):
|
||||
b = queue.create_batch(conn, "обновить", "claude", [{"type": "refresh", "params": {"jql": "project = X"}}])
|
||||
assert b["count"] == 1
|
||||
with pytest.raises(CommandError):
|
||||
queue.create_batch(conn, "x", "claude", [{"type": "nope"}])
|
||||
|
||||
|
||||
def test_claim_order_pause_and_report():
|
||||
conn = fresh()
|
||||
def test_claim_order_pause_and_report(conn):
|
||||
b1 = queue.create_batch(conn, "первый", "claude", [fields("A-1")])
|
||||
b2 = queue.create_batch(conn, "второй", "claude", [fields("A-2")])
|
||||
c = queue.claim(conn, agent_version=1, settings_version=1)
|
||||
@ -61,13 +60,13 @@ def test_claim_order_pause_and_report():
|
||||
assert queue.claim(conn)["params"]["key"] == "A-2" and queue.batch_view(conn, b2["id"])["status"] == "running"
|
||||
|
||||
|
||||
def test_claim_is_exclusive(tmp_path):
|
||||
def test_claim_is_exclusive(tmp_path, open_db):
|
||||
import threading
|
||||
path = tmp_path / "hub.sqlite"
|
||||
setup = db.connect(path)
|
||||
setup = open_db(path)
|
||||
settings.ensure_defaults(setup)
|
||||
queue.create_batch(setup, "x", "claude", [fields("A-1")])
|
||||
conns = [db.connect(path), db.connect(path)]
|
||||
conns = [open_db(path), open_db(path)]
|
||||
barrier, results = threading.Barrier(2), []
|
||||
|
||||
def worker(c):
|
||||
@ -83,8 +82,7 @@ def test_claim_is_exclusive(tmp_path):
|
||||
assert row["n"] == 1 and row["d"] == len(got)
|
||||
|
||||
|
||||
def test_backup_failure_does_not_block_claim(caplog):
|
||||
conn = fresh()
|
||||
def test_backup_failure_does_not_block_claim(conn, caplog):
|
||||
queue.create_batch(conn, "x", "claude", [fields("A-1")])
|
||||
|
||||
def broken(label):
|
||||
@ -96,8 +94,7 @@ def test_backup_failure_does_not_block_claim(caplog):
|
||||
assert "не создана" in caplog.text
|
||||
|
||||
|
||||
def test_temp_key_substitution_and_skip():
|
||||
conn = fresh()
|
||||
def test_temp_key_substitution_and_skip(conn):
|
||||
mirror.set_note(conn, "new:60", {"fact_status": "В работе"}, "import")
|
||||
queue.create_batch(conn, "создать", "claude", [
|
||||
{"type": "ensure_issue", "params": {"temp_key": "new:60", "issue_type": "task", "summary": "Новая"}},
|
||||
@ -120,8 +117,7 @@ def test_temp_key_substitution_and_skip():
|
||||
assert view["commands"][3]["error"] == "задача new:61 не создана"
|
||||
|
||||
|
||||
def test_cancel_and_retry():
|
||||
conn = fresh()
|
||||
def test_cancel_and_retry(conn):
|
||||
b = queue.create_batch(conn, "x", "claude", [fields("A-1"), fields("A-2")])
|
||||
c = queue.claim(conn)
|
||||
with pytest.raises(ClaimedError):
|
||||
@ -139,16 +135,14 @@ def test_cancel_and_retry():
|
||||
assert queue.list_batches(conn, limit=5)[0]["id"] == r["id"]
|
||||
|
||||
|
||||
def test_report_after_force_cancel_keeps_cancelled():
|
||||
conn = fresh()
|
||||
def test_report_after_force_cancel_keeps_cancelled(conn):
|
||||
queue.create_batch(conn, "x", "claude", [fields("A-1")])
|
||||
c = queue.claim(conn)
|
||||
queue.cancel_command(conn, c["id"], force=True)
|
||||
assert queue.report_result(conn, c["id"], "failed", error="e")["status"] == "cancelled"
|
||||
|
||||
|
||||
def test_reconcile_confirms_by_state():
|
||||
conn = fresh()
|
||||
def test_reconcile_confirms_by_state(conn):
|
||||
queue.create_batch(conn, "x", "claude", [{"type": "ensure_sprint", "params": {"key": "A-1", "sprint": "Спринт 2"}}])
|
||||
c = queue.claim(conn)
|
||||
b = queue.create_batch(conn, "обновить", "claude", [{"type": "refresh", "params": {"keys": ["A-1"]}}])
|
||||
@ -160,8 +154,7 @@ def test_reconcile_confirms_by_state():
|
||||
assert queue.batch_view(conn, b["id"])["status"] == "done"
|
||||
|
||||
|
||||
def test_auto_refresh_conditions():
|
||||
conn = fresh()
|
||||
def test_auto_refresh_conditions(conn):
|
||||
assert queue.maybe_auto_refresh(conn) is None # агент не на связи
|
||||
queue.claim(conn, agent_version=1, settings_version=1) # отметился, очередь пуста
|
||||
b = queue.maybe_auto_refresh(conn)
|
||||
@ -177,8 +170,7 @@ def test_auto_refresh_conditions():
|
||||
assert queue.maybe_auto_refresh(conn) is None
|
||||
|
||||
|
||||
def test_backup_called_for_first_write_only(tmp_path):
|
||||
conn = fresh()
|
||||
def test_backup_called_for_first_write_only(conn, tmp_path):
|
||||
calls = []
|
||||
queue.create_batch(conn, "x", "claude", [fields("A-1"), fields("A-2")])
|
||||
c1 = queue.claim(conn, backup=calls.append)
|
||||
@ -187,28 +179,27 @@ def test_backup_called_for_first_write_only(tmp_path):
|
||||
assert calls == [f"batch-{c1['batch_id']}"]
|
||||
|
||||
|
||||
def test_no_backup_for_refresh_batch():
|
||||
conn = fresh()
|
||||
def test_no_backup_for_refresh_batch(conn):
|
||||
calls = []
|
||||
queue.create_batch(conn, "x", "claude", [{"type": "refresh", "params": {"jql": "project = X"}}])
|
||||
queue.claim(conn, backup=calls.append)
|
||||
assert calls == []
|
||||
|
||||
|
||||
def test_make_backup_rotates(tmp_path):
|
||||
def test_make_backup_rotates(tmp_path, open_db):
|
||||
from jira_hub import backup
|
||||
conn = db.connect(tmp_path / "hub.sqlite")
|
||||
conn = open_db(tmp_path / "hub.sqlite")
|
||||
for i in range(4):
|
||||
backup.make_backup(conn, tmp_path / "backups", f"b{i}", keep=2)
|
||||
names = sorted(p.name for p in (tmp_path / "backups").glob("*.sqlite"))
|
||||
assert len(names) == 2 and names[-1].endswith("-b3.sqlite")
|
||||
assert db.connect(tmp_path / "backups" / names[-1]).execute("SELECT COUNT(*) AS n FROM agent_status").fetchone()["n"] == 1
|
||||
assert open_db(tmp_path / "backups" / names[-1]).execute("SELECT COUNT(*) AS n FROM agent_status").fetchone()["n"] == 1
|
||||
|
||||
|
||||
def test_rotation_keeps_schema_migration_copy(tmp_path):
|
||||
def test_rotation_keeps_schema_migration_copy(tmp_path, open_db):
|
||||
"""Копия базы перед миграцией схемы не участвует в ротации копий пакетов (M-1 финального ревью)."""
|
||||
from jira_hub import backup
|
||||
conn = db.connect(tmp_path / "hub.sqlite")
|
||||
conn = open_db(tmp_path / "hub.sqlite")
|
||||
backup.make_backup(conn, tmp_path / "backups", "schema-v1", keep=0)
|
||||
for i in range(4):
|
||||
backup.make_backup(conn, tmp_path / "backups", f"batch-{i}", keep=2)
|
||||
@ -224,9 +215,8 @@ def _claimed_ensure_issue(conn, summary="Тема"):
|
||||
return c
|
||||
|
||||
|
||||
def test_reconcile_does_not_confirm_ensure_issue_without_reporter():
|
||||
def test_reconcile_does_not_confirm_ensure_issue_without_reporter(conn):
|
||||
"""ensure_issue по состоянию не подтверждается: совпадение темы не закрывает команду, заметка new:N не переезжает."""
|
||||
conn = fresh()
|
||||
c = _claimed_ensure_issue(conn)
|
||||
mirror.set_note(conn, "new:1", {"work_kind": "разработка"}, "t")
|
||||
created = db.utcnow().isoformat()
|
||||
@ -236,9 +226,8 @@ def test_reconcile_does_not_confirm_ensure_issue_without_reporter():
|
||||
assert mirror.get_note(conn, "new:1") is not None and mirror.get_note(conn, "A-7") is None
|
||||
|
||||
|
||||
def test_reconcile_does_not_overwrite_finished_command(monkeypatch):
|
||||
def test_reconcile_does_not_overwrite_finished_command(conn, monkeypatch):
|
||||
"""M-1: отчёт агента/отмена между check_confirmed и UPDATE не перезаписываются."""
|
||||
conn = fresh()
|
||||
queue.create_batch(conn, "x", "claude", [{"type": "ensure_sprint", "params": {"key": "A-1", "sprint": "Спринт 2"}}])
|
||||
c = queue.claim(conn)
|
||||
mirror.apply_state(conn, [{"key": "A-1", "summary": "т", "sprint": "Спринт 2", "labels": []}], [])
|
||||
@ -264,8 +253,7 @@ def _two_batches_with_dependent(conn):
|
||||
return a, b, first
|
||||
|
||||
|
||||
def test_redelivered_definer_blocks_dependents():
|
||||
conn = fresh()
|
||||
def test_redelivered_definer_blocks_dependents(conn):
|
||||
a, b, first = _two_batches_with_dependent(conn)
|
||||
assert queue.claim(conn)["id"] == first["id"] # довыдача определителя, зависимые ждут
|
||||
conn.execute("UPDATE command SET status = 'done', result = '{\"key\": \"A-9\"}' WHERE id = ?", (first["id"],))
|
||||
@ -273,8 +261,7 @@ def test_redelivered_definer_blocks_dependents():
|
||||
assert nxt["batch_id"] == a["id"] and nxt["params"]["source"] == "A-9"
|
||||
|
||||
|
||||
def test_auto_refresh_waits_for_claimed_without_report():
|
||||
conn = fresh()
|
||||
def test_auto_refresh_waits_for_claimed_without_report(conn):
|
||||
queue.claim(conn, agent_version=1, settings_version=1) # агент на связи
|
||||
queue.create_batch(conn, "y", "claude", [fields("A-3")])
|
||||
queue.claim(conn) # выдана, отчёта нет
|
||||
@ -283,8 +270,7 @@ def test_auto_refresh_waits_for_claimed_without_report():
|
||||
assert queue.maybe_auto_refresh(conn) is None # claimed без отчёта — очередь занята
|
||||
|
||||
|
||||
def test_redelivery_of_lost_claim_before_queue():
|
||||
conn = fresh()
|
||||
def test_redelivery_of_lost_claim_before_queue(conn):
|
||||
queue.create_batch(conn, "x", "claude", [fields("A-1"), fields("A-2")])
|
||||
c = queue.claim(conn)
|
||||
again = queue.claim(conn)
|
||||
@ -297,8 +283,7 @@ def test_redelivery_of_lost_claim_before_queue():
|
||||
assert queue.agent_view(conn)["redelivered"] == []
|
||||
|
||||
|
||||
def test_redelivery_limit_fails_command():
|
||||
conn = fresh()
|
||||
def test_redelivery_limit_fails_command(conn):
|
||||
queue.create_batch(conn, "x", "claude", [fields("A-1"), fields("A-2")])
|
||||
c = queue.claim(conn)
|
||||
queue.claim(conn)
|
||||
@ -310,8 +295,7 @@ def test_redelivery_limit_fails_command():
|
||||
assert queue.MAX_DELIVERIES == 3
|
||||
|
||||
|
||||
def test_redelivery_oldest_first():
|
||||
conn = fresh()
|
||||
def test_redelivery_oldest_first(conn):
|
||||
queue.create_batch(conn, "x", "claude", [fields("A-1")])
|
||||
queue.create_batch(conn, "y", "claude", [fields("A-2")])
|
||||
rows = conn.execute("SELECT id FROM command WHERE type = 'ensure_fields' ORDER BY id").fetchall()
|
||||
@ -320,16 +304,14 @@ def test_redelivery_oldest_first():
|
||||
assert queue.claim(conn)["id"] == rows[1]["id"]
|
||||
|
||||
|
||||
def test_redelivered_refresh_keeps_one_sync_run():
|
||||
conn = fresh()
|
||||
def test_redelivered_refresh_keeps_one_sync_run(conn):
|
||||
queue.create_batch(conn, "обновить", "claude", [{"type": "refresh", "params": {"keys": ["A-1"]}}])
|
||||
r = queue.claim(conn)
|
||||
assert queue.claim(conn)["id"] == r["id"]
|
||||
assert conn.execute("SELECT COUNT(*) AS n FROM sync_run WHERE command_id = ?", (r["id"],)).fetchone()["n"] == 1
|
||||
|
||||
|
||||
def test_redelivery_does_not_repeat_backup():
|
||||
conn = fresh()
|
||||
def test_redelivery_does_not_repeat_backup(conn):
|
||||
queue.create_batch(conn, "x", "claude", [fields("A-1")])
|
||||
labels = []
|
||||
queue.claim(conn, backup=labels.append)
|
||||
@ -337,9 +319,8 @@ def test_redelivery_does_not_repeat_backup():
|
||||
assert len(labels) == 1
|
||||
|
||||
|
||||
def test_migrated_claimed_is_redelivered():
|
||||
def test_migrated_claimed_is_redelivered(conn):
|
||||
"""Команда, взятая до миграции (deliveries = 1 после неё), довыдаётся со счётчиком 2."""
|
||||
conn = fresh()
|
||||
queue.create_batch(conn, "x", "claude", [fields("A-1")])
|
||||
cid = conn.execute("SELECT id FROM command WHERE type = 'ensure_fields'").fetchone()["id"]
|
||||
conn.execute("UPDATE command SET status = 'claimed', claimed_at = '2026-10-12T00:00:00+00:00', deliveries = 1 WHERE id = ?", (cid,))
|
||||
@ -347,8 +328,7 @@ def test_migrated_claimed_is_redelivered():
|
||||
assert queue.get_command(conn, cid)["deliveries"] == 2
|
||||
|
||||
|
||||
def test_report_for_queued_command_rejected():
|
||||
conn = fresh()
|
||||
def test_report_for_queued_command_rejected(conn):
|
||||
queue.create_batch(conn, "x", "claude", [fields("A-1")])
|
||||
cid = conn.execute("SELECT id FROM command WHERE type = 'ensure_fields'").fetchone()["id"]
|
||||
with pytest.raises(queue.NotClaimedError):
|
||||
@ -356,8 +336,7 @@ def test_report_for_queued_command_rejected():
|
||||
assert queue.get_command(conn, cid)["status"] == "queued"
|
||||
|
||||
|
||||
def test_late_report_after_force_cancel_keeps_issue_key():
|
||||
conn = fresh()
|
||||
def test_late_report_after_force_cancel_keeps_issue_key(conn):
|
||||
mirror.set_note(conn, "new:7", {"fact_status": "В работе"}, "import")
|
||||
queue.create_batch(conn, "x", "claude", [
|
||||
{"type": "ensure_issue", "params": {"temp_key": "new:7", "issue_type": "task", "summary": "Тема"}}])
|
||||
@ -371,8 +350,7 @@ def test_late_report_after_force_cancel_keeps_issue_key():
|
||||
assert again["result"] == {"key": "A-20", "created": True} # второй поздний отчёт ничего не меняет
|
||||
|
||||
|
||||
def test_late_failed_report_after_force_cancel_changes_nothing():
|
||||
conn = fresh()
|
||||
def test_late_failed_report_after_force_cancel_changes_nothing(conn):
|
||||
queue.create_batch(conn, "x", "claude", [
|
||||
{"type": "ensure_issue", "params": {"temp_key": "new:7", "issue_type": "task", "summary": "Тема"}}])
|
||||
c = queue.claim(conn)
|
||||
@ -388,16 +366,14 @@ def _fail_by_limit(conn, cid):
|
||||
assert queue.get_command(conn, cid)["status"] == "failed"
|
||||
|
||||
|
||||
def test_error_is_trimmed_to_1500():
|
||||
conn = fresh()
|
||||
def test_error_is_trimmed_to_1500(conn):
|
||||
queue.create_batch(conn, "x", "claude", [fields("A-1")])
|
||||
c = queue.claim(conn)
|
||||
out = queue.report_result(conn, c["id"], "failed", error="е" * 5000)
|
||||
assert out["error"] == "е" * 1500 and queue.ERROR_LIMIT == 1500
|
||||
|
||||
|
||||
def test_late_done_after_delivery_limit_keeps_issue_key():
|
||||
conn = fresh()
|
||||
def test_late_done_after_delivery_limit_keeps_issue_key(conn):
|
||||
mirror.set_note(conn, "new:8", {"fact_status": "План"}, "import")
|
||||
queue.create_batch(conn, "x", "claude", [
|
||||
{"type": "ensure_issue", "params": {"temp_key": "new:8", "issue_type": "task", "summary": "Тема"}}])
|
||||
@ -411,8 +387,7 @@ def test_late_done_after_delivery_limit_keeps_issue_key():
|
||||
assert again["result"] == {"key": "A-30", "created": True} # повтор ничего не меняет
|
||||
|
||||
|
||||
def test_cancelled_batch_with_done_command_is_cancelled():
|
||||
conn = fresh()
|
||||
def test_cancelled_batch_with_done_command_is_cancelled(conn):
|
||||
b = queue.create_batch(conn, "x", "claude", [fields("A-1"), fields("A-2"), fields("A-3")])
|
||||
first = queue.claim(conn)
|
||||
queue.report_result(conn, first["id"], "done", result={"changed": []})
|
||||
@ -424,8 +399,7 @@ def test_cancelled_batch_with_done_command_is_cancelled():
|
||||
assert view["counts"]["done"] == 2
|
||||
|
||||
|
||||
def test_empty_full_refresh_keeps_neighbors():
|
||||
conn = fresh()
|
||||
def test_empty_full_refresh_keeps_neighbors(conn):
|
||||
jql = settings.get(conn, "base_jql")
|
||||
mirror.apply_state(conn, [{"key": "A-1", "summary": "x", "labels": []}], [],
|
||||
fetched_at="2020-01-01T00:00:00+00:00") # прочитана давно — до старта прогона
|
||||
@ -435,8 +409,7 @@ def test_empty_full_refresh_keeps_neighbors():
|
||||
assert conn.execute("SELECT neighbor FROM issue WHERE key = 'A-1'").fetchone()["neighbor"] == 0
|
||||
|
||||
|
||||
def test_cancel_finished_batch_stays_done():
|
||||
conn = fresh()
|
||||
def test_cancel_finished_batch_stays_done(conn):
|
||||
b = queue.create_batch(conn, "x", "claude", [fields("A-1")])
|
||||
while (c := queue.claim(conn)):
|
||||
queue.report_result(conn, c["id"], "done", result={"changed": []})
|
||||
@ -445,8 +418,7 @@ def test_cancel_finished_batch_stays_done():
|
||||
assert conn.execute("SELECT cancelled_at FROM batch WHERE id = ?", (b["id"],)).fetchone()["cancelled_at"] is None
|
||||
|
||||
|
||||
def test_redelivered_full_refresh_marks_neighbors_once():
|
||||
conn = fresh()
|
||||
def test_redelivered_full_refresh_marks_neighbors_once(conn):
|
||||
jql = settings.get(conn, "base_jql")
|
||||
mirror.apply_state(conn, [{"key": "OLD", "summary": "x", "labels": []}], [], fetched_at="2020-01-01T00:00:00+00:00")
|
||||
queue.create_batch(conn, "полный", "claude", [{"type": "refresh", "params": {"jql": jql}}])
|
||||
@ -463,8 +435,7 @@ def test_redelivered_full_refresh_marks_neighbors_once():
|
||||
assert conn.execute("SELECT issues_count FROM sync_run WHERE command_id = ?", (r["id"],)).fetchone()["issues_count"] == 1
|
||||
|
||||
|
||||
def test_redelivered_refresh_resets_counts():
|
||||
conn = fresh()
|
||||
def test_redelivered_refresh_resets_counts(conn):
|
||||
queue.create_batch(conn, "обновить", "claude", [{"type": "refresh", "params": {"keys": ["A-1"]}}])
|
||||
r = queue.claim(conn)
|
||||
queue.record_state(conn, r["id"], 150, 40)
|
||||
@ -476,8 +447,7 @@ def test_redelivered_refresh_resets_counts():
|
||||
assert run["started_at"] == started
|
||||
|
||||
|
||||
def test_refresh_failed_by_limit_finishes_sync_run():
|
||||
conn = fresh()
|
||||
def test_refresh_failed_by_limit_finishes_sync_run(conn):
|
||||
queue.create_batch(conn, "обновить", "claude", [{"type": "refresh", "params": {"keys": ["A-1"]}}])
|
||||
r = queue.claim(conn)
|
||||
for _ in range(queue.MAX_DELIVERIES):
|
||||
@ -486,8 +456,7 @@ def test_refresh_failed_by_limit_finishes_sync_run():
|
||||
assert conn.execute("SELECT finished_at FROM sync_run WHERE command_id = ?", (r["id"],)).fetchone()["finished_at"]
|
||||
|
||||
|
||||
def test_cancel_batch_reports_still_claimed():
|
||||
conn = fresh()
|
||||
def test_cancel_batch_reports_still_claimed(conn):
|
||||
b = queue.create_batch(conn, "x", "claude", [fields("A-1"), fields("A-2")])
|
||||
c = queue.claim(conn)
|
||||
view = queue.cancel_batch(conn, b["id"])
|
||||
|
||||
@ -8,14 +8,15 @@ from jira_hub import db, settings
|
||||
FIXTURE = Path(__file__).parent / "fixtures" / "config.example.json"
|
||||
|
||||
|
||||
def fresh():
|
||||
conn = db.connect(":memory:")
|
||||
settings.ensure_defaults(conn)
|
||||
return conn
|
||||
@pytest.fixture
|
||||
def conn(open_db):
|
||||
"""База в памяти с настройками по умолчанию; закрывается после теста."""
|
||||
c = open_db()
|
||||
settings.ensure_defaults(c)
|
||||
return c
|
||||
|
||||
|
||||
def test_defaults_and_tokens():
|
||||
conn = fresh()
|
||||
def test_defaults_and_tokens(conn):
|
||||
assert settings.get(conn, "base_jql") == "project = AURORA AND labels = AWPCA"
|
||||
assert settings.get(conn, "pacing")["ensure_fields"] == 120
|
||||
tok = settings.tokens(conn)
|
||||
@ -23,8 +24,7 @@ def test_defaults_and_tokens():
|
||||
assert "tokens" not in settings.get_all(conn)
|
||||
|
||||
|
||||
def test_set_bumps_version():
|
||||
conn = fresh()
|
||||
def test_set_bumps_version(conn):
|
||||
v0 = settings.get(conn, "settings_version")
|
||||
v1 = settings.set_(conn, "pacing", {"ensure_fields": 5})
|
||||
assert v1 == v0 + 1
|
||||
@ -32,14 +32,12 @@ def test_set_bumps_version():
|
||||
|
||||
|
||||
@pytest.mark.parametrize("key", ["tokens", "settings_version", "schema_version"])
|
||||
def test_protected_keys(key):
|
||||
conn = fresh()
|
||||
def test_protected_keys(conn, key):
|
||||
with pytest.raises(ValueError):
|
||||
settings.set_(conn, key, "x")
|
||||
|
||||
|
||||
def test_init_from_config():
|
||||
conn = fresh()
|
||||
def test_init_from_config(conn):
|
||||
cfg = json.loads(FIXTURE.read_text(encoding="utf-8"))
|
||||
keys = settings.init_from_config(conn, cfg)
|
||||
assert "transitions_map" in keys
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user