from __future__ import annotations import importlib.util import os import tempfile import unittest from io import StringIO from pathlib import Path from unittest import mock REPO_ROOT = Path(__file__).resolve().parents[1] CHECK_PY = REPO_ROOT / "skills" / "deployer" / "scripts" / "deploy" / "check.py" LIB_PY = REPO_ROOT / "skills" / "deployer" / "scripts" / "deploy" / "lib.py" lib_spec = importlib.util.spec_from_file_location("deployer_lib", LIB_PY) assert lib_spec is not None and lib_spec.loader is not None deployer_lib = importlib.util.module_from_spec(lib_spec) lib_spec.loader.exec_module(deployer_lib) check_spec = importlib.util.spec_from_file_location("deployer_check", CHECK_PY) assert check_spec is not None and check_spec.loader is not None deployer_check = importlib.util.module_from_spec(check_spec) check_spec.loader.exec_module(deployer_check) def run_check(project: Path, hosts: set[str] | None = None) -> tuple[int, str]: stdout = StringIO() stderr = StringIO() deployer_lib.PROJECT_ROOT = None deployer_lib._SSH_HOSTS = None deployer_check.lib.PROJECT_ROOT = None deployer_check.lib._SSH_HOSTS = None patched_hosts = hosts if hosts is not None else set() with mock.patch("sys.stdout", stdout), mock.patch("sys.stderr", stderr): with mock.patch.object(deployer_check.lib, "ssh_config_hosts", return_value=patched_hosts): with mock.patch.dict(os.environ, {}, clear=False): os.environ.pop("DEPLOYER_ROOT", None) code = deployer_check.main(["--project", str(project)]) return code, stdout.getvalue() + stderr.getvalue() class DeployerCheckTests(unittest.TestCase): def tearDown(self) -> None: deployer_lib.PROJECT_ROOT = None deployer_lib._SSH_HOSTS = None deployer_check.lib.PROJECT_ROOT = None deployer_check.lib._SSH_HOSTS = None def test_skill_documents_init_and_check_script(self) -> None: skill = (REPO_ROOT / "skills" / "deployer" / "SKILL.md").read_text( encoding="utf-8" ) self.assertIn("## 初始化", skill) self.assertIn("scripts/deploy/check.py", skill) self.assertIn("不要写假 node", skill) def test_missing_layout_fails(self) -> None: with tempfile.TemporaryDirectory() as temp: project = Path(temp) code, text = run_check(project) self.assertEqual(code, 1, text) self.assertIn("[FAIL] 1. 部署根存在", text) self.assertIn("test/compose.yaml", text) def test_compose_without_node_is_partial_failure(self) -> None: with tempfile.TemporaryDirectory() as temp: project = Path(temp) env = project / ".pouch" / "deployer" / "test" env.mkdir(parents=True) (env / "compose.yaml").write_text("services:\n web:\n image: nginx\n", encoding="utf-8") code, text = run_check(project, hosts={"my-vps"}) self.assertEqual(code, 1, text) self.assertIn("[PASS] 1. 部署根存在", text) self.assertIn("[PASS] 4. 至少有一个 compose.yaml", text) self.assertIn("[FAIL] 5. 每个服务能解析 node", text) self.assertIn("MISSING node", text) def test_node_missing_from_ssh_config_fails(self) -> None: with tempfile.TemporaryDirectory() as temp: project = Path(temp) root = project / ".pouch" / "deployer" env = root / "test" env.mkdir(parents=True) (root / "_config.yaml").write_text("node: my-vps\n", encoding="utf-8") (env / "compose.yaml").write_text("services:\n web:\n image: nginx\n", encoding="utf-8") code, text = run_check(project, hosts=set()) self.assertEqual(code, 1, text) self.assertIn("[PASS] 5. 每个服务能解析 node", text) self.assertIn("[FAIL] 6. node 出现在 SSH config", text) self.assertIn("my-vps NOT in ~/.ssh/config", text) def test_ready_when_node_and_compose_present(self) -> None: with tempfile.TemporaryDirectory() as temp: project = Path(temp) root = project / ".pouch" / "deployer" env = root / "test" env.mkdir(parents=True) (root / "_config.yaml").write_text("node: my-vps\nbase_path: /opt/app\n", encoding="utf-8") (env / "compose.yaml").write_text("services:\n web:\n image: nginx\n", encoding="utf-8") code, text = run_check(project, hosts={"my-vps"}) self.assertEqual(code, 0, text) self.assertIn("RESULT: PASSED", text) self.assertIn("[PASS] 7. list 可发现服务", text) def test_argocd_only_passes_compose_as_skip(self) -> None: with tempfile.TemporaryDirectory() as temp: project = Path(temp) root = project / ".pouch" / "deployer" root.mkdir(parents=True) (root / "argocd.yaml").write_text( "repo: git@git.example.com:org/infra-gitops.git\n", encoding="utf-8", ) code, text = run_check(project) self.assertEqual(code, 0, text) self.assertIn("[PASS] 3. Argo CD 指针", text) self.assertIn("[SKIP] 4. 至少有一个 compose.yaml", text) if __name__ == "__main__": unittest.main()