import json import subprocess import sys import tempfile import tomllib import unittest from pathlib import Path SCRIPTS_DIR = Path(__file__).resolve().parent.parent / "scripts" if str(SCRIPTS_DIR) not in sys.path: sys.path.insert(0, str(SCRIPTS_DIR)) import agent_install # noqa: E402 def make_project(root: Path) -> None: (root / "pyproject.toml").write_text('[project]\nname = "test"\n', encoding="utf-8") (root / "uv.lock").write_text("version = 1\n", encoding="utf-8") (root / "config.example.json").write_text(json.dumps({"env": "nb-mcp", "collections": {}}), encoding="utf-8") def fake_which(name: str) -> str | None: return { "git": "/usr/bin/git", "uv": "/opt/bin/uv", "nb": "/opt/bin/nb", }.get(name) class AgentInstallTests(unittest.TestCase): def test_inspect_reports_required_tools_and_config_as_optional_before_prepare(self) -> None: with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) make_project(root) result = agent_install.inspect_project(root, which=fake_which, python_version=(3, 12, 4)) self.assertTrue(result["ok"]) checks = {check["name"]: check for check in result["checks"]} self.assertTrue(checks["uv"]["ok"]) self.assertFalse(checks["config"]["ok"]) self.assertFalse(checks["config"]["required"]) def test_prepare_is_idempotent_and_never_overwrites_existing_config(self) -> None: calls = [] def runner(command, **kwargs): calls.append((command, kwargs)) return subprocess.CompletedProcess(command, 0, stdout="", stderr="") with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) make_project(root) first = agent_install.prepare_project(root, runner=runner, which=fake_which, python_version=(3, 12, 4)) config_path = root / "config.json" config_path.write_text(json.dumps({"env": "custom-env"}), encoding="utf-8") second = agent_install.prepare_project(root, runner=runner, which=fake_which, python_version=(3, 12, 4)) preserved = json.loads(config_path.read_text(encoding="utf-8")) self.assertTrue(first["configCreated"]) self.assertFalse(second["configCreated"]) self.assertEqual(preserved["env"], "custom-env") self.assertEqual(calls[0][0], ["/opt/bin/uv", "sync", "--locked", "--python", "3.12"]) self.assertEqual(len(calls), 2) def test_set_env_backs_up_config_before_changing_it(self) -> None: with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) make_project(root) original = {"env": "nb-mcp", "collections": {"customers": {"resource": "customers"}}} (root / "config.json").write_text(json.dumps(original), encoding="utf-8") result = agent_install.set_config_env(root, "nb-mcp-acme") changed = json.loads((root / "config.json").read_text(encoding="utf-8")) backup = json.loads((root / "config.json.pre-agent-install.bak").read_text(encoding="utf-8")) self.assertTrue(result["changed"]) self.assertEqual(changed["env"], "nb-mcp-acme") self.assertEqual(backup, original) def test_client_config_uses_absolute_paths_and_is_read_only_by_default(self) -> None: with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) config = agent_install.render_client_config(root, which=fake_which) writable = agent_install.render_client_config(root, enable_writes=True, which=fake_which) server = config["mcpServers"]["nocobase-crm"] self.assertEqual(server["command"], "/opt/bin/uv") self.assertEqual(server["env"]["NOCOBASE_MCP_NB_BIN"], "/opt/bin/nb") self.assertIn(str(root.resolve()), server["args"]) self.assertNotIn("--enable-writes", server["args"]) self.assertIn("--enable-writes", writable["mcpServers"]["nocobase-crm"]["args"]) def test_doctor_checks_local_install_without_reading_nb_secrets(self) -> None: commands = [] def runner(command, **kwargs): commands.append(command) return subprocess.CompletedProcess(command, 0, stdout="", stderr="") with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) make_project(root) (root / "config.json").write_text(json.dumps({"env": "nb-mcp"}), encoding="utf-8") result = agent_install.doctor_project( root, runner=runner, which=fake_which, python_version=(3, 12, 4), ) self.assertTrue(result["ok"]) self.assertEqual(commands[0], ["/opt/bin/uv", "lock", "--check"]) self.assertNotIn("--show-secrets", json.dumps(result)) self.assertTrue(any("crm_health" in step for step in result["remainingVerification"])) def test_release_manifest_version_matches_package_version(self) -> None: root = Path(__file__).resolve().parent.parent manifest = json.loads((root / "install-manifest.json").read_text(encoding="utf-8")) package = tomllib.loads((root / "pyproject.toml").read_text(encoding="utf-8")) version = package["project"]["version"] self.assertEqual(manifest["version"], version) self.assertEqual(manifest["distribution"]["releaseRef"], f"v{version}") self.assertTrue(manifest["securityDefaults"]["readOnly"]) self.assertFalse(manifest["securityDefaults"]["uploads"]) if __name__ == "__main__": unittest.main()