Хаб: базовая JQL из проектов, настройки без base_jql/board_id, init заводит AWPCA (#61)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
b9f5a24512
commit
203c7e7b3c
@ -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 = [
|
||||
|
||||
@ -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":
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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}}])
|
||||
|
||||
@ -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():
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user