I-1 (R-8): ensure_fields с полем из одних пробелов даёт 422, очистка только "" или null. I-2: agent_view не падает на нецифровой версии агента (GET /agent, /agents). I-3 (R-9): files.size через fstat у открытого файла, _intact ловит OSError, put переписывает нечитаемый файл. M-2: /agent/state отвергает пустые не-списки в issues. T2.1, T3.1, T4.1-T4.3, T5.2, T6.1, T1.5: тесты и мелкие правки. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
186 lines
8.5 KiB
Python
186 lines
8.5 KiB
Python
import json
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
from sdlc_hub.core import queue, settings
|
||
from sdlc_hub.modules.jira import settings as jira_settings
|
||
from sdlc_hub.modules.jira.commands import CommandError
|
||
|
||
FIXTURE = Path(__file__).parent / "fixtures" / "config.example.json"
|
||
|
||
|
||
@pytest.fixture
|
||
def conn(open_db):
|
||
"""База в памяти с настройками по умолчанию; закрывается после теста."""
|
||
c = open_db()
|
||
settings.ensure_defaults(c)
|
||
return c
|
||
|
||
|
||
def test_defaults_and_tokens(conn):
|
||
assert settings.get(conn, "pacing")["ensure_fields"] == 120
|
||
tok = settings.tokens(conn)
|
||
assert len(tok["agents"]["jira"]) > 20 and tok["agents"]["jira"] != 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("jira") and "board_id" not in settings.agent_keys("jira")
|
||
assert "board_id" not in settings.for_agent(conn, "jira") # мёртвый ключ: агент читает его через .get (design 6)
|
||
|
||
|
||
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})
|
||
assert v1 == v0 + 1
|
||
assert settings.get(conn, "pacing") == {"ensure_fields": 5}
|
||
|
||
|
||
@pytest.mark.parametrize("key", ["tokens", "settings_version", "schema_version"])
|
||
def test_protected_keys(conn, key):
|
||
with pytest.raises(ValueError):
|
||
settings.set_(conn, key, "x")
|
||
|
||
|
||
def test_init_from_config(conn):
|
||
cfg = json.loads(FIXTURE.read_text(encoding="utf-8"))
|
||
keys = jira_settings.init_from_config(conn, cfg)
|
||
assert "link_types" in keys and "base_jql" not in keys and "board_id" not in keys
|
||
assert "transitions_map" not in keys and "status_labels" not in keys # fact-status-removal
|
||
assert {"transitions_map", "status_labels"}.isdisjoint(settings.get_all(conn))
|
||
assert "base_jql" not in settings.get_all(conn) and "board_id" not in settings.get_all(conn)
|
||
assert settings.get(conn, "field_ids")["planned_end"] == "customfield_10802"
|
||
assert settings.get(conn, "issue_types")["epic"] == {"name": "Фича", "id": "12300"}
|
||
assert settings.get(conn, "pacing") == jira_settings.DEFAULT_PACING
|
||
agent_view = settings.for_agent(conn, "jira")
|
||
assert "tokens" not in agent_view and "auth" not in agent_view and "base_url" not in agent_view
|
||
assert agent_view["link_types"]["связанные"] == "ASUPR_Связанные"
|
||
with pytest.raises(RuntimeError):
|
||
jira_settings.init_from_config(conn, cfg)
|
||
jira_settings.init_from_config(conn, cfg, force=True)
|
||
|
||
|
||
def test_relay_dir_default_and_validation(tmp_path, open_db):
|
||
conn = open_db(tmp_path / "hub.sqlite")
|
||
settings.ensure_defaults(conn)
|
||
assert settings.get(conn, "relay_dir") is None
|
||
settings.validate("relay_dir", None)
|
||
settings.validate("relay_dir", "/Volumes/externalSSD/utm/rshb/jira-bridge")
|
||
for bad in ("rshb/jira-bridge", "", 5, ["/x"]):
|
||
with pytest.raises(ValueError):
|
||
settings.validate("relay_dir", bad)
|
||
assert "relay_dir" not in settings.agent_keys("jira")
|
||
|
||
|
||
def test_init_without_bug_keys_rejects_bug(conn):
|
||
"""Спека data-model «Конфиг без ключей ошибки»: bug в issue_types нет, постановка bug — 422."""
|
||
cfg = json.loads(FIXTURE.read_text(encoding="utf-8"))
|
||
jira_settings.init_from_config(conn, cfg)
|
||
assert set(settings.get(conn, "issue_types")) == {"task", "epic"}
|
||
with pytest.raises(CommandError, match="нет настройки issue_types.bug"):
|
||
queue.create_batch(conn, "ошибка", "claude", [{"type": "ensure_issue", "params": {
|
||
"temp_key": "new:1", "issue_type": "bug", "summary": "x", "labels": ["AWPCA"]}}])
|
||
|
||
|
||
@pytest.mark.parametrize("extra, expected", [
|
||
({"issue_type_bug": "Ошибка"}, {"name": "Ошибка", "id": None}),
|
||
({"issue_type_bug_id": "10102"}, {"name": None, "id": "10102"}),
|
||
({"issue_type_bug": "Ошибка", "issue_type_bug_id": "10102"}, {"name": "Ошибка", "id": "10102"}),
|
||
])
|
||
def test_init_bug_from_config(conn, extra, expected):
|
||
cfg = {**json.loads(FIXTURE.read_text(encoding="utf-8")), **extra}
|
||
jira_settings.init_from_config(conn, cfg)
|
||
types = settings.get(conn, "issue_types")
|
||
assert types["bug"] == expected
|
||
assert types["task"] == {"name": "Задача", "id": "10100"} and types["epic"] == {"name": "Фича", "id": "12300"}
|
||
|
||
|
||
def test_attachment_pacing(conn):
|
||
assert jira_settings.DEFAULT_PACING["ensure_attachment"] == 30
|
||
assert jira_settings.pacing(conn)["ensure_attachment"] == 30
|
||
assert jira_settings.pacing_from_config({"issue_seconds": 90, "comment_seconds": 20})["ensure_attachment"] == 20
|
||
settings.validate("pacing", {"ensure_attachment": 10})
|
||
|
||
|
||
def test_closed_statuses_default(conn):
|
||
assert settings.get(conn, "closed_statuses") == ["Готово", "Снято"]
|
||
assert "closed_statuses" not in settings.agent_keys("jira")
|
||
assert "closed_statuses" not in settings.for_agent(conn, "jira")
|
||
|
||
|
||
def test_closed_statuses_must_be_list():
|
||
with pytest.raises(ValueError):
|
||
settings.validate("closed_statuses", "Готово")
|
||
settings.validate("closed_statuses", ["Готово"])
|
||
|
||
|
||
def test_pacing_fallback_for_new_type(conn):
|
||
settings.set_(conn, "pacing", {"ensure_fields": 5})
|
||
assert jira_settings.pacing(conn)["fetch_attachment"] == 2
|
||
settings.validate("pacing", {"fetch_attachment": 3})
|
||
|
||
|
||
def test_sprint_types_pacing(conn):
|
||
for t in ("ensure_sprint_meta", "ensure_sprint_state", "ensure_rank"):
|
||
assert jira_settings.DEFAULT_PACING[t] == 30 and jira_settings.pacing(conn)[t] == 30
|
||
assert jira_settings.pacing_from_config({"comment_seconds": 45})[t] == 45
|
||
settings.set_(conn, "pacing", {"ensure_fields": 5}) # тип не в сохранённой настройке — умолчание
|
||
assert jira_settings.pacing(conn)["ensure_rank"] == 30
|
||
settings.validate("pacing", {"ensure_rank": 10})
|
||
assert queue.estimate(conn, ["ensure_sprint_meta", "ensure_sprint_state", "ensure_rank"]) == 90
|
||
|
||
|
||
def test_v3_keys_removed_on_open(conn):
|
||
v0 = settings.get(conn, "settings_version")
|
||
conn.execute("INSERT OR REPLACE INTO setting(key, value) VALUES ('board_id', '4216'), ('base_jql', '\"project = AURORA\"')")
|
||
settings.ensure_defaults(conn)
|
||
stored = {r["key"] for r in conn.execute("SELECT key FROM setting")}
|
||
assert {"board_id", "base_jql"}.isdisjoint(stored)
|
||
assert settings.get(conn, "settings_version") == v0 # версия настроек не растёт
|
||
with pytest.raises(ValueError, match="доска Jira — свойство проекта"):
|
||
settings.validate("board_id", 4216)
|
||
with pytest.raises(ValueError, match="собирается из проектов"):
|
||
settings.validate("base_jql", "project = AURORA")
|
||
|
||
|
||
def test_v3_keys_removed_on_hub_start_api(tmp_path):
|
||
import threading
|
||
|
||
from sdlc_hub.core import api, db
|
||
from sdlc_hub.core.client import HubClient, HubError
|
||
path = tmp_path / "hub.sqlite"
|
||
c = db.connect(path)
|
||
settings.ensure_defaults(c)
|
||
c.execute("INSERT OR REPLACE INTO setting(key, value) VALUES ('board_id', '4216'), ('base_jql', '\"project = AURORA\"')")
|
||
c.commit()
|
||
c.close()
|
||
h = api.Hub(path, backups_dir=tmp_path / "backups")
|
||
conn = h.conn()
|
||
token = settings.tokens(conn)["client"]
|
||
conn.close()
|
||
srv = api.make_server(h, "127.0.0.1", 0)
|
||
th = threading.Thread(target=srv.serve_forever, daemon=True)
|
||
th.start()
|
||
try:
|
||
client = HubClient(f"http://127.0.0.1:{srv.server_port}", token)
|
||
got = client.get("/settings")
|
||
assert "board_id" not in got and got["base_jql"] is None # base_jql вычисляется (без проектов — None)
|
||
for key in ("board_id", "base_jql"):
|
||
with pytest.raises(HubError) as e:
|
||
client.put("/settings", {key: 1})
|
||
assert e.value.status == 422 and e.value.detail
|
||
finally:
|
||
srv.shutdown()
|
||
srv.server_close()
|
||
th.join(5)
|