personal memory agent
0

Configure Feed

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

solstone / tests / test_skills_build.py
7.0 kB 210 lines
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 )