Skip to content
This repository was archived by the owner on Jan 2, 2026. It is now read-only.

Commit cd709f3

Browse files
zircoteclaude
andcommitted
test: add E2E functional tests for commands and hooks
Add comprehensive E2E tests to validate plugin functionality and prevent regressions in API contracts: Commands tested (tests/test_e2e_commands.py): - /memory:status - config API exports, IndexService.get_stats() - /memory:capture - CaptureService API and result structure - /memory:recall - RecallService.search() and result attributes - /memory:search - semantic and text search APIs - /memory:sync - reindex(), verify_consistency(), repair() Hooks tested (tests/test_e2e_hooks.py): - SessionStart - handler main(), entry point syntax - UserPromptSubmit - SignalDetector API - PostToolUse - DomainExtractor singleton, NamespaceParser - PreCompact - CaptureDecider, GuidanceBuilder APIs - Stop - SessionAnalyzer methods Also validates: - hooks.json configuration and timeouts - All referenced hook entry point files exist - Handlers have consistent main() interface Total: 48 new E2E tests, 1324 tests passing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent fba7510 commit cd709f3

2 files changed

Lines changed: 765 additions & 0 deletions

File tree

tests/test_e2e_commands.py

Lines changed: 361 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,361 @@
1+
"""End-to-end functional tests for plugin commands.
2+
3+
These tests validate the command execution paths used by the skill files
4+
to ensure API contracts are maintained and prevent regressions.
5+
6+
Each command corresponds to a skill file in commands/:
7+
- /memory:capture -> commands/capture.md
8+
- /memory:recall -> commands/recall.md
9+
- /memory:search -> commands/search.md
10+
- /memory:status -> commands/status.md
11+
- /memory:sync -> commands/sync.md
12+
"""
13+
14+
from __future__ import annotations
15+
16+
import subprocess
17+
import sys
18+
from pathlib import Path
19+
from typing import TYPE_CHECKING
20+
21+
import pytest
22+
23+
if TYPE_CHECKING:
24+
pass
25+
26+
27+
class TestStatusCommandE2E:
28+
"""E2E tests for /memory:status command.
29+
30+
Tests the exact Python code executed by commands/status.md.
31+
"""
32+
33+
def test_basic_status_api_exists(self) -> None:
34+
"""Test that all APIs used by basic status command exist."""
35+
# These imports must succeed for the command to work
36+
from git_notes_memory import get_sync_service
37+
from git_notes_memory.config import (
38+
get_data_path,
39+
get_embedding_model,
40+
get_index_path,
41+
)
42+
from git_notes_memory.index import IndexService
43+
44+
# Functions must be callable
45+
assert callable(get_sync_service)
46+
assert callable(get_index_path)
47+
assert callable(get_data_path)
48+
assert callable(get_embedding_model)
49+
50+
# Classes must be instantiable
51+
index_path = get_index_path()
52+
index = IndexService(index_path)
53+
assert index is not None
54+
55+
def test_basic_status_execution(self) -> None:
56+
"""Test basic status command executes without error."""
57+
code = """
58+
from git_notes_memory import get_sync_service
59+
from git_notes_memory.index import IndexService
60+
from git_notes_memory.config import get_embedding_model, get_index_path, get_data_path
61+
62+
sync = get_sync_service()
63+
index_path = get_index_path()
64+
65+
if index_path.exists():
66+
index = IndexService(index_path)
67+
index.initialize()
68+
stats = index.get_stats()
69+
assert stats.total_memories >= 0
70+
assert stats.index_size_bytes >= 0
71+
index.close()
72+
73+
assert get_embedding_model() == "all-MiniLM-L6-v2"
74+
assert get_data_path() is not None
75+
"""
76+
result = subprocess.run(
77+
[sys.executable, "-c", code],
78+
capture_output=True,
79+
text=True,
80+
timeout=30,
81+
)
82+
assert result.returncode == 0, f"Status command failed: {result.stderr}"
83+
84+
def test_verbose_status_api_exists(self) -> None:
85+
"""Test that all APIs used by verbose status command exist."""
86+
from git_notes_memory import get_sync_service
87+
from git_notes_memory.config import (
88+
NAMESPACES,
89+
get_index_path,
90+
)
91+
from git_notes_memory.index import IndexService
92+
93+
# NAMESPACES must be iterable (frozenset)
94+
assert isinstance(NAMESPACES, frozenset)
95+
assert len(NAMESPACES) == 10
96+
97+
# Sync service must have verify_consistency
98+
sync = get_sync_service()
99+
assert hasattr(sync, "verify_consistency")
100+
101+
# IndexService.get_stats must return proper structure
102+
index_path = get_index_path()
103+
if index_path.exists():
104+
index = IndexService(index_path)
105+
index.initialize()
106+
stats = index.get_stats()
107+
# Stats must have required attributes
108+
assert hasattr(stats, "total_memories")
109+
assert hasattr(stats, "by_namespace")
110+
assert hasattr(stats, "by_spec")
111+
assert hasattr(stats, "last_sync")
112+
assert hasattr(stats, "index_size_bytes")
113+
index.close()
114+
115+
116+
class TestCaptureCommandE2E:
117+
"""E2E tests for /memory:capture command."""
118+
119+
def test_capture_api_exists(self) -> None:
120+
"""Test that capture API exists and is callable."""
121+
from git_notes_memory import get_capture_service
122+
123+
capture = get_capture_service()
124+
assert hasattr(capture, "capture")
125+
assert callable(capture.capture)
126+
127+
def test_capture_result_structure(self, tmp_path: Path) -> None:
128+
"""Test that capture result has expected structure."""
129+
from git_notes_memory.capture import CaptureService
130+
from git_notes_memory.index import IndexService
131+
132+
# Set up isolated test environment
133+
index_path = tmp_path / "index.db"
134+
index = IndexService(index_path)
135+
index.initialize()
136+
137+
capture = CaptureService(
138+
repo_path=tmp_path,
139+
index_service=index,
140+
)
141+
142+
# Initialize git repo
143+
subprocess.run(
144+
["git", "init"],
145+
cwd=tmp_path,
146+
capture_output=True,
147+
check=True,
148+
)
149+
subprocess.run(
150+
["git", "config", "user.email", "test@example.com"],
151+
cwd=tmp_path,
152+
capture_output=True,
153+
check=True,
154+
)
155+
subprocess.run(
156+
["git", "config", "user.name", "Test User"],
157+
cwd=tmp_path,
158+
capture_output=True,
159+
check=True,
160+
)
161+
# Create initial commit
162+
(tmp_path / "README.md").write_text("test")
163+
subprocess.run(
164+
["git", "add", "."],
165+
cwd=tmp_path,
166+
capture_output=True,
167+
check=True,
168+
)
169+
subprocess.run(
170+
["git", "commit", "-m", "Initial"],
171+
cwd=tmp_path,
172+
capture_output=True,
173+
check=True,
174+
)
175+
176+
result = capture.capture(
177+
namespace="learnings",
178+
summary="Test E2E capture",
179+
content="This is a test capture for E2E validation.",
180+
)
181+
182+
# Result must have expected attributes for command script
183+
assert hasattr(result, "success")
184+
assert hasattr(result, "memory")
185+
assert hasattr(result, "warning")
186+
187+
if result.success:
188+
assert result.memory is not None
189+
assert hasattr(result.memory, "namespace")
190+
assert hasattr(result.memory, "id")
191+
assert hasattr(result.memory, "summary")
192+
193+
index.close()
194+
195+
196+
class TestRecallCommandE2E:
197+
"""E2E tests for /memory:recall command."""
198+
199+
def test_recall_api_exists(self) -> None:
200+
"""Test that recall API exists and is callable."""
201+
from git_notes_memory import get_recall_service
202+
203+
recall = get_recall_service()
204+
assert hasattr(recall, "search")
205+
assert callable(recall.search)
206+
207+
def test_recall_result_structure(self) -> None:
208+
"""Test that recall results have expected structure."""
209+
from git_notes_memory import get_recall_service
210+
211+
recall = get_recall_service()
212+
results = recall.search(query="test", k=5)
213+
214+
# Results is a list
215+
assert isinstance(results, list)
216+
217+
# Each result must have expected attributes
218+
for r in results:
219+
assert hasattr(r, "namespace")
220+
assert hasattr(r, "summary")
221+
assert hasattr(r, "score")
222+
assert hasattr(r, "timestamp")
223+
assert hasattr(r, "content")
224+
225+
226+
class TestSearchCommandE2E:
227+
"""E2E tests for /memory:search command."""
228+
229+
def test_search_api_exists(self) -> None:
230+
"""Test that search API exists."""
231+
from git_notes_memory import get_recall_service
232+
233+
recall = get_recall_service()
234+
# Semantic search
235+
assert hasattr(recall, "search")
236+
# Text search
237+
assert hasattr(recall, "search_text")
238+
239+
def test_search_text_result_structure(self) -> None:
240+
"""Test that text search results have expected structure."""
241+
from git_notes_memory import get_recall_service
242+
243+
recall = get_recall_service()
244+
results = recall.search_text(query="test", limit=5)
245+
246+
assert isinstance(results, list)
247+
for m in results:
248+
# Must have attributes used in command script
249+
assert hasattr(m, "namespace")
250+
assert hasattr(m, "summary")
251+
assert hasattr(m, "timestamp")
252+
253+
254+
class TestSyncCommandE2E:
255+
"""E2E tests for /memory:sync command."""
256+
257+
def test_sync_api_exists(self) -> None:
258+
"""Test that sync API exists."""
259+
from git_notes_memory import get_sync_service
260+
261+
sync = get_sync_service()
262+
263+
# Must have methods used by command scripts
264+
assert hasattr(sync, "reindex")
265+
assert hasattr(sync, "verify_consistency")
266+
assert hasattr(sync, "repair")
267+
268+
def test_reindex_returns_count(self) -> None:
269+
"""Test that reindex returns a count."""
270+
from git_notes_memory import get_sync_service
271+
272+
sync = get_sync_service()
273+
count = sync.reindex(full=False)
274+
275+
assert isinstance(count, int)
276+
assert count >= 0
277+
278+
def test_verify_consistency_result_structure(self) -> None:
279+
"""Test that verify_consistency returns proper structure."""
280+
from git_notes_memory import get_sync_service
281+
282+
sync = get_sync_service()
283+
result = sync.verify_consistency()
284+
285+
# Must have attributes used by command script
286+
assert hasattr(result, "is_consistent")
287+
assert hasattr(result, "missing_in_index")
288+
assert hasattr(result, "orphaned_in_index")
289+
assert hasattr(result, "mismatched")
290+
291+
assert isinstance(result.is_consistent, bool)
292+
assert isinstance(result.missing_in_index, (list, tuple))
293+
assert isinstance(result.orphaned_in_index, (list, tuple))
294+
295+
296+
class TestConfigAPIExports:
297+
"""Tests to ensure config module exports required APIs for commands."""
298+
299+
def test_get_embedding_model_exported(self) -> None:
300+
"""Test get_embedding_model is exported and callable."""
301+
from git_notes_memory.config import get_embedding_model
302+
303+
result = get_embedding_model()
304+
assert isinstance(result, str)
305+
assert len(result) > 0
306+
307+
def test_get_index_path_exported(self) -> None:
308+
"""Test get_index_path is exported and callable."""
309+
from git_notes_memory.config import get_index_path
310+
311+
result = get_index_path()
312+
assert isinstance(result, Path)
313+
314+
def test_get_data_path_exported(self) -> None:
315+
"""Test get_data_path is exported and callable."""
316+
from git_notes_memory.config import get_data_path
317+
318+
result = get_data_path()
319+
assert isinstance(result, Path)
320+
321+
def test_namespaces_exported(self) -> None:
322+
"""Test NAMESPACES is exported and is a frozenset."""
323+
from git_notes_memory.config import NAMESPACES
324+
325+
assert isinstance(NAMESPACES, frozenset)
326+
assert "learnings" in NAMESPACES
327+
assert "decisions" in NAMESPACES
328+
329+
330+
class TestIndexServiceAPI:
331+
"""Tests to ensure IndexService has required methods for commands."""
332+
333+
def test_index_service_get_stats(self, tmp_path: Path) -> None:
334+
"""Test IndexService.get_stats returns proper structure."""
335+
from git_notes_memory.index import IndexService
336+
337+
index = IndexService(tmp_path / "test.db")
338+
index.initialize()
339+
stats = index.get_stats()
340+
341+
# Verify all attributes used by commands
342+
assert hasattr(stats, "total_memories")
343+
assert hasattr(stats, "by_namespace")
344+
assert hasattr(stats, "by_spec")
345+
assert hasattr(stats, "last_sync")
346+
assert hasattr(stats, "index_size_bytes")
347+
348+
index.close()
349+
350+
def test_index_service_initialize_required(self, tmp_path: Path) -> None:
351+
"""Test that initialize() must be called before operations."""
352+
from git_notes_memory.exceptions import MemoryIndexError
353+
from git_notes_memory.index import IndexService
354+
355+
index = IndexService(tmp_path / "test.db")
356+
357+
# Should raise without initialize
358+
with pytest.raises(MemoryIndexError):
359+
index.get_stats()
360+
361+
index.close()

0 commit comments

Comments
 (0)