import hashlib from pathlib import Path import os import subprocess import tempfile import unittest SCRIPT = Path(__file__).resolve().parents[1] / "install.sh" class InstallerTests(unittest.TestCase): def run_installer( self, home: Path, model: Path, *arguments: str, install_root_override: str | None = None, ) -> subprocess.CompletedProcess[str]: environment = { **os.environ, "HOME": str(home), "XDG_CONFIG_HOME": str(home / ".config"), "PI_DICTATE_INSTALL_ROOT": install_root_override or str( home / ".local/share/pi-dictate" ), } command = [str(SCRIPT), *arguments, "--dry-run"] if not any(action in arguments for action in ("uninstall", "check")): command.extend(("--model-source", str(model), "--model-sha256", hashlib.sha256(model.read_bytes()).hexdigest())) return subprocess.run( command, env=environment, capture_output=True, text=True, check=False, ) def test_dry_run_seeds_dictionary_and_installs_owned_unit_copies(self) -> None: with tempfile.TemporaryDirectory() as directory: home = Path(directory) model = home / "model.nemo" model.write_bytes(b"model") result = self.run_installer(home, model) self.assertEqual(result.returncode, 0, result.stderr) self.assertIn("coding-terms.txt", result.stdout) self.assertIn("pi-dictate.socket", result.stdout) self.assertIn("install -m 0644", result.stdout) self.assertNotIn("ln -s", result.stdout) self.assertIn(".nemo.install.", result.stdout) self.assertIn("mv -f", result.stdout) self.assertIn("uv venv --relocatable", result.stdout) self.assertIn("uv sync --frozen --no-dev --no-editable", result.stdout) self.assertIn("--reinstall-package pi-dictate-service", result.stdout) self.assertIn("Would install pi-dictate", result.stdout) self.assertFalse((home / ".local/share/pi-dictate").exists()) def test_ignores_dangerous_install_root_environment_override(self) -> None: with tempfile.TemporaryDirectory() as directory: home = Path(directory) model = home / "model.nemo" model.write_bytes(b"model") result = self.run_installer( home, model, "uninstall", install_root_override="/", ) self.assertEqual(result.returncode, 0, result.stderr) self.assertIn(str(home / ".local/share/pi-dictate"), result.stdout) self.assertNotIn("rm -rf / ", result.stdout) def test_uninstall_refuses_to_disable_an_unmanaged_same_named_unit(self) -> None: with tempfile.TemporaryDirectory() as directory: home = Path(directory) model = home / "model.nemo" model.write_bytes(b"model") unit = home / ".config/systemd/user/pi-dictate.socket" unit.parent.mkdir(parents=True) unit.write_text("[Socket]\nListenStream=/tmp/unrelated.sock\n", encoding="utf-8") result = self.run_installer(home, model, "uninstall") self.assertEqual(result.returncode, 1) self.assertIn("unmanaged systemd unit", result.stderr) self.assertNotIn("systemctl --user disable", result.stdout) self.assertEqual( unit.read_text(encoding="utf-8"), "[Socket]\nListenStream=/tmp/unrelated.sock\n", ) def test_uninstall_refuses_a_modified_managed_unit(self) -> None: with tempfile.TemporaryDirectory() as directory: home = Path(directory) model = home / "model.nemo" model.write_bytes(b"model") unit = home / ".config/systemd/user/pi-dictate.socket" unit.parent.mkdir(parents=True) unit.write_text( "# Managed by pi-dictate install.sh\n[Socket]\nListenStream=/tmp/tampered.sock\n", encoding="utf-8", ) result = self.run_installer(home, model, "uninstall") self.assertEqual(result.returncode, 1) self.assertIn("unmanaged systemd unit", result.stderr) self.assertNotIn("systemctl --user disable", result.stdout) self.assertTrue(unit.exists()) def test_reinstall_preserves_an_existing_host_dictionary(self) -> None: with tempfile.TemporaryDirectory() as directory: home = Path(directory) model = home / "model.nemo" model.write_bytes(b"model") dictionary = home / ".local/share/pi-dictate/config/coding-terms.txt" dictionary.parent.mkdir(parents=True) dictionary.write_text("my private term\n", encoding="utf-8") result = self.run_installer(home, model) self.assertEqual(result.returncode, 0, result.stderr) self.assertIn("Keeping existing coding dictionary", result.stdout) self.assertEqual(dictionary.read_text(encoding="utf-8"), "my private term\n") if __name__ == "__main__": unittest.main()