diff --git a/solstone/think/user_config.py b/solstone/think/user_config.py index bf14c43fe..e6fb25645 100644 --- a/solstone/think/user_config.py +++ b/solstone/think/user_config.py @@ -15,12 +15,21 @@ from pathlib import Path from typing import Any +def _home() -> Path: + expanded = os.path.expanduser("~") + if expanded.startswith("~"): + raise RuntimeError( + f"Could not determine home directory: expanded home {expanded!r} remains tilde-leading" + ) + return Path(expanded) + + def default_journal() -> str: - return str(Path.home() / "journal") + return str(_home() / "journal") def config_path() -> Path: - return Path.home() / ".config" / "solstone" / "config.toml" + return _home() / ".config" / "solstone" / "config.toml" def read_user_config() -> dict[str, str]: diff --git a/tests/test_config_cli.py b/tests/test_config_cli.py index 0c241e628..7abda7ff4 100644 --- a/tests/test_config_cli.py +++ b/tests/test_config_cli.py @@ -18,6 +18,7 @@ def home_root(monkeypatch, tmp_path): home = tmp_path / "home" home.mkdir() monkeypatch.setattr(Path, "home", classmethod(lambda cls: home)) + monkeypatch.setenv("HOME", str(home)) return home diff --git a/tests/test_install_guard.py b/tests/test_install_guard.py index db6c95997..f9d995c60 100644 --- a/tests/test_install_guard.py +++ b/tests/test_install_guard.py @@ -140,6 +140,7 @@ def home_root(monkeypatch, tmp_path): home = tmp_path / "home" home.mkdir() monkeypatch.setattr(Path, "home", classmethod(lambda cls: home)) + monkeypatch.setenv("HOME", str(home)) return home diff --git a/tests/test_setup.py b/tests/test_setup.py index 2cc4cfdd8..989f0ccbd 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -43,6 +43,7 @@ def patch_home(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path: home = tmp_path / "home" home.mkdir() monkeypatch.setattr(Path, "home", classmethod(lambda cls: home)) + monkeypatch.setenv("HOME", str(home)) return home diff --git a/tests/test_think_utils.py b/tests/test_think_utils.py index 1af47bb0c..3f159aec2 100644 --- a/tests/test_think_utils.py +++ b/tests/test_think_utils.py @@ -1126,6 +1126,7 @@ class TestJournalResolution: def test_get_journal_info_source_tree_fallback(self, monkeypatch, tmp_path): monkeypatch.delenv("SOLSTONE_JOURNAL", raising=False) monkeypatch.setattr(Path, "home", classmethod(lambda cls: tmp_path)) + monkeypatch.setenv("HOME", str(tmp_path)) path, source = get_journal_info() @@ -1140,6 +1141,7 @@ class TestJournalResolution: monkeypatch.delenv("SOLSTONE_JOURNAL", raising=False) monkeypatch.setattr(utils, "get_project_root", lambda: str(tmp_path)) monkeypatch.setattr(Path, "home", classmethod(lambda cls: tmp_path)) + monkeypatch.setenv("HOME", str(tmp_path)) path, source = get_journal_info() @@ -1178,6 +1180,7 @@ class TestGetJournalInfoConfigBranch: ): monkeypatch.delenv("SOLSTONE_JOURNAL", raising=False) monkeypatch.setattr(Path, "home", classmethod(lambda cls: tmp_path)) + monkeypatch.setenv("HOME", str(tmp_path)) self.write_config(tmp_path, 'journal = "/tmp/from-config"\n') path, source = get_journal_info() @@ -1200,6 +1203,7 @@ class TestGetJournalInfoConfigBranch: ): monkeypatch.setenv("SOLSTONE_JOURNAL", "") monkeypatch.setattr(Path, "home", classmethod(lambda cls: tmp_path)) + monkeypatch.setenv("HOME", str(tmp_path)) self.write_config(tmp_path, 'journal = "/tmp/from-config"\n') path, source = get_journal_info() @@ -1210,6 +1214,7 @@ class TestGetJournalInfoConfigBranch: def test_empty_journal_key_in_config_falls_through(self, monkeypatch, tmp_path): monkeypatch.delenv("SOLSTONE_JOURNAL", raising=False) monkeypatch.setattr(Path, "home", classmethod(lambda cls: tmp_path)) + monkeypatch.setenv("HOME", str(tmp_path)) self.write_config(tmp_path, 'journal = ""\n') _path, source = get_journal_info() @@ -1219,6 +1224,7 @@ class TestGetJournalInfoConfigBranch: def test_whitespace_only_journal_key_falls_through(self, monkeypatch, tmp_path): monkeypatch.delenv("SOLSTONE_JOURNAL", raising=False) monkeypatch.setattr(Path, "home", classmethod(lambda cls: tmp_path)) + monkeypatch.setenv("HOME", str(tmp_path)) self.write_config(tmp_path, 'journal = " "\n') _path, source = get_journal_info() @@ -1228,6 +1234,7 @@ class TestGetJournalInfoConfigBranch: def test_config_branch_wins_over_source_branch(self, monkeypatch, tmp_path): monkeypatch.delenv("SOLSTONE_JOURNAL", raising=False) monkeypatch.setattr(Path, "home", classmethod(lambda cls: tmp_path)) + monkeypatch.setenv("HOME", str(tmp_path)) self.write_config(tmp_path, 'journal = "/tmp/from-config"\n') path, source = get_journal_info() diff --git a/tests/test_user_config.py b/tests/test_user_config.py index 914baf312..da8ce50cb 100644 --- a/tests/test_user_config.py +++ b/tests/test_user_config.py @@ -17,10 +17,35 @@ from solstone.think.user_config import ( @pytest.fixture def fake_home(tmp_path, monkeypatch): - monkeypatch.setattr(Path, "home", classmethod(lambda cls: tmp_path)) + monkeypatch.setenv("HOME", str(tmp_path)) return tmp_path +@pytest.mark.parametrize("home_value", ["~", "~/", "~/x", "~~", "~x"]) +def test_home_rejects_tilde_leading_expanded_values(monkeypatch, home_value): + monkeypatch.setenv("HOME", home_value) + + for resolve in (default_journal, config_path): + with pytest.raises(RuntimeError, match="tilde-leading"): + resolve() + + +@pytest.mark.parametrize( + ("home_value", "expected_journal", "expected_config"), + [ + ("./~", "~/journal", Path("~/.config/solstone/config.toml")), + ("x~", "x~/journal", Path("x~/.config/solstone/config.toml")), + ], +) +def test_home_allows_non_tilde_leading_edges( + monkeypatch, home_value, expected_journal, expected_config +): + monkeypatch.setenv("HOME", home_value) + + assert default_journal() == expected_journal + assert config_path() == expected_config + + def test_default_journal_returns_home_journal(fake_home): assert default_journal() == str(fake_home / "journal")