import json import os from pathlib import Path import pytest from sdlc_hub.core import api, cli, paths, release, settings FIXTURE = Path(__file__).parent / "fixtures" / "config.example.json" def test_init_import_tokens(tmp_path, monkeypatch, capsys, open_db): monkeypatch.setenv("SDLC_HUB_HOME", str(tmp_path / "home")) assert cli.main(["init", "--config", str(FIXTURE)]) == 0 out = capsys.readouterr().out assert "transitions_map" in out and "агент jira: " in out assert cli.main(["init", "--config", str(FIXTURE)]) == 1 assert "уже инициализирован" in capsys.readouterr().err conn = open_db(paths.db_path()) assert [(x["label"], x["name"], x["board_id"]) for x in api.project_rows(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 api.project_rows(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 rep = json.loads(capsys.readouterr().out) assert rep["notes"] == 1 and rep["missing_keys"] == ["AURORA-1"] assert cli.main(["tokens"]) == 0 assert settings.tokens(conn)["client"] in capsys.readouterr().out def test_agent_publish(tmp_path, monkeypatch, capsys): monkeypatch.setenv("SDLC_HUB_HOME", str(tmp_path / "home")) src = tmp_path / "agent.py" src.write_text("AGENT_VERSION = 6\n", encoding="utf-8") assert cli.main(["agent", "publish", str(src)]) == 0 out = capsys.readouterr().out assert out.startswith("опубликована v6, sha256 ") and "…, 18 байт" in out assert (release.module_dir(paths.release_root(), "jira") / "manifest.json").exists() assert cli.main(["agent", "publish", str(src)]) == 1 assert "опубликована v6, новая должна быть больше" in capsys.readouterr().err def test_agent_publish_write_failure(tmp_path, monkeypatch, capsys): """Диск полон при записи манифеста: сообщение и код 1 без трейсбека, опубликованной остаётся v8 (M-1 #25).""" monkeypatch.setenv("SDLC_HUB_HOME", str(tmp_path / "home")) src = tmp_path / "agent.py" src.write_text("AGENT_VERSION = 8\n", encoding="utf-8") assert cli.main(["agent", "publish", str(src)]) == 0 capsys.readouterr() real = os.replace def fake(s, d, *a, **kw): if Path(d).name == "manifest.json": raise OSError(28, "No space left on device") return real(s, d, *a, **kw) monkeypatch.setattr(os, "replace", fake) src.write_text("AGENT_VERSION = 9\n", encoding="utf-8") assert cli.main(["agent", "publish", str(src)]) == 1 err = capsys.readouterr().err assert err.startswith("не удалось записать публикацию: ") and "Traceback" not in err monkeypatch.setattr(os, "replace", real) assert release.load(release.module_dir(paths.release_root(), "jira"))["version"] == 8 assert sorted(p.name for p in release.module_dir(paths.release_root(), "jira").iterdir()) == ["agent.py", "manifest.json"] def test_mcp_module_required(monkeypatch, capsys): from sdlc_hub.core import mcp as core_mcp from sdlc_hub.modules.jira import mcp as jira_mcp seen = [] monkeypatch.setattr(core_mcp, "PROJECT", None) # run() пишет глобал; monkeypatch вернёт прежнее значение monkeypatch.setattr(jira_mcp.mcp, "run", lambda transport="stdio": seen.append(core_mcp.PROJECT)) assert cli.main(["mcp", "--project", "AWPCA"]) == 2 err = capsys.readouterr().err assert "укажите модуль: sdlc-hub mcp <модуль>" in err and "jira, wiki" in err assert seen == [] assert cli.main(["mcp", "jira", "--project", "AWPCA"]) == 0 assert seen == ["AWPCA"] def test_mcp_unknown_module(monkeypatch, capsys): from sdlc_hub.modules.jira import mcp as jira_mcp seen = [] monkeypatch.setattr(jira_mcp.mcp, "run", lambda transport="stdio": seen.append(transport)) assert cli.main(["mcp", "nope"]) == 2 err = capsys.readouterr().err assert "неизвестный модуль nope" in err and "jira" in err assert seen == [] def test_agent_publish_migrates_old_layout(tmp_path, monkeypatch, capsys): """M-3: корневые agent.py/manifest.json прежней раскладки переезжают в release/jira/ до проверки «только вверх».""" monkeypatch.setenv("SDLC_HUB_HOME", str(tmp_path)) rel = tmp_path / "release" rel.mkdir() old = tmp_path / "old.py" old.write_text("AGENT_VERSION = 14\n", encoding="utf-8") release.publish(rel, old) # прежняя раскладка: release/agent.py + manifest.json new = tmp_path / "new.py" new.write_text("AGENT_VERSION = 13\n", encoding="utf-8") assert cli.main(["agent", "publish", str(new)]) == 1 # v13 < v14: «только вверх» видит прежнюю публикацию assert not (rel / "agent.py").exists() and not (rel / "manifest.json").exists() assert json.loads((rel / "jira" / "manifest.json").read_text(encoding="utf-8"))["version"] == 14 def _homes(monkeypatch, tmp_path, legacy=True, new=False): monkeypatch.delenv("SDLC_HUB_HOME", raising=False) monkeypatch.setenv("HOME", str(tmp_path)) share = tmp_path / ".local" / "share" if legacy: (share / "jira-hub").mkdir(parents=True) if new: (share / "sdlc-hub").mkdir(parents=True) return share @pytest.mark.parametrize("argv", [["serve"], ["tokens"], ["mcp", "jira"], ["agent", "publish", "x.py"]]) def test_legacy_home_refused(monkeypatch, tmp_path, capsys, argv): share = _homes(monkeypatch, tmp_path) assert cli.main(argv) == 2 err = capsys.readouterr().err assert "~/.local/share/jira-hub" in err and "scripts/cutover.sh" in err assert not (share / "sdlc-hub").exists() assert sorted(p.name for p in (share / "jira-hub").iterdir()) == [] def test_both_homes_warn(monkeypatch, tmp_path, capsys): _homes(monkeypatch, tmp_path, new=True) cli.main(["tokens"]) assert "прежний каталог ~/.local/share/jira-hub можно удалить" in capsys.readouterr().err def test_env_home_skips_legacy_check(monkeypatch, tmp_path, capsys): _homes(monkeypatch, tmp_path) monkeypatch.setenv("SDLC_HUB_HOME", str(tmp_path / "x")) assert cli.main(["tokens"]) == 0