personal memory agent
0

Configure Feed

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

solstone / tests / test_template_substitution.py
9.4 kB 280 lines
1# SPDX-License-Identifier: AGPL-3.0-only 2# Copyright (c) 2026 sol pbc 3 4"""Tests for template variable substitution in load_prompt.""" 5 6import json 7 8import pytest 9 10from solstone.think.prompts import _flatten_identity_to_template_vars, load_prompt 11 12 13@pytest.fixture 14def mock_journal_with_config(tmp_path, monkeypatch): 15 """Create a temporary journal with config.""" 16 # Create config directory and journal.json 17 config_dir = tmp_path / "config" 18 config_dir.mkdir() 19 20 config = { 21 "identity": { 22 "name": "Test User", 23 "preferred": "Testy", 24 "bio": "a curious software engineer interested in AI", 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/Los_Angeles", 34 }, 35 "agent": { 36 "name": "sol", 37 "name_status": "default", 38 "named_date": None, 39 }, 40 } 41 42 with open(config_dir / "journal.json", "w") as f: 43 json.dump(config, f) 44 45 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) 46 47 yield tmp_path 48 49 50@pytest.fixture 51def mock_prompt_dir(tmp_path): 52 """Create a temporary directory with test prompt files.""" 53 prompts_dir = tmp_path / "prompts" 54 prompts_dir.mkdir() 55 56 # Create a prompt with template variables 57 template_prompt = """Hello $name, also known as $preferred! 58 59You use $pronouns_subject/$pronouns_object/$pronouns_possessive/$pronouns_reflexive pronouns. 60 61Capitalized: $Pronouns_subject will do it $Pronouns_reflexive. 62 63Bio: $bio 64 65Timezone: $timezone 66""" 67 (prompts_dir / "test_template.md").write_text(template_prompt) 68 69 # Create a prompt without template variables 70 plain_prompt = "This is a plain prompt without any variables." 71 (prompts_dir / "plain.md").write_text(plain_prompt) 72 73 return prompts_dir 74 75 76def test_flatten_identity_basic_fields(): 77 """Test flattening of basic identity fields.""" 78 identity = {"name": "Alice Smith", "preferred": "Alice", "timezone": "UTC"} 79 80 result = _flatten_identity_to_template_vars(identity) 81 82 assert result["name"] == "Alice Smith" 83 assert result["Name"] == "Alice smith" # Capitalized 84 assert result["preferred"] == "Alice" 85 assert result["Preferred"] == "Alice" 86 assert result["timezone"] == "UTC" 87 88 89def test_flatten_identity_nested_pronouns(): 90 """Test flattening of nested pronoun fields.""" 91 identity = { 92 "pronouns": { 93 "subject": "she", 94 "object": "her", 95 "possessive": "her", 96 "reflexive": "herself", 97 } 98 } 99 100 result = _flatten_identity_to_template_vars(identity) 101 102 assert result["pronouns_subject"] == "she" 103 assert result["Pronouns_subject"] == "She" 104 assert result["pronouns_object"] == "her" 105 assert result["pronouns_possessive"] == "her" 106 assert result["pronouns_reflexive"] == "herself" 107 assert result["Pronouns_reflexive"] == "Herself" 108 109 110def test_flatten_identity_with_bio(mock_journal_with_config): 111 """Test bio field extraction.""" 112 from solstone.think.utils import get_config 113 114 config = get_config() 115 identity = config["identity"] 116 117 result = _flatten_identity_to_template_vars(identity) 118 119 assert result["bio"] == "a curious software engineer interested in AI" 120 assert result["Bio"] == "A curious software engineer interested in ai" 121 122 123def test_load_prompt_with_substitution(mock_journal_with_config, mock_prompt_dir): 124 """Test that load_prompt performs template substitution.""" 125 result = load_prompt("test_template", base_dir=mock_prompt_dir) 126 127 # Check that variables were substituted 128 assert "Test User" in result.text 129 assert "Testy" in result.text 130 assert "they/them/their/themselves" in result.text 131 assert "They will do it Themselves" in result.text 132 assert "a curious software engineer interested in AI" in result.text 133 assert "America/Los_Angeles" in result.text 134 135 # Ensure no template variables remain 136 assert "$name" not in result.text 137 assert "$pronouns_subject" not in result.text 138 assert "$bio" not in result.text 139 140 141def test_load_prompt_without_substitution(mock_journal_with_config, mock_prompt_dir): 142 """Test that prompts without variables work normally.""" 143 result = load_prompt("plain", base_dir=mock_prompt_dir) 144 145 assert result.text == "This is a plain prompt without any variables." 146 147 148def test_load_prompt_missing_config_graceful(tmp_path, mock_prompt_dir, monkeypatch): 149 """Test that load_prompt works even without config (safe_substitute).""" 150 # Point to a journal without config 151 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) 152 153 result = load_prompt("test_template", base_dir=mock_prompt_dir) 154 155 # When config exists but has empty values, safe_substitute replaces them with empty strings 156 # Variables that are not set will remain empty in the output (replaced with "") 157 # The prompt should still load without errors 158 assert result.path.exists() 159 160 161def test_load_prompt_with_custom_context(mock_journal_with_config, mock_prompt_dir): 162 """Test that custom context variables are substituted.""" 163 # Create a prompt with context variables 164 context_prompt = """Day: $day 165Segment: $segment 166Custom value: $custom_value""" 167 (mock_prompt_dir / "context_test.md").write_text(context_prompt) 168 169 result = load_prompt( 170 "context_test", 171 base_dir=mock_prompt_dir, 172 context={"day": "20250110", "segment": "143022_300", "custom_value": "hello"}, 173 ) 174 175 assert "Day: 20250110" in result.text 176 assert "Segment: 143022_300" in result.text 177 assert "Custom value: hello" in result.text 178 179 180def test_load_prompt_context_uppercase_versions( 181 mock_journal_with_config, mock_prompt_dir 182): 183 """Test that uppercase-first versions are created for context variables.""" 184 context_prompt = """lowercase: $agent 185Uppercase: $Agent""" 186 (mock_prompt_dir / "uppercase_test.md").write_text(context_prompt) 187 188 result = load_prompt( 189 "uppercase_test", 190 base_dir=mock_prompt_dir, 191 context={"agent": "meetings"}, 192 ) 193 194 assert "lowercase: meetings" in result.text 195 assert "Uppercase: Meetings" in result.text 196 197 198def test_load_prompt_context_overrides_identity( 199 mock_journal_with_config, mock_prompt_dir 200): 201 """Test that context variables override identity variables.""" 202 override_prompt = "Name: $name" 203 (mock_prompt_dir / "override_test.md").write_text(override_prompt) 204 205 # Without context, should use identity name 206 result_default = load_prompt("override_test", base_dir=mock_prompt_dir) 207 assert "Name: Test User" in result_default.text 208 209 # With context, should override 210 result_override = load_prompt( 211 "override_test", 212 base_dir=mock_prompt_dir, 213 context={"name": "Custom Name"}, 214 ) 215 assert "Name: Custom Name" in result_override.text 216 217 218def test_load_prompt_context_stringifies_values( 219 mock_journal_with_config, mock_prompt_dir 220): 221 """Test that non-string context values are converted to strings.""" 222 stringify_prompt = "Number: $count, Bool: $flag" 223 (mock_prompt_dir / "stringify_test.md").write_text(stringify_prompt) 224 225 result = load_prompt( 226 "stringify_test", 227 base_dir=mock_prompt_dir, 228 context={"count": 42, "flag": True}, 229 ) 230 231 assert "Number: 42" in result.text 232 assert "Bool: True" in result.text 233 234 235def test_load_prompt_empty_context(mock_journal_with_config, mock_prompt_dir): 236 """Test that empty context dict behaves same as None. 237 238 Note: mock_journal_with_config needed for get_config() call in load_prompt. 239 """ 240 result_none = load_prompt("plain", base_dir=mock_prompt_dir, context=None) 241 result_empty = load_prompt("plain", base_dir=mock_prompt_dir, context={}) 242 243 assert result_none.text == result_empty.text 244 245 246def test_load_prompt_identity_vars_follow_journal_override(monkeypatch, tmp_path): 247 """Journal identity/ content should not leak across journal overrides.""" 248 249 def write_journal(journal_dir, awareness_text): 250 config_dir = journal_dir / "config" 251 config_dir.mkdir(parents=True) 252 (config_dir / "journal.json").write_text( 253 json.dumps( 254 { 255 "identity": {"name": "Test User"}, 256 "agent": {"name": "sol", "name_status": "default"}, 257 } 258 ) 259 ) 260 identity_dir = journal_dir / "identity" 261 identity_dir.mkdir() 262 (identity_dir / "awareness.md").write_text(awareness_text) 263 264 prompt_dir = tmp_path / "prompts" 265 prompt_dir.mkdir() 266 (prompt_dir / "identity_vars.md").write_text("Awareness:\n$identity_awareness\n") 267 268 journal_one = tmp_path / "journal-one" 269 journal_two = tmp_path / "journal-two" 270 write_journal(journal_one, "first awareness") 271 write_journal(journal_two, "second awareness") 272 273 monkeypatch.setenv("SOLSTONE_JOURNAL", str(journal_one)) 274 first = load_prompt("identity_vars", base_dir=prompt_dir) 275 assert "first awareness" in first.text 276 277 monkeypatch.setenv("SOLSTONE_JOURNAL", str(journal_two)) 278 second = load_prompt("identity_vars", base_dir=prompt_dir) 279 assert "second awareness" in second.text 280 assert "first awareness" not in second.text