personal memory agent
1# SPDX-License-Identifier: AGPL-3.0-only
2# Copyright (c) 2026 sol pbc
3
4"""Tests for journal configuration utilities."""
5
6import json
7import os
8
9import pytest
10
11from think.utils import get_config
12
13
14@pytest.fixture
15def config_journal(tmp_path):
16 """Create a temporary journal with config."""
17 config_dir = tmp_path / "config"
18 config_dir.mkdir()
19
20 config_data = {
21 "identity": {
22 "name": "Test User",
23 "preferred": "Tester",
24 "bio": "a software engineer and tester",
25 "pronouns": {
26 "subject": "they",
27 "object": "them",
28 "possessive": "their",
29 "reflexive": "themselves",
30 },
31 "aliases": ["test", "tester"],
32 "email_addresses": ["test@example.com"],
33 "timezone": "America/New_York",
34 }
35 }
36
37 config_file = config_dir / "journal.json"
38 with open(config_file, "w") as f:
39 json.dump(config_data, f, indent=2)
40 f.write("\n")
41
42 return tmp_path
43
44
45def test_get_config_default_structure(tmp_path, monkeypatch):
46 """Test get_config returns default structure when file doesn't exist."""
47 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path))
48
49 config = get_config()
50
51 assert "identity" in config
52 assert config["identity"]["name"] == ""
53 assert config["identity"]["preferred"] == ""
54 assert config["identity"]["pronouns"] == {
55 "subject": "",
56 "object": "",
57 "possessive": "",
58 "reflexive": "",
59 }
60 assert config["identity"]["aliases"] == []
61 assert config["identity"]["email_addresses"] == []
62 assert config["identity"]["timezone"] == ""
63 assert config["identity"]["bio"] == ""
64
65 # Describe defaults
66 assert "describe" in config
67 assert isinstance(config["describe"]["redact"], list)
68 assert len(config["describe"]["redact"]) > 0
69
70
71def test_get_config_default_is_deep_copy(tmp_path, monkeypatch):
72 """Test that modifying returned defaults doesn't affect future calls."""
73 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path))
74
75 config1 = get_config()
76 config1["identity"]["name"] = "Modified"
77 config1["describe"]["redact"].append("extra rule")
78
79 config2 = get_config()
80 assert config2["identity"]["name"] == ""
81 assert "extra rule" not in config2["describe"]["redact"]
82
83
84def test_get_config_loads_existing(config_journal, monkeypatch):
85 """Test get_config loads existing configuration."""
86 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(config_journal))
87
88 config = get_config()
89
90 assert config["identity"]["name"] == "Test User"
91 assert config["identity"]["preferred"] == "Tester"
92 assert config["identity"]["pronouns"] == {
93 "subject": "they",
94 "object": "them",
95 "possessive": "their",
96 "reflexive": "themselves",
97 }
98 assert config["identity"]["aliases"] == ["test", "tester"]
99 assert config["identity"]["email_addresses"] == ["test@example.com"]
100 assert config["identity"]["timezone"] == "America/New_York"
101 assert config["identity"]["bio"] == "a software engineer and tester"
102
103
104def test_get_config_existing_is_master(tmp_path, monkeypatch):
105 """Test that existing journal.json is returned as-is without merging defaults."""
106 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path))
107
108 # Create config with only a name - no other identity fields, no describe
109 config_dir = tmp_path / "config"
110 config_dir.mkdir()
111
112 partial_config = {
113 "identity": {
114 "name": "Partial User",
115 }
116 }
117
118 config_file = config_dir / "journal.json"
119 with open(config_file, "w") as f:
120 json.dump(partial_config, f)
121
122 config = get_config()
123
124 # User's value is preserved
125 assert config["identity"]["name"] == "Partial User"
126 # Missing fields are NOT filled from defaults - journal.json is master
127 assert "preferred" not in config["identity"]
128 assert "describe" not in config
129
130
131def test_get_config_empty_journal(tmp_path, monkeypatch):
132 """Test get_config returns defaults with an empty journal directory."""
133 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path))
134
135 config = get_config()
136 assert "identity" in config
137 assert config["identity"]["name"] == ""
138
139
140def test_get_config_handles_invalid_json(tmp_path, monkeypatch):
141 """Test get_config returns defaults when JSON is invalid."""
142 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path))
143
144 # Create config with invalid JSON
145 config_dir = tmp_path / "config"
146 config_dir.mkdir()
147
148 config_file = config_dir / "journal.json"
149 with open(config_file, "w") as f:
150 f.write("{ invalid json }")
151
152 # Should return default structure and log warning
153 config = get_config()
154
155 assert "identity" in config
156 assert config["identity"]["name"] == ""
157 assert config["identity"]["pronouns"] == {
158 "subject": "",
159 "object": "",
160 "possessive": "",
161 "reflexive": "",
162 }
163 assert config["identity"]["bio"] == ""
164 assert "describe" in config
165
166
167def test_get_config_with_fixtures():
168 """Test get_config with tests/fixtures/journal path."""
169 # Set _SOLSTONE_JOURNAL_OVERRIDE to fixtures
170 os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = "tests/fixtures/journal"
171
172 config = get_config()
173
174 # Fixtures has journal.json - returned as-is
175 assert "identity" in config
176 assert isinstance(config["identity"]["name"], str)
177 assert isinstance(config["identity"]["preferred"], str)
178 assert isinstance(config["identity"]["pronouns"], dict)
179 assert "subject" in config["identity"]["pronouns"]
180 assert "object" in config["identity"]["pronouns"]
181 assert "possessive" in config["identity"]["pronouns"]
182 assert "reflexive" in config["identity"]["pronouns"]
183 assert isinstance(config["identity"]["aliases"], list)
184 assert isinstance(config["identity"]["email_addresses"], list)
185 assert isinstance(config["identity"]["timezone"], str)
186 assert isinstance(config["identity"]["bio"], str)