sdlc-hub/tests/test_agents.py
Andrey Epifancev 4ea287fbdf Модуль wiki: корень проекта, команды, фоновая синхронизация (#91)
Каркас модуля wiki: схема (wiki_project, wiki_page, wiki_attachment, wiki_sync_stage), настройки,
поле проекта wiki_root с хуками создания и правки, команды wiki_sync и wiki_fetch,
фоновая синхронизация. Зависимость markdownify>=1.2.3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 01:53:31 +04:00

263 lines
13 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.

"""Агенты модулей: токен определяет модуль, миграция v7 сохраняет агента jira (wiki-module, решения 1–3, 6, 15)."""
import os
import pytest
from jira_hub.core import api, cli, db, queue, release, settings
from jira_hub.core.client import HubError
from v6db import make_v6_db
def test_v6_upgrade_keeps_jira_agent(tmp_path):
make_v6_db(tmp_path / "hub.sqlite")
rel = tmp_path / "release"
rel.mkdir()
(rel / "agent.py").write_text("AGENT_VERSION = 14\n", encoding="utf-8")
release.publish(rel, rel / "agent.py")
hub = api.Hub(tmp_path / "hub.sqlite", backups_dir=tmp_path / "backups")
hub.claim_wait = 0
conn = db.connect(tmp_path / "hub.sqlite")
try:
tok = settings.tokens(conn)["agents"]["jira"]
assert tok == "T-agent"
code, body = api.h_claim(hub, conn, None, {"_agent": "jira"}, {"agent_version": 14})
assert code == 200 and body["agent_release"]["version"] == 14 # релиз v14 перенесён в release/jira/
assert db._schema_version(conn) == 7
tok = settings.tokens(conn)
assert tok["client"] == "T-client" and tok["agents"]["jira"] == "T-agent" and "agent" not in tok
row = conn.execute("SELECT * FROM agent_status WHERE agent = 'jira'").fetchone()
assert row["agent_version"] == "14"
assert conn.execute("SELECT COUNT(*) AS n FROM agent_status").fetchone()["n"] == 2 # jira и wiki
assert list((tmp_path / "backups").glob("*schema-v6*")) # копия перед миграцией
finally:
conn.close()
def test_v6_upgrade_keeps_settings_and_projects(tmp_path):
make_v6_db(tmp_path / "hub.sqlite")
conn = db.connect(tmp_path / "hub.sqlite")
try:
assert settings.get(conn, "min_agent_version") == 1
rows = conn.execute("SELECT label, board_id, board_type FROM jira_project ORDER BY label").fetchall()
assert [tuple(r) for r in rows] == [("AWPCA", 4216, "scrum"), ("sfm_kedr", None, None)]
finally:
conn.close()
def test_migrate_adds_status_row_for_new_agent_module(conn_two_modules):
"""R-1: строки agent_status для модулей реестра вставляются при каждом migrate, а не только в шаге 6 → 7."""
names = {r["agent"] for r in conn_two_modules.execute("SELECT agent FROM agent_status")}
assert names == {"jira", "stub"}
def test_ensure_defaults_adds_missing_agent_token(conn_two_modules):
tok = settings.tokens(conn_two_modules)
assert set(tok["agents"]) == {"jira", "stub"} and tok["agents"]["jira"] != tok["agents"]["stub"]
def test_agent_token_routes(hub_server):
assert hub_server["agent"].post("/agent/claim", {})["command"] is None
assert hub_server["agent"].get("/agent/settings")["settings_version"] >= 1
with pytest.raises(HubError) as e:
hub_server["client"].post("/agent/claim", {})
assert e.value.status == 401
def test_module_agent_route_requires_own_module(hub_server_two_modules):
jira, stub = hub_server_two_modules["agents"]["jira"], hub_server_two_modules["agents"]["stub"]
with pytest.raises(HubError) as e:
stub.post("/agent/state", {})
assert e.value.status == 401
with pytest.raises(HubError) as e:
jira.post("/agent/stub", {})
assert e.value.status == 401
assert stub.post("/agent/stub", {}) == {"agent": "stub"}
assert jira.post("/agent/claim", {})["command"] is None # маршруты ядра открыты обоим агентам
assert stub.post("/agent/claim", {})["command"] is None
def test_agent_settings_per_module(hub_server_two_modules):
jira = hub_server_two_modules["agents"]["jira"].get("/agent/settings")
stub = hub_server_two_modules["agents"]["stub"].get("/agent/settings")
assert "project_key" in jira and "transitions_map" in jira and "settings_version" in jira
assert "stub_key" not in jira and "board_id" not in jira
assert set(stub) == {"stub_key", "settings_version"}
def test_agent_query_cannot_be_forged(hub_server_two_modules):
"""Служебный ключ запроса приходит только от диспетчера: _agent в адресе перезаписывается."""
stub = hub_server_two_modules["agents"]["stub"]
assert stub.call("POST", "/agent/stub", {}, params={"_agent": "jira"}) == {"agent": "stub"}
def test_tokens_cli_lists_agents(tmp_path, monkeypatch, capsys):
monkeypatch.setenv("JIRA_HUB_HOME", str(tmp_path))
assert cli.main(["tokens"]) == 0
lines = capsys.readouterr().out.splitlines()
assert lines[0].startswith("клиентский токен: ") and lines[1].startswith("агент jira: ")
assert lines[2].startswith("агент wiki: ") and lines[3].startswith("база: ")
def test_agent_view_min_version_from_module_key(conn_two_modules):
conn_two_modules.execute("UPDATE agent_status SET agent_version = '3' WHERE agent = 'jira'")
settings.set_(conn_two_modules, "min_agent_version", 5)
assert queue.agent_view(conn_two_modules, "jira")["warning"] == "обновите агента"
assert queue.agent_view(conn_two_modules, "stub")["warning"] is None
# --- очередь, релиз и статус по агенту (Task 2) ------------------------------------------------------------------
def _two_batches(conn):
queue.create_batch(conn, "w", "t", [{"type": "stub_cmd", "params": {}}])
queue.create_batch(conn, "j", "t", [{"type": "ensure_issue",
"params": {"temp_key": "new:1", "issue_type": "task", "summary": "Тема"}}])
def test_wiki_queue_does_not_block_jira(conn_two_modules):
conn = conn_two_modules
_two_batches(conn)
s = queue.claim(conn, "stub")
assert s["type"] == "stub_cmd"
j = queue.claim(conn, "jira") # stub_cmd ещё claimed, но это очередь другого агента
assert j["type"] == "ensure_issue"
assert queue.agent_view(conn, "stub")["current_command_id"] == s["id"]
assert queue.agent_view(conn, "jira")["current_command_id"] == j["id"]
def test_redeliver_only_own(conn_two_modules):
conn = conn_two_modules
_two_batches(conn)
s = queue.claim(conn, "stub")
j = queue.claim(conn, "jira")
assert queue.claim(conn, "jira")["id"] == j["id"] # довыдача — своя команда, не stub_cmd
assert queue.claim(conn, "stub")["id"] == s["id"]
assert queue.agent_view(conn, "jira")["redelivered"][0]["id"] == j["id"]
assert [r["id"] for r in queue.agent_view(conn, "stub")["redelivered"]] == [s["id"]]
def test_agent_version_per_agent(conn_two_modules):
conn = conn_two_modules
queue.claim(conn, "jira", agent_version=14)
assert queue.agent_version(conn, "jira") == "14" and queue.agent_version(conn, "stub") is None
queue.claim(conn, "stub", agent_version=1)
assert queue.agent_version(conn, "jira") == "14" and queue.agent_version(conn, "stub") == "1"
def test_has_active_per_module(conn_two_modules):
conn = conn_two_modules
assert not queue.has_active(conn)
queue.create_batch(conn, "w", "t", [{"type": "stub_cmd", "params": {}}])
assert queue.has_active(conn) and queue.has_active(conn, "stub") and not queue.has_active(conn, "jira")
def _publish(root, module, tmp_path, version):
src = tmp_path / f"{module}{version}.py"
src.write_text(f"AGENT_VERSION = {version}\n", encoding="utf-8")
return release.publish(release.module_dir(root, module), src)
def test_release_per_module(hub_server_two_modules, tmp_path):
root = hub_server_two_modules["hub"].release_root
_publish(root, "jira", tmp_path, 14)
_publish(root, "stub", tmp_path, 1)
assert release.module_dir(root, "stub") == root / "stub" and oct(release.module_dir(root, "stub").stat().st_mode & 0o777) == "0o700"
jira, stub = hub_server_two_modules["agents"]["jira"], hub_server_two_modules["agents"]["stub"]
assert jira.post("/agent/claim", {})["agent_release"]["version"] == 14
assert stub.post("/agent/claim", {})["agent_release"]["version"] == 1
assert jira.get("/agent/release")["version"] == 14 and stub.get("/agent/release")["version"] == 1
def test_release_layout_migration(tmp_path):
root = tmp_path / "release"
root.mkdir()
(root / "agent.py").write_text("AGENT_VERSION = 14\n", encoding="utf-8")
release.publish(root, root / "agent.py") # старая раскладка: agent.py и manifest.json в корне
hub = api.Hub(tmp_path / "hub.sqlite", backups_dir=tmp_path / "backups")
assert not (root / "agent.py").exists() and not (root / "manifest.json").exists()
assert release.current(hub.release_root / "jira")["version"] == 14
api.Hub(tmp_path / "hub.sqlite", backups_dir=tmp_path / "backups") # повторный старт ничего не ломает
assert release.current(root / "jira")["version"] == 14
def test_migrate_layout_failure_is_warning(tmp_path, monkeypatch, caplog):
root = tmp_path / "release"
root.mkdir()
(root / "agent.py").write_text("AGENT_VERSION = 14\n", encoding="utf-8")
real = os.replace
monkeypatch.setattr("os.replace", lambda *a, **k: (_ for _ in ()).throw(OSError(13, "нет прав")))
release.migrate_layout(root) # без исключения
assert any("release" in r.getMessage() for r in caplog.records)
assert (root / "agent.py").exists()
monkeypatch.setattr("os.replace", real)
def test_migrate_layout_resumes_after_partial_failure(tmp_path, monkeypatch):
root = tmp_path / "release"
root.mkdir()
(root / "agent.py").write_text("AGENT_VERSION = 14\n", encoding="utf-8")
release.publish(root, root / "agent.py")
real, calls = os.replace, []
def flaky(a, b, *x, **k):
calls.append(a)
if len(calls) == 2:
raise OSError(5, "сбой")
return real(a, b, *x, **k)
monkeypatch.setattr("os.replace", flaky)
release.migrate_layout(root)
assert (root / "jira" / "agent.py").exists() and not (root / "jira" / "manifest.json").exists()
assert (root / "manifest.json").exists() and not (root / "agent.py").exists()
monkeypatch.setattr("os.replace", real)
release.migrate_layout(root) # повторный старт доделывает
assert release.current(root / "jira")["version"] == 14
assert not (root / "manifest.json").exists() and not (root / "agent.py").exists()
def test_release_dirs_are_private(tmp_path):
root = tmp_path / "release"
root.mkdir(mode=0o755)
(root / "jira").mkdir(mode=0o755)
api.Hub(tmp_path / "hub.sqlite", backups_dir=tmp_path / "backups")
assert root.stat().st_mode & 0o777 == 0o700 and (root / "jira").stat().st_mode & 0o777 == 0o700
def test_publish_unknown_module(tmp_path, monkeypatch, capsys):
monkeypatch.setenv("JIRA_HUB_HOME", str(tmp_path / "home"))
src = tmp_path / "agent.py"
src.write_text("AGENT_VERSION = 1\n", encoding="utf-8")
assert cli.main(["agent", "publish", "--module", "nope", str(src)]) == 2
assert "неизвестный модуль nope; модули с агентами: jira" in capsys.readouterr().err
def test_publish_module_option(tmp_path, monkeypatch, capsys):
monkeypatch.setenv("JIRA_HUB_HOME", str(tmp_path / "home"))
src = tmp_path / "agent.py"
src.write_text("AGENT_VERSION = 1\n", encoding="utf-8")
assert cli.main(["agent", "publish", "--module", "jira", str(src)]) == 0
assert (tmp_path / "home" / "release" / "jira" / "manifest.json").exists()
def test_agents_endpoint(hub_server_two_modules):
c = hub_server_two_modules["client"]
agents = c.get("/agents")["agents"]
assert [a["module"] for a in agents] == ["jira", "stub"]
assert c.get("/agent")["module"] == "jira" and c.get("/agent", module="stub")["module"] == "stub"
with pytest.raises(HubError) as e:
c.get("/agent", module="nope")
assert e.value.status == 404 and e.value.error == "not_found"
def test_wiki_agent_token_after_upgrade(tmp_path):
"""Миграция v6: токен jira сохранён, токен wiki создан; строка agent_status есть у обоих."""
make_v6_db(tmp_path / "hub.sqlite")
conn = db.connect(tmp_path / "hub.sqlite")
try:
settings.ensure_defaults(conn)
agents = settings.tokens(conn)["agents"]
assert agents["jira"] == "T-agent" and agents["wiki"] and agents["wiki"] != agents["jira"]
names = {r["agent"] for r in conn.execute("SELECT agent FROM agent_status")}
assert names == {"jira", "wiki"}
finally:
conn.close()