Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 27 additions & 4 deletions plugin/scripts/verify_refs.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,15 @@ def cmd_check(entries, project, cfg, strict):
issues.append((e, ref, actionable))
notes = [n for n in (config_warning(project), version_warning(project)) if n]
if not issues:
print("OK learnings: all entries have valid frontmatter file refs.")
if entries:
print("OK learnings: all entries have valid frontmatter file refs.")
else:
# "all entries are valid" after checking none reads as a clean bill
# of health. Say what was actually examined -- the personal store is
# out of scope here (see `_personal_entries`), so a personal-only
# project would otherwise be told everything is fine, twice over.
print("OK learnings: no entries in the team store - nothing to "
"check (the personal store is not ref-checked).")
for n in notes:
print(f" {n}")
return 0
Expand Down Expand Up @@ -554,10 +562,25 @@ def main():
project = find_project_dir()
cfg = load_config(project)
store = project / cfg["storeDir"]
# A project can legitimately run personal-store-only: `personalStoreDir` set,
# no team store. Bailing on the team store alone made every mode dead there
# while recall and capture kept working -- the store looked healthy and its
# maintenance tooling was silently gone. Only give up when BOTH are absent.
# Which entries each mode covers is unchanged: the personal store stays out
# of everything but --stats, for the reasons in `_personal_entries`.
personal = personal_store(project, cfg)
has_personal = personal is not None and personal.is_dir()
if not store.is_dir():
print(f"No learnings store at {store} - run /lore:init first.")
return 0
entries = list(iter_entries(store))
if not has_personal:
print(f"No learnings store at {store} - run /lore:init first.")
return 0
if args.index:
# --index regenerates the team store's README; there is no team
# store, and the personal one is deliberately never indexed.
print(f"No team store at {store} - nothing to index "
f"(the personal store is never indexed).")
return 0
entries = list(iter_entries(store)) if store.is_dir() else []
if args.index:
return cmd_index(entries, project, cfg, store)
if args.stats:
Expand Down
68 changes: 68 additions & 0 deletions tests/test_verify_refs.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
"""Tests for verify_refs.py: dates, dupes, config, index, git drift."""
import contextlib
import io
import json
import os
import shutil
import subprocess
import tempfile
import unittest
from datetime import date, timedelta
from pathlib import Path
from unittest import mock

import _util # noqa: F401
from _common import DEFAULTS, iter_entries
Expand Down Expand Up @@ -225,6 +228,71 @@ def test_absent_personal_store_prints_nothing(self):
self.assertNotIn("personal store", out.getvalue())


class TestPersonalOnlyStore(unittest.TestCase):
"""A project may run personal-store-only: `personalStoreDir` set, no team store.

Bailing on the team store alone used to kill every mode there, while recall
and capture kept working — the store looked healthy and its maintenance
tooling was silently gone.
"""

def _run(self, proj, *argv):
out = io.StringIO()
with mock.patch.dict(os.environ, {"CLAUDE_PROJECT_DIR": str(proj)}), \
mock.patch("sys.argv", ["verify_refs.py", *argv]), \
contextlib.redirect_stdout(out):
rc = verify_refs.main()
return rc, out.getvalue()

def _project(self, with_team_store=False):
td = tempfile.TemporaryDirectory()
self.addCleanup(td.cleanup)
proj = Path(td.name)
(proj / ".lore.json").write_text(
json.dumps({"personalStoreDir": ".lore/personal"}), encoding="utf-8")
write_entry(proj / ".lore" / "personal", "prefs/p.md", title="P")
if with_team_store:
write_entry(proj / "learnings", "x/a.md", title="A")
return proj

def test_default_mode_runs_instead_of_bailing(self):
rc, text = self._run(self._project())
self.assertEqual(rc, 0)
self.assertNotIn("run /lore:init first", text)
self.assertIn("nothing to check", text)

def test_stats_reports_the_personal_entries(self):
rc, text = self._run(self._project(), "--stats")
self.assertEqual(rc, 0)
self.assertIn("personal store: 1 entries", text)

def test_report_and_dupes_run(self):
for flag in ("--report", "--dupes"):
with self.subTest(flag=flag):
rc, text = self._run(self._project(), flag)
self.assertEqual(rc, 0)
self.assertNotIn("run /lore:init first", text)

def test_index_is_a_no_op_without_a_team_store(self):
proj = self._project()
rc, text = self._run(proj, "--index")
self.assertEqual(rc, 0)
self.assertIn("nothing to index", text)
self.assertFalse((proj / "learnings").exists())

def test_no_store_at_all_still_bails(self):
td = tempfile.TemporaryDirectory()
self.addCleanup(td.cleanup)
rc, text = self._run(Path(td.name))
self.assertEqual(rc, 0)
self.assertIn("run /lore:init first", text)

def test_populated_team_store_message_unchanged(self):
rc, text = self._run(self._project(with_team_store=True))
self.assertEqual(rc, 0)
self.assertIn("all entries have valid frontmatter file refs", text)


@unittest.skipUnless(GIT, "git not available")
class TestGitDrift(unittest.TestCase):
def _git(self, proj, *args):
Expand Down
Loading