personal memory agent
1# SPDX-License-Identifier: AGPL-3.0-only
2# Copyright (c) 2026 sol pbc
3
4from __future__ import annotations
5
6from pathlib import Path
7
8import pytest
9
10from solstone.think.user_config import (
11 config_path,
12 default_journal,
13 read_user_config,
14 write_user_config,
15)
16
17
18@pytest.fixture
19def fake_home(tmp_path, monkeypatch):
20 monkeypatch.setenv("HOME", str(tmp_path))
21 return tmp_path
22
23
24@pytest.mark.parametrize("home_value", ["~", "~/", "~/x", "~~", "~x"])
25def test_home_rejects_tilde_leading_expanded_values(monkeypatch, home_value):
26 monkeypatch.setenv("HOME", home_value)
27
28 for resolve in (default_journal, config_path):
29 with pytest.raises(RuntimeError, match="tilde-leading"):
30 resolve()
31
32
33@pytest.mark.parametrize(
34 ("home_value", "expected_journal", "expected_config"),
35 [
36 ("./~", "~/journal", Path("~/.config/solstone/config.toml")),
37 ("x~", "x~/journal", Path("x~/.config/solstone/config.toml")),
38 ],
39)
40def test_home_allows_non_tilde_leading_edges(
41 monkeypatch, home_value, expected_journal, expected_config
42):
43 monkeypatch.setenv("HOME", home_value)
44
45 assert default_journal() == expected_journal
46 assert config_path() == expected_config
47
48
49def test_default_journal_returns_home_journal(fake_home):
50 assert default_journal() == str(fake_home / "journal")
51
52
53def test_config_path_returns_user_dot_config(fake_home):
54 assert config_path() == fake_home / ".config" / "solstone" / "config.toml"
55
56
57def test_read_missing_file_returns_empty_dict(fake_home):
58 assert read_user_config() == {}
59
60
61def test_read_malformed_toml_returns_empty_dict(fake_home):
62 cfg = config_path()
63 cfg.parent.mkdir(parents=True)
64 cfg.write_text("this is not = toml", encoding="utf-8")
65
66 assert read_user_config() == {}
67
68
69def test_read_missing_journal_key_returns_empty_dict_keys(fake_home):
70 cfg = config_path()
71 cfg.parent.mkdir(parents=True)
72 cfg.write_text('other = "x"\n', encoding="utf-8")
73
74 assert read_user_config() == {"other": "x"}
75
76
77def test_read_drops_non_string_values(fake_home):
78 cfg = config_path()
79 cfg.parent.mkdir(parents=True)
80 cfg.write_text('journal = 123\nname = "ok"\n', encoding="utf-8")
81
82 assert read_user_config() == {"name": "ok"}
83
84
85def test_write_then_read_roundtrip_plain_path(fake_home):
86 write_user_config(journal="/tmp/x")
87
88 assert read_user_config() == {"journal": "/tmp/x"}
89
90
91def test_write_then_read_roundtrip_path_with_spaces(fake_home):
92 write_user_config(journal="/tmp/some path/journal")
93
94 assert read_user_config() == {"journal": "/tmp/some path/journal"}
95
96
97def test_write_then_read_roundtrip_path_with_quotes_and_backslashes(fake_home):
98 journal = '/tmp/with"quote/and\\backslash'
99
100 write_user_config(journal=journal)
101
102 assert read_user_config() == {"journal": journal}
103
104
105def test_write_creates_parent_directory(fake_home):
106 write_user_config(journal="/x")
107
108 assert config_path().parent.is_dir()
109
110
111def test_write_atomic_no_tmp_left_behind(fake_home):
112 write_user_config(journal="/x")
113
114 leftovers = [
115 path
116 for path in config_path().parent.iterdir()
117 if path.name.startswith(".tmp_config")
118 ]
119 assert leftovers == []
120
121
122def test_write_returns_config_path(fake_home):
123 assert write_user_config(journal="/x") == config_path()