personal memory agent
0

Configure Feed

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

solstone / tests / test_generators.py
9.3 kB 246 lines
1# SPDX-License-Identifier: AGPL-3.0-only 2# Copyright (c) 2026 sol pbc 3 4import importlib 5import os 6import uuid 7from pathlib import Path 8 9import pytest 10 11 12def test_get_talent_configs_generators(): 13 """Test that system generators are discovered with source field.""" 14 talent = importlib.import_module("solstone.think.talent") 15 generators = talent.get_talent_configs(type="generate") 16 assert "schedule" in generators 17 info = generators["schedule"] 18 assert os.path.basename(info["path"]) == "schedule.md" 19 assert isinstance(info["color"], str) 20 assert isinstance(info["mtime"], int) 21 assert "title" in info 22 assert "occurrences" not in info 23 # New: check source field 24 assert info.get("source") == "system" 25 26 27def test_get_output_name(): 28 """Test generator key to filename conversion.""" 29 talent = importlib.import_module("solstone.think.talent") 30 31 # System generators: key unchanged 32 assert talent.get_output_name("activity") == "activity" 33 assert talent.get_output_name("flow") == "flow" 34 35 # App generators: _app_name format 36 assert talent.get_output_name("chat:sentiment") == "_chat_sentiment" 37 assert talent.get_output_name("my_app:weekly_summary") == "_my_app_weekly_summary" 38 39 40def test_get_talent_configs_app_discovery(tmp_path, monkeypatch): 41 """Test that app generators are discovered from apps/*/talent/.""" 42 talent = importlib.import_module("solstone.think.talent") 43 44 # Create a fake app with a generator 45 app_dir = tmp_path / "apps" / "test_app" / "talent" 46 app_dir.mkdir(parents=True) 47 48 # Create generator files with frontmatter 49 (app_dir / "custom_generator.md").write_text( 50 '{\n "title": "Custom Generator",\n "color": "#ff0000"\n}\n\nTest prompt' 51 ) 52 53 # Also create workspace.html to make it a valid app (not strictly required for generators) 54 (tmp_path / "apps" / "test_app" / "workspace.html").write_text("<h1>Test</h1>") 55 56 # For now, just verify system generators have correct source 57 generators = talent.get_talent_configs(type="generate") 58 for key, info in generators.items(): 59 if ":" not in key: 60 assert info.get("source") == "system", f"{key} should have source=system" 61 62 63def test_get_talent_configs_by_schedule(): 64 """Test filtering generators by schedule.""" 65 talent = importlib.import_module("solstone.think.talent") 66 67 # Get daily generators 68 daily = talent.get_talent_configs(type="generate", schedule="daily") 69 assert len(daily) > 0 70 for key, meta in daily.items(): 71 assert meta.get("schedule") == "daily", f"{key} should have schedule=daily" 72 73 # Get segment generators 74 segment = talent.get_talent_configs(type="generate", schedule="segment") 75 assert len(segment) > 0 76 for key, meta in segment.items(): 77 assert meta.get("schedule") == "segment", f"{key} should have schedule=segment" 78 79 # Verify no overlap 80 assert not set(daily.keys()) & set(segment.keys()), ( 81 "daily and segment should not overlap" 82 ) 83 84 # Unknown schedule returns empty dict 85 assert talent.get_talent_configs(type="generate", schedule="hourly") == {} 86 assert talent.get_talent_configs(type="generate", schedule="") == {} 87 88 89def test_get_talent_configs_include_disabled(monkeypatch): 90 """Test include_disabled parameter.""" 91 talent = importlib.import_module("solstone.think.talent") 92 93 # Get generators without disabled (default) 94 without_disabled = talent.get_talent_configs(type="generate", schedule="daily") 95 96 # Get generators with disabled included 97 with_disabled = talent.get_talent_configs( 98 type="generate", schedule="daily", include_disabled=True 99 ) 100 101 # Should have at least as many with disabled included 102 assert len(with_disabled) >= len(without_disabled) 103 104 105def test_scheduled_generators_have_valid_schedule(): 106 """Test that scheduled generators have valid schedule field. 107 108 Generators with a schedule field must have valid values 109 ('segment', 'daily', 'weekly', 'cadence', or 'activity'). Some generators 110 (like importer) have output but no schedule - they're used for ad-hoc 111 processing, not scheduled runs. 112 """ 113 talent = importlib.import_module("solstone.think.talent") 114 115 generators = talent.get_talent_configs(type="generate") 116 valid_schedules = ("segment", "daily", "activity", "weekly", "cadence") 117 118 for key, meta in generators.items(): 119 sched = meta.get("schedule") 120 if sched is not None: 121 assert sched in valid_schedules, ( 122 f"Generator '{key}' has invalid schedule '{sched}'" 123 ) 124 125 126def test_all_talent_configs_declare_non_empty_title(): 127 talent = importlib.import_module("solstone.think.talent") 128 129 configs = talent.get_talent_configs(include_disabled=True) 130 missing = [ 131 key 132 for key, meta in configs.items() 133 if not isinstance(meta.get("title"), str) or not meta["title"].strip() 134 ] 135 136 assert not missing, f"talent configs missing non-empty title: {missing}" 137 138 139def test_sense_in_segment_schedule(): 140 """Test that sense generator exists in segment schedule at priority 5.""" 141 talent = importlib.import_module("solstone.think.talent") 142 143 generators = talent.get_talent_configs(type="generate", schedule="segment") 144 assert "sense" in generators 145 146 sense = generators["sense"] 147 assert sense.get("priority") == 5, "sense should be at priority 5" 148 149 sources = sense.get("load", {}) 150 151 assert sources.get("transcripts") is True, "sense should include transcripts" 152 assert sources.get("percepts") is True, "sense should include percepts" 153 154 155def _write_temp_talent_prompt(talent_dir: Path, stem: str, frontmatter: str) -> Path: 156 talent_dir.mkdir(parents=True, exist_ok=True) 157 prompt_path = talent_dir / f"{stem}.md" 158 prompt_path.write_text( 159 f"{frontmatter}\n\nTemporary test prompt\n", encoding="utf-8" 160 ) 161 return prompt_path 162 163 164def test_get_talent_configs_raises_on_missing_type_with_output(tmp_path, monkeypatch): 165 talent = importlib.import_module("solstone.think.talent") 166 monkeypatch.setattr(talent, "TALENT_DIR", tmp_path) 167 stem = f"test_missing_type_output_{uuid.uuid4().hex}" 168 prompt_path = _write_temp_talent_prompt( 169 tmp_path, 170 stem, 171 '{\n "schedule": "daily",\n "priority": 10,\n "output": "md"\n}', 172 ) 173 try: 174 with pytest.raises( 175 ValueError, match=rf"Prompt '{stem}'.*missing required 'type'" 176 ): 177 talent.get_talent_configs(include_disabled=True) 178 finally: 179 prompt_path.unlink(missing_ok=True) 180 181 182def test_get_talent_configs_allows_missing_type_with_tools(tmp_path, monkeypatch): 183 talent = importlib.import_module("solstone.think.talent") 184 monkeypatch.setattr(talent, "TALENT_DIR", tmp_path) 185 stem = f"test_missing_type_tools_{uuid.uuid4().hex}" 186 prompt_path = _write_temp_talent_prompt( 187 tmp_path, 188 stem, 189 '{\n "schedule": "daily",\n "priority": 10,\n "tools": "journal"\n}', 190 ) 191 try: 192 configs = talent.get_talent_configs(include_disabled=True) 193 assert stem in configs 194 assert configs[stem].get("type") is None 195 finally: 196 prompt_path.unlink(missing_ok=True) 197 198 199def test_get_talent_configs_raises_when_generate_missing_output(tmp_path, monkeypatch): 200 talent = importlib.import_module("solstone.think.talent") 201 monkeypatch.setattr(talent, "TALENT_DIR", tmp_path) 202 stem = f"test_generate_missing_output_{uuid.uuid4().hex}" 203 prompt_path = _write_temp_talent_prompt( 204 tmp_path, 205 stem, 206 '{\n "type": "generate",\n "schedule": "daily",\n "priority": 10\n}', 207 ) 208 try: 209 with pytest.raises( 210 ValueError, 211 match=rf"Prompt '{stem}'.*type='generate'.*missing required 'output'", 212 ): 213 talent.get_talent_configs(include_disabled=True) 214 finally: 215 prompt_path.unlink(missing_ok=True) 216 217 218def test_get_talent_configs_allows_cogitate_without_tools(tmp_path, monkeypatch): 219 talent = importlib.import_module("solstone.think.talent") 220 monkeypatch.setattr(talent, "TALENT_DIR", tmp_path) 221 stem = f"test_cogitate_missing_tools_{uuid.uuid4().hex}" 222 prompt_path = _write_temp_talent_prompt( 223 tmp_path, 224 stem, 225 '{\n "type": "cogitate",\n "schedule": "daily",\n "priority": 10\n}', 226 ) 227 try: 228 configs = talent.get_talent_configs(include_disabled=True) 229 assert stem in configs 230 assert configs[stem]["type"] == "cogitate" 231 finally: 232 prompt_path.unlink(missing_ok=True) 233 234 235def test_get_talent_configs_type_generate_returns_only_generate(): 236 talent = importlib.import_module("solstone.think.talent") 237 generators = talent.get_talent_configs(type="generate") 238 assert generators, "Expected at least one generate prompt" 239 assert all(meta.get("type") == "generate" for meta in generators.values()) 240 241 242def test_get_talent_configs_type_cogitate_returns_only_cogitate(): 243 talent = importlib.import_module("solstone.think.talent") 244 cogitate_prompts = talent.get_talent_configs(type="cogitate") 245 assert cogitate_prompts, "Expected at least one cogitate prompt" 246 assert all(meta.get("type") == "cogitate" for meta in cogitate_prompts.values())