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
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("---")