"""Installer tests against a fake user home (HARBOR_INSTALL_HOME) - real host configs are never touched.""" import json import os import shutil import tomllib import unittest import yaml from helpers import make_home, temp_dir from harbor import install CODEX_CONFIG = '''model = "gpt" notify = ["x.exe", "turn-ended"] [features] [mcp_servers.node_repl] command = "node" [tui.model_availability_nux] seen = true ''' DSH_PATCH = '''# Your patch layer - id: ui-chat name: "@deepseek-ai/dsh-client-ui-chat" config: transcriptView: standard - id: tools config: mode: !!js process.env.DSH_TOOLS_MODE ''' class Installer(unittest.TestCase): def setUp(self): self.dir = temp_dir() self.home = self.dir / "user" (self.home / ".codex").mkdir(parents=True) (self.home / ".codex" / "config.toml").write_text(CODEX_CONFIG, encoding="utf-8") (self.home / ".codex" / "AGENTS.md").write_text("# My rules\n\nBe brief.\n", encoding="utf-8") (self.home / ".config" / "opencode").mkdir(parents=True) (self.home / ".config" / "opencode" / "opencode.jsonc").write_text('{\n "$schema": "https://opencode.ai/config.json"\n}', encoding="utf-8") (self.home / ".dsh" / "profiles" / "desktop").mkdir(parents=True) (self.home / ".dsh" / "profiles" / "desktop" / "cordis.patch.yml").write_text(DSH_PATCH, encoding="utf-8") self.harbor_home = self.dir / "harbor-home" make_home(self.harbor_home) self.saved = {k: os.environ.get(k) for k in ("HARBOR_INSTALL_HOME", "HARBOR_HOME", "HARBOR_PROJECT", "APPDATA")} os.environ.update(HARBOR_INSTALL_HOME=str(self.home), HARBOR_HOME=str(self.harbor_home), APPDATA=str(self.dir / "appdata")) os.environ.pop("HARBOR_PROJECT", None) self.cwd = os.getcwd() os.chdir(self.dir) # resolve projects from the test folder, not from wherever the suite runs def tearDown(self): os.chdir(self.cwd) for k, v in self.saved.items(): if v is None: os.environ.pop(k, None) else: os.environ[k] = v shutil.rmtree(self.dir, ignore_errors=True) def read(self, *parts): return (self.home.joinpath(*parts)).read_text(encoding="utf-8") def test_codex_install_is_idempotent_and_reversible(self): install.install("codex", python="py -3.12") config = tomllib.loads(self.read(".codex", "config.toml")) server = config["mcp_servers"]["harbor"] self.assertEqual(server["env"]["HARBOR_AGENT"], "Codex") self.assertEqual(server["args"][-3:], ["--as", "Codex", "mcp"]) self.assertEqual(config["mcp_servers"]["node_repl"]["command"], "node") # untouched self.assertTrue(config["tui"]["model_availability_nux"]["seen"]) hooks = json.loads(self.read(".codex", "hooks.json"))["hooks"] self.assertEqual(sorted(hooks), ["PreToolUse", "SessionStart", "Stop", "UserPromptSubmit"]) self.assertIn("hook pre-tool --host codex", hooks["PreToolUse"][0]["hooks"][0]["command"]) self.assertIn("Be brief.", self.read(".codex", "AGENTS.md")) self.assertIn("you are **Codex**", self.read(".codex", "AGENTS.md")) once = self.read(".codex", "config.toml") report = install.install("codex", python="py -3.12") self.assertEqual(self.read(".codex", "config.toml"), once) self.assertTrue(all(line.startswith("unchanged") for line in report if line.startswith(("unchanged", "update", "create")))) install.install("codex", remove=True) self.assertEqual(self.read(".codex", "config.toml"), CODEX_CONFIG) self.assertEqual(self.read(".codex", "AGENTS.md"), "# My rules\n\nBe brief.\n") self.assertFalse((self.home / ".codex" / "hooks.json").exists()) backups = list((self.harbor_home / "backups" / "integrations").rglob("config.toml")) self.assertTrue(backups) def test_codex_keeps_other_hooks(self): (self.home / ".codex" / "hooks.json").write_text(json.dumps( {"hooks": {"Stop": [{"hooks": [{"type": "command", "command": "mine.exe"}]}]}}), encoding="utf-8") install.install("codex", python="py -3.12") stop = json.loads(self.read(".codex", "hooks.json"))["hooks"]["Stop"] self.assertEqual(len(stop), 2) install.install("codex", remove=True) self.assertEqual(json.loads(self.read(".codex", "hooks.json")), {"hooks": {"Stop": [{"hooks": [{"type": "command", "command": "mine.exe"}]}]}}) def test_opencode(self): install.install("opencode", python="py -3.12") config = json.loads(self.read(".config", "opencode", "opencode.jsonc")) self.assertEqual(config["$schema"], "https://opencode.ai/config.json") self.assertEqual(config["mcp"]["harbor"]["environment"], {"HARBOR_AGENT": "OpenCode", "HARBOR_HOST": "opencode"}) plugin = self.read(".config", "opencode", "plugin", "harbor.js") self.assertIn('const AGENT = "OpenCode";', plugin) self.assertIn('const COMMAND = ["py", "-3.12", "-B",', plugin) self.assertIn("export const HarborPlugin", plugin) install.install("opencode", remove=True) self.assertEqual(json.loads(self.read(".config", "opencode", "opencode.jsonc")), {"$schema": "https://opencode.ai/config.json"}) self.assertFalse((self.home / ".config" / "opencode" / "plugin" / "harbor.js").exists()) def test_deepseek(self): install.install("deepseek", python="py -3.12") patch = self.read(".dsh", "profiles", "desktop", "cordis.patch.yml") self.assertTrue(patch.startswith(DSH_PATCH.rstrip("\n"))) rows = install._TolerantLoader.load(patch) inserted = rows[-1]["insert"] self.assertEqual([r["id"] for r in inserted], ["mcp-harbor", "hooks-harbor"]) self.assertEqual(inserted[0]["config"]["env"], {"HARBOR_AGENT": "DeepSeek", "HARBOR_HOST": "deepseek"}) hooks = json.loads((self.harbor_home / "integrations" / "deepseek" / "hooks.json").read_text(encoding="utf-8")) self.assertIn("--host deepseek", hooks["hooks"]["Stop"][0]["hooks"][0]["command"]) install.install("deepseek", remove=True) self.assertEqual(self.read(".dsh", "profiles", "desktop", "cordis.patch.yml"), DSH_PATCH) def test_kimi_plugin_is_bound_to_a_project(self): install.install("kimi", project="test", python="py -3.12", register=False) folder = self.harbor_home / "integrations" / "kimi" / "harbor-test" manifest = json.loads((folder / "kimi.plugin.json").read_text(encoding="utf-8")) self.assertEqual(manifest["mcpServers"]["harbor"]["env"], {"HARBOR_AGENT": "Kimi", "HARBOR_PROJECT": "test", "HARBOR_HOST": "kimi"}) self.assertEqual([h["event"] for h in manifest["hooks"]], ["UserPromptSubmit", "PreToolUse", "Stop"]) self.assertTrue(all("--project test" in h["command"] for h in manifest["hooks"])) self.assertTrue((folder / "skills" / "harbor-test" / "SKILL.md").exists()) def test_refusals(self): with self.assertRaises(install.InstallError): install.install("codex", agent="Nobody") (self.home / ".config" / "opencode" / "opencode.jsonc").write_text('{ // mine\n}', encoding="utf-8") with self.assertRaises(install.InstallError): install.install("opencode") (self.home / ".codex" / "config.toml").write_text(CODEX_CONFIG + "\n[mcp_servers.harbor]\ncommand='x'\n", encoding="utf-8") with self.assertRaises(install.InstallError): install.install("codex") def test_dry_run_writes_nothing(self): report = install.install("deepseek", dry_run=True, python="py -3.12") self.assertTrue(any(line.startswith("update") for line in report)) self.assertEqual(self.read(".dsh", "profiles", "desktop", "cordis.patch.yml"), DSH_PATCH) if __name__ == "__main__": unittest.main()