From 203c7e7b3c15427d511bfbb11a82050b412341ac Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Mon, 19 Oct 2026 20:25:04 +0400 Subject: [PATCH] =?UTF-8?q?=D0=A5=D0=B0=D0=B1:=20=D0=B1=D0=B0=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D1=8F=20JQL=20=D0=B8=D0=B7=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B5=D0=BA=D1=82=D0=BE=D0=B2,=20=D0=BD=D0=B0=D1=81=D1=82?= =?UTF-8?q?=D1=80=D0=BE=D0=B9=D0=BA=D0=B8=20=D0=B1=D0=B5=D0=B7=20base=5Fjq?= =?UTF-8?q?l/board=5Fid,=20init=20=D0=B7=D0=B0=D0=B2=D0=BE=D0=B4=D0=B8?= =?UTF-8?q?=D1=82=20AWPCA=20(#61)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- src/jira_hub/api.py | 13 +++++--- src/jira_hub/cli.py | 5 ++- src/jira_hub/projects.py | 13 ++++++++ src/jira_hub/queue.py | 11 ++++--- src/jira_hub/settings.py | 11 ++++--- tests/test_api.py | 66 +++++++++++++++++++++++++++++++--------- tests/test_cli.py | 9 ++++-- tests/test_mcp.py | 3 +- tests/test_projects.py | 16 ++++++++++ tests/test_queue.py | 28 +++++++++++++---- tests/test_settings.py | 19 ++++++++++-- 11 files changed, 153 insertions(+), 41 deletions(-) diff --git a/src/jira_hub/api.py b/src/jira_hub/api.py index 4826f90..9b8b109 100644 --- a/src/jira_hub/api.py +++ b/src/jira_hub/api.py @@ -14,7 +14,7 @@ from pathlib import Path from typing import NamedTuple from urllib.parse import parse_qs, unquote, urlparse -from . import backup, db, files, mirror, queue, relay, release, settings +from . import backup, db, files, mirror, projects, queue, relay, release, settings from .commands import CommandError from .paths import backups_dir as default_backups_dir from .paths import secure_dir @@ -230,7 +230,7 @@ def h_issue(hub, conn, m, q, body): def h_board(hub, conn, m, q, body): - return 200, mirror.board(conn, settings.get(conn, "base_jql")) + return 200, mirror.board(conn, projects.base_jql(conn)) def h_app(hub, conn, m, q, body): @@ -343,7 +343,10 @@ def h_refresh(hub, conn, m, q, body): if body.get("jql") is not None and not isinstance(body["jql"], str): raise ApiError(422, "bad_refresh", "jql — строка") if body.get("full"): - params = {"jql": settings.get(conn, "base_jql")} + jql = projects.base_jql(conn) + if jql is None: + raise ApiError(422, "bad_refresh", "нет проектов") + params = {"jql": jql} elif body.get("jql"): params = {"jql": body["jql"]} elif body.get("keys"): @@ -369,7 +372,7 @@ def h_sync_runs(hub, conn, m, q, body): def h_settings_get(hub, conn, m, q, body): - return 200, settings.get_all(conn) + return 200, {**settings.get_all(conn), "base_jql": projects.base_jql(conn)} def h_settings_put(hub, conn, m, q, body): @@ -387,7 +390,7 @@ def h_settings_put(hub, conn, m, q, body): with db.transaction(conn): for key, value in body.items(): settings.set_(conn, key, value) - return 200, settings.get_all(conn) + return 200, {**settings.get_all(conn), "base_jql": projects.base_jql(conn)} ROUTES = [ diff --git a/src/jira_hub/cli.py b/src/jira_hub/cli.py index ce90d04..7e997eb 100644 --- a/src/jira_hub/cli.py +++ b/src/jira_hub/cli.py @@ -4,7 +4,7 @@ import argparse import json import sys -from . import db, importer, paths, settings +from . import db, importer, paths, projects, settings def main(argv=None) -> int: @@ -61,6 +61,9 @@ def main(argv=None) -> int: print(e, file=sys.stderr) return 1 print("перенесено: " + ", ".join(keys)) + if projects.ensure_legacy(conn, cfg.get("sprints", {}).get("board_id")): + board = projects.get(conn, projects.LEGACY_LABEL)["board_id"] + print(f"проект {projects.LEGACY_LABEL} «{projects.LEGACY_NAME}», доска {board}") _print_tokens(conn) return 0 if args.cmd == "import": diff --git a/src/jira_hub/projects.py b/src/jira_hub/projects.py index d42074e..a231d32 100644 --- a/src/jira_hub/projects.py +++ b/src/jira_hub/projects.py @@ -130,6 +130,19 @@ def parse_legacy_labels(jql) -> list[str] | None: return out or None +def ensure_legacy(conn, board_id: int | None) -> bool: + """Завести AWPCA «АРМ КА» с доской, если проекта с меткой awpca (без учёта регистра) нет; True — заведён. + Неверная доска из конфига — проект без доски.""" + if LEGACY_LABEL.casefold() in {x.casefold() for x in labels(conn)}: + return False + try: + validate(LEGACY_LABEL, LEGACY_NAME, board_id) + except ProjectError: + board_id = None + create(conn, LEGACY_LABEL, LEGACY_NAME, board_id) + return True + + def migrate_v3(conn) -> None: """Миграция v3 → v4: проекты из прежней base_jql, доска — единственному проекту, очередь и журнал — на новую JQL.""" old = settings.get(conn, "base_jql") diff --git a/src/jira_hub/queue.py b/src/jira_hub/queue.py index 66d2dcc..7a7095b 100644 --- a/src/jira_hub/queue.py +++ b/src/jira_hub/queue.py @@ -7,7 +7,7 @@ from datetime import timedelta from . import backup as _backup # noqa: F401 (имя модуля занято параметром backup в claim) from . import commands as C -from . import db, files, mirror, settings +from . import db, files, mirror, projects, settings log = logging.getLogger(__name__) @@ -248,8 +248,8 @@ def is_full_refresh(conn, command_id) -> bool: if not command_id: return False cmd = get_command(conn, command_id) - return bool(cmd and cmd["type"] == "refresh" and cmd["params"].get("jql") - and cmd["params"]["jql"] == settings.get(conn, "base_jql")) + base = projects.base_jql(conn) + return bool(base and cmd and cmd["type"] == "refresh" and cmd["params"].get("jql") == base) def record_state(conn, command_id, issues_count, links_count) -> None: @@ -373,6 +373,9 @@ def agent_view(conn) -> dict: def maybe_auto_refresh(conn) -> dict | None: + jql = projects.base_jql(conn) + if jql is None: + return None minutes = int(settings.get(conn, "auto_refresh_minutes") or 0) if minutes <= 0 or not agent_view(conn)["online"]: return None @@ -381,4 +384,4 @@ def maybe_auto_refresh(conn) -> dict | None: last = conn.execute("SELECT MAX(started_at) AS t FROM sync_run").fetchone()["t"] if last and db.utcnow() - db.parse(last) < timedelta(minutes=minutes): return None - return create_batch(conn, "Автообновление", "hub", [{"type": "refresh", "params": {"jql": settings.get(conn, "base_jql")}}]) + return create_batch(conn, "Автообновление", "hub", [{"type": "refresh", "params": {"jql": jql}}]) diff --git a/src/jira_hub/settings.py b/src/jira_hub/settings.py index 5a49561..c278a7c 100644 --- a/src/jira_hub/settings.py +++ b/src/jira_hub/settings.py @@ -8,7 +8,6 @@ DEFAULT_PACING = {"ensure_issue": 120, "ensure_fields": 120, "ensure_comment": 3 "ensure_link": 30, "ensure_sprint": 30, "ensure_attachment": 30, "refresh": 2} DEFAULTS = { - "base_jql": "project = AURORA AND labels = AWPCA", "project_key": "AURORA", "pacing": DEFAULT_PACING, "backup_keep": 30, @@ -29,13 +28,12 @@ DEFAULTS = { "comment_marker": "", "transitions_map": {}, "link_types": {}, - "board_id": None, "relay_dir": None, "closed_statuses": ["Готово", "Снято"], # статусы Jira закрытых задач: список и доска приложения (hide-closed) } # Что получает агент (без токенов и служебных ключей хаба) -AGENT_KEYS = ["base_jql", "project_key", "issue_types", "epic_name_field_id", "field_ids", "epic_link", +AGENT_KEYS = ["project_key", "issue_types", "epic_name_field_id", "field_ids", "epic_link", "priority_map", "priority_value_key", "labels", "status_labels", "kind_labels", "comment_marker", "transitions_map", "link_types", "board_id", "pacing", "min_agent_version", "settings_version"] @@ -48,11 +46,16 @@ def _is_int(v) -> bool: def validate(key, value) -> None: """Проверить значение настройки до записи; ValueError — на 422 в API. + base_jql и board_id не пишутся: JQL собирается из проектов, доска — свойство проекта; pacing — объект {тип команды: целое >= 0} только с известными типами команд; backup_keep, auto_refresh_minutes, min_agent_version — целые >= 0; port — целое 1..65535; relay_dir — null или абсолютный путь строкой; остальные ключи — по типу значения по умолчанию (объект, список, строка, флаг), если оно не None; неизвестные ключи и ключи с умолчанием None принимаются как есть.""" + if key == "base_jql": + raise ValueError("базовая JQL собирается из проектов (GET /projects)") + if key == "board_id": + raise ValueError("доска Jira — свойство проекта (POST /projects)") if key == "pacing": if not isinstance(value, dict): raise ValueError("pacing — объект {тип команды: пауза в секундах}") @@ -153,7 +156,6 @@ def init_from_config(conn, cfg: dict, force=False) -> list[str]: project = cfg["project_key"] values = { "project_key": project, - "base_jql": f"project = {project} AND labels = AWPCA", "issue_types": _issue_types(cfg), "epic_name_field_id": cfg.get("epic_name_field_id"), "field_ids": cfg.get("fields", {}), @@ -166,7 +168,6 @@ def init_from_config(conn, cfg: dict, force=False) -> list[str]: "comment_marker": cfg.get("comment_marker", ""), "transitions_map": cfg.get("transitions", {}).get("map", {}), "link_types": cfg.get("link_types", {}), - "board_id": cfg.get("sprints", {}).get("board_id"), "pacing": pacing_from_config(cfg.get("pacing", {})), } for key, value in values.items(): diff --git a/tests/test_api.py b/tests/test_api.py index 1351418..7aa6889 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -11,7 +11,7 @@ from datetime import timedelta import pytest -from jira_hub import api, db, files, queue, release, settings +from jira_hub import api, db, files, projects, queue, release, settings from jira_hub.client import HubClient, HubError @@ -33,6 +33,14 @@ def hub(tmp_path): th.join(5) +def _add_project(hub, add_project, *args, **kw): + conn = hub["hub"].conn() + try: + return add_project(conn, *args, **kw) + finally: + conn.close() + + def test_auth_scopes(hub): with pytest.raises(HubError) as e: hub["agent"].get("/issues") @@ -93,7 +101,8 @@ def test_cancel_batch_api_reports_still_claimed(hub): assert out["still_claimed"] == [{"id": cmd["id"], "type": "ensure_fields"}] -def test_interface_routes(hub): +def test_interface_routes(hub, add_project): + _add_project(hub, add_project) c = hub["client"] with pytest.raises(HubError) as e: c.get("/issues/A-404") @@ -121,7 +130,7 @@ def test_interface_routes(hub): retry = c.post(f"/commands/{cmd['id']}/retry", {"author": "claude"}) assert c.get(f"/batches/{retry['id']}")["title"] == f"Повтор команды {cmd['id']}" full = c.post("/refresh", {"full": True}) - assert c.get(f"/batches/{full['id']}")["commands"][0]["params"]["jql"] == "project = AURORA AND labels = AWPCA" + assert c.get(f"/batches/{full['id']}")["commands"][0]["params"]["jql"] == "project = AURORA AND labels in (AWPCA)" keys = c.post("/refresh", {"keys": ["A-1"]}) assert c.get(f"/batches/{keys['id']}")["commands"][0]["params"] == {"keys": ["A-1"]} @@ -265,8 +274,7 @@ def test_settings_put_protected_key_in_batch_rejects_whole_batch(hub): @pytest.mark.parametrize("body", [ {"pacing": 5}, {"pacing": {"ensure_fields": "abc"}}, {"pacing": {"ensure_fields": -1}}, {"pacing": {"неизвестная": 1}}, {"port": "x"}, {"port": 0}, {"port": 70000}, {"backup_keep": -1}, - {"auto_refresh_minutes": 1.5}, {"min_agent_version": "2"}, {"link_types": []}, {"base_jql": 5}, - {"labels": {}}, {"initialized": 1}, {"closed_statuses": "Готово"}, + {"auto_refresh_minutes": 1.5}, {"min_agent_version": "2"}, {"link_types": []}, {"labels": {}}, {"initialized": 1}, {"closed_statuses": "Готово"}, {"pacing": {"refresh": 3}, "backup_keep": -1}, # хорошее рядом с плохим не записывается ]) def test_settings_put_invalid_value_is_422(hub, body): @@ -278,15 +286,33 @@ def test_settings_put_invalid_value_is_422(hub, body): assert c.get("/settings") == before +@pytest.mark.parametrize("body, text", [({"base_jql": "project = AURORA"}, "базовая JQL собирается из проектов"), + ({"board_id": 12}, "доска Jira — свойство проекта")]) +def test_settings_put_rejects_jql_and_board(hub, body, text): + c = hub["client"] + v0 = c.get("/settings")["settings_version"] + with pytest.raises(HubError) as e: + c.put("/settings", body) + assert e.value.status == 422 and text in str(e.value.detail) + assert c.get("/settings")["settings_version"] == v0 + + +def test_full_refresh_without_projects(hub): + with pytest.raises(HubError) as e: + hub["client"].post("/refresh", {"full": True}) + assert e.value.status == 422 and "нет проектов" in str(e.value.detail) + assert hub["client"].get("/batches")["batches"] == [] + + def test_settings_put_valid_values_pass(hub): c = hub["client"] v0 = c.get("/settings")["settings_version"] out = c.put("/settings", {"pacing": {"refresh": 0, "ensure_fields": 90}, "backup_keep": 0, "port": 8800, "auto_refresh_minutes": 15, "min_agent_version": 3, "link_types": {"x": "Y"}, - "labels": ["AWPCA"], "board_id": 12}) - assert out["settings_version"] == v0 + 8 + "labels": ["AWPCA"]}) + assert out["settings_version"] == v0 + 7 got = c.get("/settings") - assert got["pacing"]["ensure_fields"] == 90 and got["port"] == 8800 and got["board_id"] == 12 + assert got["pacing"]["ensure_fields"] == 90 and got["port"] == 8800 def _run_refresh(hub, body, issues, neighbor_keys=()): @@ -299,8 +325,9 @@ def _run_refresh(hub, body, issues, neighbor_keys=()): hub["agent"].post(f"/agent/commands/{cmd['id']}/result", {"status": "done"}) -def test_refresh_by_keys_keeps_neighbor_flag(hub): +def test_refresh_by_keys_keeps_neighbor_flag(hub, add_project): """I-2(b): точечное обновление соседней задачи не опускает флаг.""" + _add_project(hub, add_project) _run_refresh(hub, {"full": True}, [{"key": "A-1", "summary": "т", "labels": []}, {"key": "B-1", "summary": "сосед", "labels": []}], neighbor_keys=["B-1"]) assert hub["client"].get("/issues/B-1")["neighbor"] is True @@ -309,8 +336,9 @@ def test_refresh_by_keys_keeps_neighbor_flag(hub): assert issue["summary"] == "сосед 2" and issue["neighbor"] is True -def test_full_refresh_marks_unseen_issue_as_neighbor(hub): +def test_full_refresh_marks_unseen_issue_as_neighbor(hub, add_project): """I-2(a): полный прогон по базовой JQL: не обновлённая им задача получает neighbor=1.""" + _add_project(hub, add_project) conn = hub["hub"].conn() conn.execute("INSERT INTO issue(key, summary, labels, fetched_at) VALUES ('X-1', 'вышла из выборки', '[]', '2020-01-01T00:00:00+00:00')") conn.close() @@ -350,7 +378,8 @@ def test_duplicate_summary_batch_is_422(hub): assert e.value.status == 422 and "Шахматка: комментарии" in e.value.detail -def test_board_route(hub): +def test_board_route(hub, add_project): + _add_project(hub, add_project) a, c = hub["agent"], hub["client"] a.post("/agent/state", {"issues": [{"key": "A-1", "summary": "Т", "labels": ["kind-dev"]}, {"key": "B-1", "summary": "Сосед", "labels": []}], @@ -517,13 +546,20 @@ def test_agent_reported_even_with_bad_update_fields(hub, tmp_path): assert v["update_hint"] == "агент запущен без run.py — обновится только вручную" +def test_settings_get_base_jql_from_projects(hub, add_project): + c = hub["client"] + assert c.get("/settings")["base_jql"] is None + _add_project(hub, add_project) + assert c.get("/settings")["base_jql"] == "project = AURORA AND labels in (AWPCA)" + + def test_settings_put_returns_all_settings(hub): """O-11 #16, решение 9, спека «Ответ на запись настроек»: все настройки после записи, как GET /settings, без токенов.""" c = hub["client"] v0 = c.get("/settings")["settings_version"] out = c.put("/settings", {"relay_dir": "/Volumes/externalSSD/utm/rshb/jira-bridge"}) assert out["relay_dir"] == "/Volumes/externalSSD/utm/rshb/jira-bridge" and out["settings_version"] == v0 + 1 - assert "tokens" not in out and out["base_jql"] == "project = AURORA AND labels = AWPCA" + assert "tokens" not in out and out["base_jql"] is None assert out == c.get("/settings") @@ -568,8 +604,9 @@ def test_agent_warning_and_offline(hub): assert c.get("/agent")["online"] is False -def test_tick_creates_auto_refresh_once(hub): +def test_tick_creates_auto_refresh_once(hub, add_project): """M-10 #4: тик при агенте на связи и пустой очереди ставит «Автообновление»; второй тик — нет (очередь занята).""" + _add_project(hub, add_project) hub["agent"].post("/agent/claim", {"agent_version": 9}) hub["hub"].tick() batches = hub["client"].get("/batches", limit=5)["batches"] @@ -700,7 +737,8 @@ def test_tick_sweeps_files_once_a_day(hub, tmp_path, monkeypatch): assert len(calls) == 2 -def test_tick_sweep_failure_logged_auto_refresh_still_runs(hub, monkeypatch, caplog): +def test_tick_sweep_failure_logged_auto_refresh_still_runs(hub, monkeypatch, caplog, add_project): + _add_project(hub, add_project) def broken(*args, **kwargs): raise OSError("диск недоступен") monkeypatch.setattr(api.files, "sweep", broken) diff --git a/tests/test_cli.py b/tests/test_cli.py index 8ea8b83..0f823a6 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -2,7 +2,7 @@ import json import os from pathlib import Path -from jira_hub import cli, paths, release, settings +from jira_hub import cli, paths, projects, release, settings FIXTURE = Path(__file__).parent / "fixtures" / "config.example.json" @@ -15,7 +15,12 @@ def test_init_import_tokens(tmp_path, monkeypatch, capsys, open_db): assert cli.main(["init", "--config", str(FIXTURE)]) == 1 assert "уже инициализирован" in capsys.readouterr().err conn = open_db(paths.db_path()) - assert settings.get(conn, "board_id") == 4216 + assert [(x["label"], x["name"], x["board_id"]) for x in projects.list_projects(conn)] == [("AWPCA", "АРМ КА", 4216)] + assert settings.get(conn, "board_id") is None + assert "проект AWPCA «АРМ КА», доска 4216" in out + assert cli.main(["init", "--config", str(FIXTURE), "--force"]) == 0 + capsys.readouterr() + assert [(x["label"], x["board_id"]) for x in projects.list_projects(conn)] == [("AWPCA", 4216)] (tmp_path / "pkg").mkdir() (tmp_path / "pkg" / "tasks.csv").write_text("Код,Статус по факту,Вид работы\nAURORA-1,В работе,UI\n", encoding="utf-8") assert cli.main(["import", str(tmp_path / "pkg")]) == 0 diff --git a/tests/test_mcp.py b/tests/test_mcp.py index 3568442..c051975 100644 --- a/tests/test_mcp.py +++ b/tests/test_mcp.py @@ -13,10 +13,11 @@ from jira_hub.client import HubClient @pytest.fixture -def live(tmp_path, monkeypatch): +def live(tmp_path, monkeypatch, add_project): h = api.Hub(tmp_path / "hub.sqlite", backups_dir=tmp_path / "backups") h.claim_wait = 0 conn = h.conn() + add_project(conn) tok = settings.tokens(conn) conn.close() srv = api.make_server(h, "127.0.0.1", 0) diff --git a/tests/test_projects.py b/tests/test_projects.py index 8196c31..80251e2 100644 --- a/tests/test_projects.py +++ b/tests/test_projects.py @@ -98,3 +98,19 @@ def test_parse_legacy_labels(jql, labels): def test_add_project_fixture(conn, add_project): p = add_project(conn) assert (p["name"], p["board_id"], p["board_type"]) == ("АРМ КА", 4216, "scrum") + + +def test_ensure_legacy(conn): + assert projects.ensure_legacy(conn, 4216) is True + assert projects.get(conn, "AWPCA")["board_id"] == 4216 and projects.get(conn, "AWPCA")["name"] == "АРМ КА" + assert projects.ensure_legacy(conn, 1) is False + + +def test_ensure_legacy_skips_case_variant_and_bad_board(open_db): + c = open_db() + settings.ensure_defaults(c) + projects.create(c, "awpca", "x") + assert projects.ensure_legacy(c, 4216) is False + c2 = open_db() + settings.ensure_defaults(c2) + assert projects.ensure_legacy(c2, "oops") is True and projects.get(c2, "AWPCA")["board_id"] is None diff --git a/tests/test_queue.py b/tests/test_queue.py index aea73c9..e03c462 100644 --- a/tests/test_queue.py +++ b/tests/test_queue.py @@ -2,7 +2,7 @@ import hashlib import pytest -from jira_hub import db, files, mirror, queue, settings +from jira_hub import db, files, mirror, projects, queue, settings from jira_hub.commands import CommandError from jira_hub.queue import ClaimedError @@ -156,7 +156,21 @@ def test_reconcile_confirms_by_state(conn): assert queue.batch_view(conn, b["id"])["status"] == "done" -def test_auto_refresh_conditions(conn): +def test_auto_refresh_needs_projects(conn): + queue.claim(conn, agent_version=1, settings_version=1) + assert queue.maybe_auto_refresh(conn) is None # агент на связи, очередь пуста, проектов нет + + +def test_auto_refresh_two_projects(conn, add_project): # сценарий «Два проекта» + add_project(conn) + add_project(conn, "STOCKS_DRRK", board_id=None) + queue.claim(conn, agent_version=1, settings_version=1) + b = queue.maybe_auto_refresh(conn) + assert queue.batch_view(conn, b["id"])["commands"][0]["params"] == {"jql": "project = AURORA AND labels in (AWPCA, STOCKS_DRRK)"} + + +def test_auto_refresh_conditions(conn, add_project): + add_project(conn) assert queue.maybe_auto_refresh(conn) is None # агент не на связи queue.claim(conn, agent_version=1, settings_version=1) # отметился, очередь пуста b = queue.maybe_auto_refresh(conn) @@ -401,8 +415,9 @@ def test_cancelled_batch_with_done_command_is_cancelled(conn): assert view["counts"]["done"] == 2 -def test_empty_full_refresh_keeps_neighbors(conn): - jql = settings.get(conn, "base_jql") +def test_empty_full_refresh_keeps_neighbors(conn, add_project): + add_project(conn) + jql = projects.base_jql(conn) mirror.apply_state(conn, [{"key": "A-1", "summary": "x", "labels": []}], [], fetched_at="2020-01-01T00:00:00+00:00") # прочитана давно — до старта прогона queue.create_batch(conn, "полный", "claude", [{"type": "refresh", "params": {"jql": jql}}]) @@ -420,8 +435,9 @@ def test_cancel_finished_batch_stays_done(conn): 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): - jql = settings.get(conn, "base_jql") +def test_redelivered_full_refresh_marks_neighbors_once(conn, add_project): + add_project(conn) + jql = projects.base_jql(conn) 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}}]) r = queue.claim(conn) diff --git a/tests/test_settings.py b/tests/test_settings.py index cbaa702..1f60b01 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -18,13 +18,26 @@ def conn(open_db): 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) assert len(tok["agent"]) > 20 and tok["agent"] != tok["client"] assert "tokens" not in settings.get_all(conn) +def test_defaults_without_jql_and_board(conn): + assert "base_jql" not in settings.DEFAULTS and "board_id" not in settings.DEFAULTS + assert "base_jql" not in settings.get_all(conn) and "board_id" not in settings.get_all(conn) + assert "base_jql" not in settings.AGENT_KEYS and "board_id" in settings.AGENT_KEYS + assert settings.for_agent(conn)["board_id"] is None + + +def test_jql_and_board_not_writable(): + with pytest.raises(ValueError, match="собирается из проектов"): + settings.validate("base_jql", "project = AURORA") + with pytest.raises(ValueError, match="свойство проекта"): + settings.validate("board_id", 1) + + def test_set_bumps_version(conn): v0 = settings.get(conn, "settings_version") v1 = settings.set_(conn, "pacing", {"ensure_fields": 5}) @@ -41,10 +54,10 @@ def test_protected_keys(conn, key): 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 + assert "transitions_map" in keys and "base_jql" not in keys and "board_id" not in keys + assert settings.get(conn, "base_jql") is None and settings.get(conn, "board_id") is None assert settings.get(conn, "field_ids")["planned_end"] == "customfield_10802" assert settings.get(conn, "issue_types")["epic"] == {"name": "Фича", "id": "12300"} - assert settings.get(conn, "board_id") == 4216 assert settings.get(conn, "pacing") == settings.DEFAULT_PACING agent_view = settings.for_agent(conn) assert "tokens" not in agent_view and "auth" not in agent_view and "base_url" not in agent_view