personal memory agent
1# SPDX-License-Identifier: AGPL-3.0-only
2# Copyright (c) 2026 sol pbc
3
4from __future__ import annotations
5
6import shutil
7from pathlib import Path
8
9import frontmatter
10import pytest
11
12from scripts import build_skill_references as skills_build
13from solstone.think.command_polarity import classify_verb
14
15REAL_ROOT = Path(__file__).resolve().parents[1]
16
17
18def _copy_fragment_tree(tmp_path: Path) -> Path:
19 root = tmp_path / "repo"
20 for rel_path in skills_build.discover_fragment_sources(REAL_ROOT):
21 src = REAL_ROOT / rel_path
22 dst = root / rel_path
23 dst.parent.mkdir(parents=True, exist_ok=True)
24 shutil.copy2(src, dst)
25 for src in sorted((REAL_ROOT / "solstone").glob("**/native/**/authority.toml")):
26 rel_path = src.relative_to(REAL_ROOT)
27 dst = root / rel_path
28 dst.parent.mkdir(parents=True, exist_ok=True)
29 shutil.copy2(src, dst)
30 return root
31
32
33def _patch_root(monkeypatch: pytest.MonkeyPatch, root: Path) -> None:
34 monkeypatch.setattr(skills_build, "ROOT", root)
35
36
37def _output_paths(root: Path) -> set[Path]:
38 return {
39 root / skills_build.SOL_COMMANDS_PATH,
40 root / skills_build.JOURNAL_COMMANDS_PATH,
41 }
42
43
44def _section(content: str, heading: str) -> str:
45 start = content.index(heading)
46 next_heading = content.find("\n## ", start + 1)
47 if next_heading == -1:
48 return content[start:]
49 return content[start:next_heading]
50
51
52def test_default_root_points_to_repo_root():
53 """Guard parents[N] root arithmetic; _patch_root masks the default elsewhere."""
54 assert skills_build.ROOT == REAL_ROOT
55 assert (skills_build.ROOT / "pyproject.toml").is_file()
56 assert (skills_build.ROOT / "Makefile").is_file()
57
58
59def test_render_is_deterministic_and_build_check_is_current(monkeypatch, tmp_path):
60 root = _copy_fragment_tree(tmp_path)
61 _patch_root(monkeypatch, root)
62
63 first = skills_build.render()
64 second = skills_build.render()
65 assert first == second
66
67 written = skills_build.build()
68 assert set(written) == _output_paths(root)
69 assert skills_build.check() == []
70
71 before = {path: path.read_bytes() for path in written}
72 skills_build.build()
73 after = {path: path.read_bytes() for path in written}
74 assert before == after
75
76
77def test_check_detects_staleness_without_writing(monkeypatch, tmp_path):
78 root = _copy_fragment_tree(tmp_path)
79 _patch_root(monkeypatch, root)
80 written = skills_build.build()
81 stale_path = written[0]
82 stale_path.write_text(
83 stale_path.read_text(encoding="utf-8") + "stale\n", encoding="utf-8"
84 )
85 before = stale_path.read_text(encoding="utf-8")
86
87 stale = skills_build.check()
88
89 assert stale == [stale_path]
90 assert stale_path.read_text(encoding="utf-8") == before
91
92
93@pytest.mark.parametrize(
94 ("content", "message"),
95 [
96 ("---\nname: [\n---\n", "malformed frontmatter"),
97 ("---\nname: wrong\ndescription: nope\n---\n", "does not match directory"),
98 ("---\nname: activities\n---\n", "missing frontmatter description"),
99 ],
100)
101def test_malformed_fragment_raises_with_path_without_partial_output(
102 monkeypatch, tmp_path, content, message
103):
104 root = _copy_fragment_tree(tmp_path)
105 _patch_root(monkeypatch, root)
106 target = root / "solstone/apps/activities/talent/activities/SKILL.md"
107 target.write_text(content, encoding="utf-8")
108
109 with pytest.raises(ValueError) as exc_info:
110 skills_build.build()
111
112 assert str(target) in str(exc_info.value)
113 assert message in str(exc_info.value)
114 for output in _output_paths(root):
115 assert not output.exists()
116
117
118def test_duplicate_fragment_name_raises_with_path_without_partial_output(
119 monkeypatch, tmp_path
120):
121 root = _copy_fragment_tree(tmp_path)
122 _patch_root(monkeypatch, root)
123 duplicate = Path("solstone/apps/entities-copy/talent/entities/SKILL.md")
124 duplicate_path = root / duplicate
125 duplicate_path.parent.mkdir(parents=True, exist_ok=True)
126 shutil.copy2(
127 root / "solstone/apps/entities/talent/entities/SKILL.md", duplicate_path
128 )
129 sources = skills_build.discover_fragment_sources(root) + (duplicate,)
130 monkeypatch.setattr(skills_build, "FRAGMENT_SOURCES", sources)
131
132 with pytest.raises(ValueError) as exc_info:
133 skills_build.build()
134
135 assert str(duplicate_path) in str(exc_info.value)
136 assert "duplicate app key 'entities'" in str(exc_info.value)
137 for output in _output_paths(root):
138 assert not output.exists()
139
140
141def test_polarity_classification_and_rendered_other_group(monkeypatch, tmp_path):
142 assert classify_verb("search") == "read"
143 assert classify_verb("merge") == "write"
144 assert classify_verb("detect") == "other"
145 assert classify_verb("scan") == "read"
146 assert classify_verb("segments") == "other"
147
148 root = _copy_fragment_tree(tmp_path)
149 _patch_root(monkeypatch, root)
150 content = skills_build.render()[str(root / skills_build.SOL_COMMANDS_PATH)]
151 health = _section(content, "## health")
152 assert "Other: `for-range`, `full`, `pipeline`, `summary`" in health
153
154
155def test_health_contributes_to_both_router_references(monkeypatch, tmp_path):
156 root = _copy_fragment_tree(tmp_path)
157 _patch_root(monkeypatch, root)
158
159 outputs = skills_build.render()
160 sol = outputs[str(root / skills_build.SOL_COMMANDS_PATH)]
161 journal = outputs[str(root / skills_build.JOURNAL_COMMANDS_PATH)]
162
163 assert "## health — `sol call health`" in sol
164 assert "## health — `journal health`, `journal talent`" in journal
165 assert "Guidance: `solstone/apps/health/talent/health/SKILL.md`" in journal
166
167
168def test_fragments_are_discovered_from_app_talent_dirs(monkeypatch, tmp_path):
169 root = _copy_fragment_tree(tmp_path)
170 extra = root / "solstone/apps/newapp/talent/newapp/SKILL.md"
171 extra.parent.mkdir(parents=True)
172 extra.write_text(
173 "---\n"
174 "name: newapp\n"
175 "description: >\n"
176 " New app commands. TRIGGER: new app, extra route.\n"
177 "---\n"
178 "\n"
179 "# New App\n",
180 encoding="utf-8",
181 )
182 _patch_root(monkeypatch, root)
183 monkeypatch.setattr(
184 skills_build,
185 "_command_names",
186 lambda app_name, _source: ("list",) if app_name == "newapp" else ("status",),
187 )
188
189 outputs = skills_build.render()
190 sol = outputs[str(root / skills_build.SOL_COMMANDS_PATH)]
191
192 assert "## newapp — `sol call newapp`" in sol
193 assert "Triggers: `new app`, `extra route`" in sol
194 assert "Guidance: `solstone/apps/newapp/talent/newapp/SKILL.md`" in sol
195
196
197def test_trigger_parsing_matches_fragment_description():
198 post = frontmatter.load(
199 REAL_ROOT / "solstone/apps/activities/talent/activities/SKILL.md"
200 )
201
202 assert skills_build._parse_triggers(post.metadata["description"]) == (
203 "activity",
204 "activities",
205 "work session",
206 "completed span",
207 "mute/unmute",
208 "activity record",
209 "meeting attendees",
210 )