personal memory agent
0

Configure Feed

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

solstone / tests / test_journal_skill.py
4.6 kB 149 lines
1# SPDX-License-Identifier: AGPL-3.0-only 2# Copyright (c) 2026 sol pbc 3 4from __future__ import annotations 5 6import shutil 7import subprocess 8from pathlib import Path 9 10import pytest 11 12 13def _repo_root() -> Path: 14 return Path(__file__).resolve().parent.parent 15 16 17def _assert_inside_repo(path: Path, repo_root: Path) -> None: 18 resolved = path.resolve() 19 assert resolved.is_relative_to(repo_root) 20 21 22def _tracked_symlinks(*roots: str) -> list[Path]: 23 repo_root = _repo_root() 24 result = subprocess.run( 25 ["git", "ls-files", *roots], 26 cwd=repo_root, 27 check=True, 28 capture_output=True, 29 text=True, 30 ) 31 return [ 32 repo_root / line 33 for line in result.stdout.splitlines() 34 if line and (repo_root / line).is_symlink() 35 ] 36 37 38def test_journal_skill_references_exist_and_linked(): 39 repo_root = _repo_root() 40 skill_path = repo_root / "solstone" / "talent" / "journal" / "SKILL.md" 41 skill_text = skill_path.read_text(encoding="utf-8") 42 references = [ 43 "references/cli.md", 44 "references/config.md", 45 "references/facets.md", 46 "references/captures.md", 47 "references/logs.md", 48 "references/storage.md", 49 "references/commands.md", 50 ] 51 52 for rel_path in references: 53 ref_path = skill_path.parent / rel_path 54 assert ref_path.exists() 55 assert ref_path.read_text(encoding="utf-8").strip() 56 assert rel_path in skill_text 57 58 59def test_journal_template_symlinks_resolve_inside_repo(): 60 repo_root = _repo_root() 61 for path in _tracked_symlinks("journal", "tests/fixtures/journal"): 62 _assert_inside_repo(path, repo_root) 63 64 65@pytest.mark.timeout(30) 66def test_make_skills_idempotent(tmp_path): 67 """The native project installer is idempotent for make skills' target shape.""" 68 repo_root = _repo_root() 69 temp_root = tmp_path / "repo" 70 temp_root.mkdir() 71 72 (temp_root / ".git").mkdir() 73 shutil.copy2(repo_root / "pyproject.toml", temp_root / "pyproject.toml") 74 bin_dir = temp_root / "bin" 75 bin_dir.mkdir() 76 native = bin_dir / "solstone-core" 77 shutil.copy2(repo_root / ".venv" / "bin" / "solstone-core", native) 78 (temp_root / "solstone").mkdir() 79 shutil.copytree( 80 repo_root / "solstone" / "talent", 81 temp_root / "solstone" / "talent", 82 symlinks=True, 83 ) 84 shutil.copytree( 85 repo_root / "solstone" / "apps", 86 temp_root / "solstone" / "apps", 87 symlinks=True, 88 ) 89 90 def link_state(root: Path) -> dict[str, tuple[str, int]]: 91 return { 92 path.relative_to(root).as_posix(): ( 93 path.readlink().as_posix(), 94 path.lstat().st_mtime_ns, 95 ) 96 for path in sorted(root.rglob("*")) 97 if path.is_symlink() 98 } 99 100 env = {"HOME": str(tmp_path / "home")} 101 102 def install() -> subprocess.CompletedProcess[str]: 103 # Deliberately not check=True: a CalledProcessError reports only the 104 # exit status in pytest's summary and swallows the binary's own 105 # explanation, which is the whole diagnosis. Assert instead, and put 106 # stderr in the failure message. 107 run = subprocess.run( 108 [ 109 str(native), 110 "__solstone_identity=sol", 111 "skills", 112 "install", 113 "--project", 114 str(temp_root), 115 "--agent", 116 "all", 117 ], 118 cwd=temp_root, 119 env=env, 120 capture_output=True, 121 text=True, 122 ) 123 assert run.returncode == 0, ( 124 f"sol skills install exited {run.returncode}: {run.stderr.strip()!r}" 125 ) 126 return run 127 128 first_run = install() 129 assert first_run.stderr == "" 130 131 first = link_state(temp_root) 132 133 second_run = install() 134 assert second_run.stderr == "" 135 136 second = link_state(temp_root) 137 assert first == second 138 assert ( 139 temp_root / ".claude" / "skills" / "journal" 140 ).readlink().as_posix() == "../../solstone/talent/journal" 141 142 # Skill-discovery contract: claude code looks at <cwd>/.claude/skills/, so 143 # after project skill installation the cwd path must resolve to a real 144 # SKILL.md whose content starts with frontmatter. Verifying it here against 145 # the tmp tree means the test is hermetic — it doesn't depend on the dev box 146 # having previously run `make install` or `make skills`. 147 discovered = temp_root / ".claude" / "skills" / "journal" / "SKILL.md" 148 assert discovered.is_file() 149 assert discovered.read_text(encoding="utf-8").startswith("---")