tmux observer
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Capture installer PATH in generated systemd unit file

cmd_install_service() now emits Environment=PATH=... in the generated
systemd unit, with the venv bin dir prepended and duplicates removed.
This ensures the service can find solstone-tmux and sol regardless of
the systemd user environment.

Files changed:
- src/solstone_tmux/cli.py — add import os, build service_path, emit Environment=PATH=
- contrib/solstone-tmux.service — add comment noting PATH is injected at install time
- tests/test_cli.py — new test file covering PATH injection, dedup, fallback, empty components

+141
+1
contrib/solstone-tmux.service
··· 4 4 5 5 [Service] 6 6 Type=simple 7 + # Environment=PATH=... (injected at install time by solstone-tmux install-service) 7 8 ExecStart=%h/.local/bin/solstone-tmux run 8 9 Restart=on-failure 9 10 RestartSec=5
+6
src/solstone_tmux/cli.py
··· 16 16 import asyncio 17 17 import json 18 18 import logging 19 + import os 19 20 import shutil 20 21 import socket 21 22 import subprocess ··· 142 143 print("Error: solstone-tmux not found on PATH", file=sys.stderr) 143 144 print("Install with: pipx install solstone-tmux", file=sys.stderr) 144 145 return 1 146 + venv_bin = str(Path(binary).resolve().parent) 147 + raw_path = os.environ.get("PATH", "/usr/local/bin:/usr/bin:/bin") 148 + path_parts = [venv_bin] + [p for p in raw_path.split(":") if p] 149 + service_path = ":".join(dict.fromkeys(path_parts)) 145 150 146 151 unit_dir = Path.home() / ".config" / "systemd" / "user" 147 152 unit_dir.mkdir(parents=True, exist_ok=True) ··· 154 159 155 160 [Service] 156 161 Type=simple 162 + Environment=PATH={service_path} 157 163 ExecStart={binary} run 158 164 Restart=on-failure 159 165 RestartSec=5
+134
tests/test_cli.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + import argparse 5 + from unittest.mock import patch 6 + 7 + from solstone_tmux.cli import cmd_install_service 8 + 9 + 10 + class TestInstallServicePath: 11 + """Tests for PATH capture in cmd_install_service.""" 12 + 13 + def _run_install(self, tmp_path, monkeypatch, binary_path, env_path=None): 14 + """Helper: run cmd_install_service with mocked deps, return unit file content.""" 15 + monkeypatch.setattr( 16 + "solstone_tmux.cli.shutil.which", lambda _name: str(binary_path) 17 + ) 18 + monkeypatch.setattr( 19 + "solstone_tmux.cli.Path.home", staticmethod(lambda: tmp_path) 20 + ) 21 + 22 + if env_path is not None: 23 + monkeypatch.setenv("PATH", env_path) 24 + else: 25 + monkeypatch.delenv("PATH", raising=False) 26 + 27 + with patch("solstone_tmux.cli.subprocess.run"): 28 + cmd_install_service(argparse.Namespace()) 29 + 30 + unit_path = tmp_path / ".config" / "systemd" / "user" / "solstone-tmux.service" 31 + return unit_path.read_text() 32 + 33 + def test_path_contains_environment_line(self, tmp_path, monkeypatch): 34 + """Generated unit file contains Environment=PATH= in [Service].""" 35 + binary = tmp_path / "venv" / "bin" / "solstone-tmux" 36 + binary.parent.mkdir(parents=True) 37 + binary.touch() 38 + 39 + content = self._run_install(tmp_path, monkeypatch, binary, "/usr/bin:/bin") 40 + 41 + assert "Environment=PATH=" in content 42 + 43 + def test_venv_bin_is_first(self, tmp_path, monkeypatch): 44 + """Venv bin dir is the first entry in the PATH.""" 45 + binary = tmp_path / "venv" / "bin" / "solstone-tmux" 46 + binary.parent.mkdir(parents=True) 47 + binary.touch() 48 + 49 + content = self._run_install(tmp_path, monkeypatch, binary, "/usr/bin:/bin") 50 + 51 + for line in content.splitlines(): 52 + if line.startswith("Environment=PATH="): 53 + path_value = line.split("=", 2)[2] 54 + parts = path_value.split(":") 55 + assert parts[0] == str(binary.resolve().parent) 56 + break 57 + else: 58 + raise AssertionError("No Environment=PATH= line found") 59 + 60 + def test_deduplication(self, tmp_path, monkeypatch): 61 + """Duplicate PATH entries are removed, first occurrence wins.""" 62 + binary = tmp_path / "venv" / "bin" / "solstone-tmux" 63 + binary.parent.mkdir(parents=True) 64 + binary.touch() 65 + venv_bin = str(binary.resolve().parent) 66 + env_path = f"{venv_bin}:/usr/bin:/usr/bin:/bin" 67 + 68 + content = self._run_install(tmp_path, monkeypatch, binary, env_path) 69 + 70 + for line in content.splitlines(): 71 + if line.startswith("Environment=PATH="): 72 + path_value = line.split("=", 2)[2] 73 + parts = path_value.split(":") 74 + assert parts == list(dict.fromkeys(parts)), "PATH contains duplicates" 75 + assert parts.count(venv_bin) == 1 76 + assert parts.count("/usr/bin") == 1 77 + break 78 + else: 79 + raise AssertionError("No Environment=PATH= line found") 80 + 81 + def test_fallback_when_no_path_env(self, tmp_path, monkeypatch): 82 + """Falls back to /usr/local/bin:/usr/bin:/bin when PATH not set.""" 83 + binary = tmp_path / "venv" / "bin" / "solstone-tmux" 84 + binary.parent.mkdir(parents=True) 85 + binary.touch() 86 + 87 + content = self._run_install(tmp_path, monkeypatch, binary, env_path=None) 88 + 89 + for line in content.splitlines(): 90 + if line.startswith("Environment=PATH="): 91 + path_value = line.split("=", 2)[2] 92 + venv_bin = str(binary.resolve().parent) 93 + expected_start = venv_bin + ":/usr/local/bin:/usr/bin:/bin" 94 + assert path_value == expected_start 95 + break 96 + else: 97 + raise AssertionError("No Environment=PATH= line found") 98 + 99 + def test_empty_path_components_filtered(self, tmp_path, monkeypatch): 100 + """Empty PATH components (from double colons) are filtered out.""" 101 + binary = tmp_path / "venv" / "bin" / "solstone-tmux" 102 + binary.parent.mkdir(parents=True) 103 + binary.touch() 104 + 105 + content = self._run_install(tmp_path, monkeypatch, binary, "/usr/bin::/bin:") 106 + 107 + for line in content.splitlines(): 108 + if line.startswith("Environment=PATH="): 109 + path_value = line.split("=", 2)[2] 110 + parts = path_value.split(":") 111 + assert "" not in parts, "Empty component in PATH" 112 + assert not path_value.startswith(":"), "PATH starts with colon" 113 + assert not path_value.endswith(":"), "PATH ends with colon" 114 + assert "::" not in path_value, "Double colon in PATH" 115 + break 116 + else: 117 + raise AssertionError("No Environment=PATH= line found") 118 + 119 + def test_no_trailing_or_leading_colons(self, tmp_path, monkeypatch): 120 + """Generated PATH has no leading or trailing colons.""" 121 + binary = tmp_path / "venv" / "bin" / "solstone-tmux" 122 + binary.parent.mkdir(parents=True) 123 + binary.touch() 124 + 125 + content = self._run_install(tmp_path, monkeypatch, binary, "/usr/bin:/bin") 126 + 127 + for line in content.splitlines(): 128 + if line.startswith("Environment=PATH="): 129 + path_value = line.split("=", 2)[2] 130 + assert not path_value.startswith(":"), "PATH starts with colon" 131 + assert not path_value.endswith(":"), "PATH ends with colon" 132 + break 133 + else: 134 + raise AssertionError("No Environment=PATH= line found")