sdlc-hub/tests/test_settings.py
Andrey Epifancev e20977ef49 Финальное ревью: пробельные поля ensure_fields, версия агента, чтение файлов, форма issues (#129, I-1, I-2, I-3, M-2, T2.1)
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>
2026-10-26 03:16:39 +04:00

186 lines
8.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)