anvil-dev-framework 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +719 -0
- package/VERSION +1 -0
- package/docs/ANVIL-REPO-IMPLEMENTATION-PLAN.md +441 -0
- package/docs/FIRST-SKILL-TUTORIAL.md +408 -0
- package/docs/INSTALLATION-RETRO-NOTES.md +458 -0
- package/docs/INSTALLATION.md +984 -0
- package/docs/anvil-hud.md +469 -0
- package/docs/anvil-init.md +255 -0
- package/docs/anvil-state.md +210 -0
- package/docs/boris-cherny-ralph-wiggum-insights.md +608 -0
- package/docs/command-reference.md +2022 -0
- package/docs/hooks-tts.md +368 -0
- package/docs/implementation-guide.md +810 -0
- package/docs/linear-github-integration.md +247 -0
- package/docs/local-issues.md +677 -0
- package/docs/patterns/README.md +419 -0
- package/docs/planning-responsibilities.md +139 -0
- package/docs/session-workflow.md +573 -0
- package/docs/simplification-plan-template.md +297 -0
- package/docs/simplification-principles.md +129 -0
- package/docs/specifications/CCS-RALPH-INTEGRATION-DESIGN.md +633 -0
- package/docs/specifications/CCS-RESEARCH-REPORT.md +169 -0
- package/docs/specifications/PLAN-ANV-verification-ralph-wiggum.md +403 -0
- package/docs/specifications/PLAN-parallel-tracks-anvil-memory-ccs.md +494 -0
- package/docs/specifications/SPEC-ANV-VRW/component-01-verify.md +208 -0
- package/docs/specifications/SPEC-ANV-VRW/component-02-stop-gate.md +226 -0
- package/docs/specifications/SPEC-ANV-VRW/component-03-posttooluse.md +209 -0
- package/docs/specifications/SPEC-ANV-VRW/component-04-ralph-wiggum.md +604 -0
- package/docs/specifications/SPEC-ANV-VRW/component-05-atomic-actions.md +311 -0
- package/docs/specifications/SPEC-ANV-VRW/component-06-verify-subagent.md +264 -0
- package/docs/specifications/SPEC-ANV-VRW/component-07-claude-md.md +363 -0
- package/docs/specifications/SPEC-ANV-VRW/index.md +182 -0
- package/docs/specifications/SPEC-ANV-anvil-memory.md +573 -0
- package/docs/specifications/SPEC-ANV-context-checkpoints.md +781 -0
- package/docs/specifications/SPEC-ANV-verification-ralph-wiggum.md +789 -0
- package/docs/sync.md +122 -0
- package/global/CLAUDE.md +140 -0
- package/global/agents/verify-app.md +164 -0
- package/global/commands/anvil-settings.md +527 -0
- package/global/commands/anvil-sync.md +121 -0
- package/global/commands/change.md +197 -0
- package/global/commands/clarify.md +252 -0
- package/global/commands/cleanup.md +292 -0
- package/global/commands/commit-push-pr.md +207 -0
- package/global/commands/decay-review.md +127 -0
- package/global/commands/discover.md +158 -0
- package/global/commands/doc-coverage.md +122 -0
- package/global/commands/evidence.md +307 -0
- package/global/commands/explore.md +121 -0
- package/global/commands/force-exit.md +135 -0
- package/global/commands/handoff.md +191 -0
- package/global/commands/healthcheck.md +302 -0
- package/global/commands/hud.md +84 -0
- package/global/commands/insights.md +319 -0
- package/global/commands/linear-setup.md +184 -0
- package/global/commands/lint-fix.md +198 -0
- package/global/commands/orient.md +510 -0
- package/global/commands/plan.md +228 -0
- package/global/commands/ralph.md +346 -0
- package/global/commands/ready.md +182 -0
- package/global/commands/release.md +305 -0
- package/global/commands/retro.md +96 -0
- package/global/commands/shard.md +166 -0
- package/global/commands/spec.md +227 -0
- package/global/commands/sprint.md +184 -0
- package/global/commands/tasks.md +228 -0
- package/global/commands/test-and-commit.md +151 -0
- package/global/commands/validate.md +132 -0
- package/global/commands/verify.md +251 -0
- package/global/commands/weekly-review.md +156 -0
- package/global/hooks/__pycache__/ralph_context_monitor.cpython-314.pyc +0 -0
- package/global/hooks/__pycache__/statusline_agent_sync.cpython-314.pyc +0 -0
- package/global/hooks/anvil_memory_observe.ts +322 -0
- package/global/hooks/anvil_memory_session.ts +166 -0
- package/global/hooks/anvil_memory_stop.ts +187 -0
- package/global/hooks/parse_transcript.py +116 -0
- package/global/hooks/post_merge_cleanup.sh +132 -0
- package/global/hooks/post_tool_format.sh +215 -0
- package/global/hooks/ralph_context_monitor.py +240 -0
- package/global/hooks/ralph_stop.sh +502 -0
- package/global/hooks/statusline.sh +1110 -0
- package/global/hooks/statusline_agent_sync.py +224 -0
- package/global/hooks/stop_gate.sh +250 -0
- package/global/lib/.claude/anvil-state.json +21 -0
- package/global/lib/__pycache__/agent_registry.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/claim_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/coderabbit_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/config_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/coordination_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/doc_coverage_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/gate_logger.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/github_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/hygiene_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/issue_models.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/issue_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/linear_data_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/linear_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/local_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/quality_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/ralph_state.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/state_manager.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/transcript_parser.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/verification_runner.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/verify_iteration.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/verify_subagent.cpython-314.pyc +0 -0
- package/global/lib/agent_registry.py +995 -0
- package/global/lib/anvil-state.sh +435 -0
- package/global/lib/claim_service.py +515 -0
- package/global/lib/coderabbit_service.py +314 -0
- package/global/lib/config_service.py +423 -0
- package/global/lib/coordination_service.py +331 -0
- package/global/lib/doc_coverage_service.py +1305 -0
- package/global/lib/gate_logger.py +316 -0
- package/global/lib/github_service.py +310 -0
- package/global/lib/handoff_generator.py +775 -0
- package/global/lib/hygiene_service.py +712 -0
- package/global/lib/issue_models.py +257 -0
- package/global/lib/issue_provider.py +339 -0
- package/global/lib/linear_data_service.py +210 -0
- package/global/lib/linear_provider.py +987 -0
- package/global/lib/linear_provider.py.backup +671 -0
- package/global/lib/local_provider.py +486 -0
- package/global/lib/orient_fast.py +457 -0
- package/global/lib/quality_service.py +470 -0
- package/global/lib/ralph_prompt_generator.py +563 -0
- package/global/lib/ralph_state.py +1202 -0
- package/global/lib/state_manager.py +417 -0
- package/global/lib/transcript_parser.py +597 -0
- package/global/lib/verification_runner.py +557 -0
- package/global/lib/verify_iteration.py +490 -0
- package/global/lib/verify_subagent.py +250 -0
- package/global/skills/README.md +155 -0
- package/global/skills/quality-gates/SKILL.md +252 -0
- package/global/skills/skill-template/SKILL.md +109 -0
- package/global/skills/testing-strategies/SKILL.md +337 -0
- package/global/templates/CHANGE-template.md +105 -0
- package/global/templates/HANDOFF-template.md +63 -0
- package/global/templates/PLAN-template.md +111 -0
- package/global/templates/SPEC-template.md +93 -0
- package/global/templates/ralph/PROMPT.md.template +89 -0
- package/global/templates/ralph/fix_plan.md.template +31 -0
- package/global/templates/ralph/progress.txt.template +23 -0
- package/global/tests/__pycache__/test_doc_coverage.cpython-314.pyc +0 -0
- package/global/tests/test_doc_coverage.py +520 -0
- package/global/tests/test_issue_models.py +299 -0
- package/global/tests/test_local_provider.py +323 -0
- package/global/tools/README.md +178 -0
- package/global/tools/__pycache__/anvil-hud.cpython-314.pyc +0 -0
- package/global/tools/anvil-hud.py +3622 -0
- package/global/tools/anvil-hud.py.bak +3318 -0
- package/global/tools/anvil-issue.py +432 -0
- package/global/tools/anvil-memory/CLAUDE.md +49 -0
- package/global/tools/anvil-memory/README.md +42 -0
- package/global/tools/anvil-memory/bun.lock +25 -0
- package/global/tools/anvil-memory/bunfig.toml +9 -0
- package/global/tools/anvil-memory/package.json +23 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/context-monitor.test.ts +535 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/edge-cases.test.ts +645 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/fixtures.ts +363 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/index.ts +8 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/integration.test.ts +417 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/prompt-generator.test.ts +571 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/ralph-stop.test.ts +440 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/test-utils.ts +252 -0
- package/global/tools/anvil-memory/src/__tests__/commands.test.ts +657 -0
- package/global/tools/anvil-memory/src/__tests__/db.test.ts +641 -0
- package/global/tools/anvil-memory/src/__tests__/hooks.test.ts +272 -0
- package/global/tools/anvil-memory/src/__tests__/performance.test.ts +427 -0
- package/global/tools/anvil-memory/src/__tests__/test-utils.ts +113 -0
- package/global/tools/anvil-memory/src/commands/checkpoint.ts +197 -0
- package/global/tools/anvil-memory/src/commands/get.ts +115 -0
- package/global/tools/anvil-memory/src/commands/init.ts +94 -0
- package/global/tools/anvil-memory/src/commands/observe.ts +163 -0
- package/global/tools/anvil-memory/src/commands/search.ts +112 -0
- package/global/tools/anvil-memory/src/db.ts +638 -0
- package/global/tools/anvil-memory/src/index.ts +205 -0
- package/global/tools/anvil-memory/src/types.ts +122 -0
- package/global/tools/anvil-memory/tsconfig.json +29 -0
- package/global/tools/ralph-loop.sh +359 -0
- package/package.json +45 -0
- package/scripts/anvil +822 -0
- package/scripts/extract_patterns.py +222 -0
- package/scripts/init-project.sh +541 -0
- package/scripts/install.sh +229 -0
- package/scripts/postinstall.js +41 -0
- package/scripts/rollback.sh +188 -0
- package/scripts/sync.sh +623 -0
- package/scripts/test-statusline.sh +248 -0
- package/scripts/update_claude_md.py +224 -0
- package/scripts/verify.sh +255 -0
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
test_doc_coverage.py - Unit tests for doc_coverage_service (ANV-31)
|
|
4
|
+
|
|
5
|
+
Tests for:
|
|
6
|
+
- DocMapping pattern matching
|
|
7
|
+
- DocIndexer export extraction
|
|
8
|
+
- DocCoverageService coverage calculation
|
|
9
|
+
- Configuration loading
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
import tempfile
|
|
15
|
+
import unittest
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
# Add parent directory to path for imports
|
|
19
|
+
sys.path.insert(0, str(Path(__file__).parent.parent / "lib"))
|
|
20
|
+
|
|
21
|
+
from doc_coverage_service import (
|
|
22
|
+
DocMapping,
|
|
23
|
+
DocCoverageConfig,
|
|
24
|
+
DocExport,
|
|
25
|
+
DocGap,
|
|
26
|
+
CoverageResult,
|
|
27
|
+
DocIndexer,
|
|
28
|
+
DocCoverageService,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TestDocMapping(unittest.TestCase):
|
|
33
|
+
"""Tests for DocMapping class."""
|
|
34
|
+
|
|
35
|
+
def test_matches_glob_pattern(self):
|
|
36
|
+
"""Test that glob patterns match correctly."""
|
|
37
|
+
mapping = DocMapping(
|
|
38
|
+
source="global/lib/*.py",
|
|
39
|
+
docs="docs/api/{basename}.md",
|
|
40
|
+
doc_type="api"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
self.assertTrue(mapping.matches("global/lib/config_service.py"))
|
|
44
|
+
self.assertTrue(mapping.matches("global/lib/doc_coverage_service.py"))
|
|
45
|
+
self.assertFalse(mapping.matches("global/hooks/session_start.py"))
|
|
46
|
+
self.assertFalse(mapping.matches("src/lib/utils.py"))
|
|
47
|
+
|
|
48
|
+
def test_get_doc_path_basename(self):
|
|
49
|
+
"""Test doc path generation with basename template."""
|
|
50
|
+
mapping = DocMapping(
|
|
51
|
+
source="global/lib/*.py",
|
|
52
|
+
docs="docs/api/{basename}.md",
|
|
53
|
+
doc_type="api"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
result = mapping.get_doc_path("global/lib/config_service.py")
|
|
57
|
+
self.assertEqual(result, "docs/api/config_service.md")
|
|
58
|
+
|
|
59
|
+
def test_get_doc_path_dirname(self):
|
|
60
|
+
"""Test doc path generation with dirname template."""
|
|
61
|
+
mapping = DocMapping(
|
|
62
|
+
source="global/skills/*/skill.md",
|
|
63
|
+
docs="docs/skills/{dirname}.md",
|
|
64
|
+
doc_type="skill"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
result = mapping.get_doc_path("global/skills/linear/skill.md")
|
|
68
|
+
self.assertEqual(result, "docs/skills/linear.md")
|
|
69
|
+
|
|
70
|
+
def test_get_doc_path_filename(self):
|
|
71
|
+
"""Test doc path generation with filename template."""
|
|
72
|
+
mapping = DocMapping(
|
|
73
|
+
source=".claude/commands/*.md",
|
|
74
|
+
docs="docs/commands/{filename}",
|
|
75
|
+
doc_type="command"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
result = mapping.get_doc_path(".claude/commands/orient.md")
|
|
79
|
+
self.assertEqual(result, "docs/commands/orient.md")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class TestDocCoverageConfig(unittest.TestCase):
|
|
83
|
+
"""Tests for DocCoverageConfig class."""
|
|
84
|
+
|
|
85
|
+
def test_default_config_has_mappings(self):
|
|
86
|
+
"""Test that default config includes standard mappings."""
|
|
87
|
+
config = DocCoverageConfig.default()
|
|
88
|
+
|
|
89
|
+
self.assertTrue(config.enabled)
|
|
90
|
+
self.assertGreater(len(config.mappings), 0)
|
|
91
|
+
self.assertEqual(config.thresholds["warning"], 80)
|
|
92
|
+
self.assertEqual(config.thresholds["critical"], 60)
|
|
93
|
+
|
|
94
|
+
def test_default_config_excludes_test_files(self):
|
|
95
|
+
"""Test that default config excludes test files."""
|
|
96
|
+
config = DocCoverageConfig.default()
|
|
97
|
+
|
|
98
|
+
self.assertIn("**/test_*.py", config.exclude)
|
|
99
|
+
self.assertIn("**/__pycache__/**", config.exclude)
|
|
100
|
+
|
|
101
|
+
def test_from_file_returns_default_if_missing(self):
|
|
102
|
+
"""Test that from_file returns default if file doesn't exist."""
|
|
103
|
+
config = DocCoverageConfig.from_file("/nonexistent/path/config.yaml")
|
|
104
|
+
|
|
105
|
+
self.assertTrue(config.enabled)
|
|
106
|
+
self.assertGreater(len(config.mappings), 0)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class TestDocIndexer(unittest.TestCase):
|
|
110
|
+
"""Tests for DocIndexer class."""
|
|
111
|
+
|
|
112
|
+
def setUp(self):
|
|
113
|
+
"""Set up test fixtures."""
|
|
114
|
+
self.config = DocCoverageConfig.default()
|
|
115
|
+
self.indexer = DocIndexer(self.config)
|
|
116
|
+
|
|
117
|
+
def test_extract_python_function(self):
|
|
118
|
+
"""Test extracting function exports from Python code."""
|
|
119
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
120
|
+
f.write('''
|
|
121
|
+
def public_function(arg1: str, arg2: int = 0) -> bool:
|
|
122
|
+
"""This is a public function."""
|
|
123
|
+
return True
|
|
124
|
+
|
|
125
|
+
def _private_function():
|
|
126
|
+
"""This should be skipped."""
|
|
127
|
+
pass
|
|
128
|
+
''')
|
|
129
|
+
f.flush()
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
exports = self.indexer._extract_python_exports(f.name, "test.py")
|
|
133
|
+
|
|
134
|
+
# Should find public function but not private
|
|
135
|
+
names = [e.name for e in exports]
|
|
136
|
+
self.assertIn("public_function", names)
|
|
137
|
+
self.assertNotIn("_private_function", names)
|
|
138
|
+
|
|
139
|
+
# Check function details
|
|
140
|
+
public_fn = next(e for e in exports if e.name == "public_function")
|
|
141
|
+
self.assertEqual(public_fn.export_type, "function")
|
|
142
|
+
self.assertIsNotNone(public_fn.docstring)
|
|
143
|
+
self.assertIn("arg1", public_fn.signature)
|
|
144
|
+
finally:
|
|
145
|
+
os.unlink(f.name)
|
|
146
|
+
|
|
147
|
+
def test_extract_python_class(self):
|
|
148
|
+
"""Test extracting class exports from Python code."""
|
|
149
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
150
|
+
f.write('''
|
|
151
|
+
class PublicClass:
|
|
152
|
+
"""A public class for testing."""
|
|
153
|
+
|
|
154
|
+
def method(self):
|
|
155
|
+
pass
|
|
156
|
+
|
|
157
|
+
class _PrivateClass:
|
|
158
|
+
"""Should be skipped."""
|
|
159
|
+
pass
|
|
160
|
+
''')
|
|
161
|
+
f.flush()
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
exports = self.indexer._extract_python_exports(f.name, "test.py")
|
|
165
|
+
|
|
166
|
+
names = [e.name for e in exports]
|
|
167
|
+
self.assertIn("PublicClass", names)
|
|
168
|
+
self.assertNotIn("_PrivateClass", names)
|
|
169
|
+
|
|
170
|
+
public_cls = next(e for e in exports if e.name == "PublicClass")
|
|
171
|
+
self.assertEqual(public_cls.export_type, "class")
|
|
172
|
+
finally:
|
|
173
|
+
os.unlink(f.name)
|
|
174
|
+
|
|
175
|
+
def test_extract_python_constant(self):
|
|
176
|
+
"""Test extracting constant exports from Python code."""
|
|
177
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
178
|
+
f.write('''
|
|
179
|
+
PUBLIC_CONSTANT = "value"
|
|
180
|
+
ANOTHER_CONST = 42
|
|
181
|
+
_private_var = "hidden"
|
|
182
|
+
lowercase_var = "not exported"
|
|
183
|
+
''')
|
|
184
|
+
f.flush()
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
exports = self.indexer._extract_python_exports(f.name, "test.py")
|
|
188
|
+
|
|
189
|
+
names = [e.name for e in exports]
|
|
190
|
+
self.assertIn("PUBLIC_CONSTANT", names)
|
|
191
|
+
self.assertIn("ANOTHER_CONST", names)
|
|
192
|
+
self.assertNotIn("_private_var", names)
|
|
193
|
+
self.assertNotIn("lowercase_var", names)
|
|
194
|
+
finally:
|
|
195
|
+
os.unlink(f.name)
|
|
196
|
+
|
|
197
|
+
def test_is_excluded(self):
|
|
198
|
+
"""Test file exclusion patterns."""
|
|
199
|
+
self.assertTrue(self.indexer._is_excluded("tests/test_something.py"))
|
|
200
|
+
self.assertTrue(self.indexer._is_excluded("dir/__pycache__/module.py"))
|
|
201
|
+
self.assertTrue(self.indexer._is_excluded("node_modules/package/index.js"))
|
|
202
|
+
self.assertFalse(self.indexer._is_excluded("global/lib/service.py"))
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class TestCoverageResult(unittest.TestCase):
|
|
206
|
+
"""Tests for CoverageResult class."""
|
|
207
|
+
|
|
208
|
+
def test_status_healthy(self):
|
|
209
|
+
"""Test healthy status for high coverage."""
|
|
210
|
+
result = CoverageResult(
|
|
211
|
+
total_exports=100,
|
|
212
|
+
documented_exports=85,
|
|
213
|
+
coverage_percent=85.0,
|
|
214
|
+
gaps=[],
|
|
215
|
+
by_type={}
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
self.assertEqual(result.status, "healthy")
|
|
219
|
+
self.assertEqual(result.status_emoji, "✅")
|
|
220
|
+
|
|
221
|
+
def test_status_warning(self):
|
|
222
|
+
"""Test warning status for medium coverage."""
|
|
223
|
+
result = CoverageResult(
|
|
224
|
+
total_exports=100,
|
|
225
|
+
documented_exports=70,
|
|
226
|
+
coverage_percent=70.0,
|
|
227
|
+
gaps=[],
|
|
228
|
+
by_type={}
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
self.assertEqual(result.status, "warning")
|
|
232
|
+
self.assertEqual(result.status_emoji, "⚠️")
|
|
233
|
+
|
|
234
|
+
def test_status_critical(self):
|
|
235
|
+
"""Test critical status for low coverage."""
|
|
236
|
+
result = CoverageResult(
|
|
237
|
+
total_exports=100,
|
|
238
|
+
documented_exports=50,
|
|
239
|
+
coverage_percent=50.0,
|
|
240
|
+
gaps=[],
|
|
241
|
+
by_type={}
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
self.assertEqual(result.status, "critical")
|
|
245
|
+
self.assertEqual(result.status_emoji, "❌")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class TestDocCoverageService(unittest.TestCase):
|
|
249
|
+
"""Tests for DocCoverageService class."""
|
|
250
|
+
|
|
251
|
+
def test_calculate_coverage_returns_result(self):
|
|
252
|
+
"""Test that calculate_coverage returns a CoverageResult."""
|
|
253
|
+
# Use current project directory
|
|
254
|
+
service = DocCoverageService(project_path=".")
|
|
255
|
+
result = service.calculate_coverage()
|
|
256
|
+
|
|
257
|
+
self.assertIsInstance(result, CoverageResult)
|
|
258
|
+
self.assertGreaterEqual(result.total_exports, 0)
|
|
259
|
+
self.assertGreaterEqual(result.coverage_percent, 0)
|
|
260
|
+
self.assertLessEqual(result.coverage_percent, 100)
|
|
261
|
+
|
|
262
|
+
def test_check_threshold(self):
|
|
263
|
+
"""Test check method with thresholds."""
|
|
264
|
+
service = DocCoverageService(project_path=".")
|
|
265
|
+
|
|
266
|
+
# This should work regardless of actual coverage
|
|
267
|
+
result_low = service.check(threshold=0)
|
|
268
|
+
self.assertTrue(result_low)
|
|
269
|
+
|
|
270
|
+
# High threshold check - result depends on actual coverage
|
|
271
|
+
_ = service.check(threshold=100)
|
|
272
|
+
|
|
273
|
+
def test_generate_report_markdown(self):
|
|
274
|
+
"""Test markdown report generation."""
|
|
275
|
+
service = DocCoverageService(project_path=".")
|
|
276
|
+
report = service.generate_report(output_format="markdown")
|
|
277
|
+
|
|
278
|
+
self.assertIn("# Documentation Coverage Report", report)
|
|
279
|
+
self.assertIn("## Summary", report)
|
|
280
|
+
self.assertIn("Coverage", report)
|
|
281
|
+
|
|
282
|
+
def test_generate_report_json(self):
|
|
283
|
+
"""Test JSON report generation."""
|
|
284
|
+
import json
|
|
285
|
+
|
|
286
|
+
service = DocCoverageService(project_path=".")
|
|
287
|
+
report = service.generate_report(output_format="json")
|
|
288
|
+
|
|
289
|
+
# Should be valid JSON
|
|
290
|
+
data = json.loads(report)
|
|
291
|
+
self.assertIn("total_exports", data)
|
|
292
|
+
self.assertIn("coverage_percent", data)
|
|
293
|
+
self.assertIn("status", data)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class TestDocExport(unittest.TestCase):
|
|
297
|
+
"""Tests for DocExport class."""
|
|
298
|
+
|
|
299
|
+
def test_qualified_name(self):
|
|
300
|
+
"""Test qualified name generation."""
|
|
301
|
+
export = DocExport(
|
|
302
|
+
name="my_function",
|
|
303
|
+
export_type="function",
|
|
304
|
+
source_file="path/to/module.py",
|
|
305
|
+
line_number=42
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
self.assertEqual(export.qualified_name, "path/to/module.py:my_function")
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
# =============================================================================
|
|
312
|
+
# Gap Detection Tests (ANV-218)
|
|
313
|
+
# =============================================================================
|
|
314
|
+
|
|
315
|
+
# Import gap detection classes
|
|
316
|
+
from doc_coverage_service import (
|
|
317
|
+
StaleDocWarning,
|
|
318
|
+
GapDetectionResult,
|
|
319
|
+
GapDetector,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class TestStaleDocWarning(unittest.TestCase):
|
|
324
|
+
"""Tests for StaleDocWarning dataclass."""
|
|
325
|
+
|
|
326
|
+
def test_message_property(self):
|
|
327
|
+
"""Test human-readable warning message generation."""
|
|
328
|
+
warning = StaleDocWarning(
|
|
329
|
+
source_file="global/lib/service.py",
|
|
330
|
+
doc_file="docs/api/service.md",
|
|
331
|
+
source_mtime=1700000000.0,
|
|
332
|
+
doc_mtime=1699000000.0,
|
|
333
|
+
days_stale=12,
|
|
334
|
+
severity="warning"
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
message = warning.message
|
|
338
|
+
self.assertIn("service.md", message)
|
|
339
|
+
self.assertIn("12 days", message)
|
|
340
|
+
self.assertIn("stale", message.lower())
|
|
341
|
+
|
|
342
|
+
def test_severity_levels(self):
|
|
343
|
+
"""Test different severity levels."""
|
|
344
|
+
for severity in ["info", "warning", "critical"]:
|
|
345
|
+
warning = StaleDocWarning(
|
|
346
|
+
source_file="test.py",
|
|
347
|
+
doc_file="test.md",
|
|
348
|
+
source_mtime=1700000000.0,
|
|
349
|
+
doc_mtime=1699000000.0,
|
|
350
|
+
days_stale=5,
|
|
351
|
+
severity=severity
|
|
352
|
+
)
|
|
353
|
+
self.assertEqual(warning.severity, severity)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
class TestGapDetectionResult(unittest.TestCase):
|
|
357
|
+
"""Tests for GapDetectionResult dataclass."""
|
|
358
|
+
|
|
359
|
+
def test_to_context_block_with_warnings(self):
|
|
360
|
+
"""Test context block generation with warnings."""
|
|
361
|
+
warnings = [
|
|
362
|
+
StaleDocWarning(
|
|
363
|
+
source_file="global/lib/service.py",
|
|
364
|
+
doc_file="docs/api/service.md",
|
|
365
|
+
source_mtime=1700000000.0,
|
|
366
|
+
doc_mtime=1699000000.0,
|
|
367
|
+
days_stale=7,
|
|
368
|
+
severity="warning"
|
|
369
|
+
)
|
|
370
|
+
]
|
|
371
|
+
result = GapDetectionResult(
|
|
372
|
+
stale_docs=warnings,
|
|
373
|
+
changed_files_without_docs=["global/lib/new_module.py"],
|
|
374
|
+
total_warnings=2,
|
|
375
|
+
sensitivity="balanced"
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
context = result.to_context_block()
|
|
379
|
+
|
|
380
|
+
# Should include header
|
|
381
|
+
self.assertIn("Documentation Coverage Warnings", context)
|
|
382
|
+
# Should include stale doc warning
|
|
383
|
+
self.assertIn("service.md", context)
|
|
384
|
+
# Should include changed file without docs
|
|
385
|
+
self.assertIn("new_module.py", context)
|
|
386
|
+
|
|
387
|
+
def test_to_context_block_empty(self):
|
|
388
|
+
"""Test context block is empty when no warnings."""
|
|
389
|
+
result = GapDetectionResult(
|
|
390
|
+
stale_docs=[],
|
|
391
|
+
changed_files_without_docs=[],
|
|
392
|
+
total_warnings=0,
|
|
393
|
+
sensitivity="balanced"
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
context = result.to_context_block()
|
|
397
|
+
self.assertEqual(context, "")
|
|
398
|
+
|
|
399
|
+
def test_total_warnings_count(self):
|
|
400
|
+
"""Test total warnings includes both stale and changed."""
|
|
401
|
+
warnings = [
|
|
402
|
+
StaleDocWarning(
|
|
403
|
+
source_file="a.py",
|
|
404
|
+
doc_file="a.md",
|
|
405
|
+
source_mtime=1700000000.0,
|
|
406
|
+
doc_mtime=1699000000.0,
|
|
407
|
+
days_stale=5,
|
|
408
|
+
severity="info"
|
|
409
|
+
)
|
|
410
|
+
]
|
|
411
|
+
result = GapDetectionResult(
|
|
412
|
+
stale_docs=warnings,
|
|
413
|
+
changed_files_without_docs=["b.py", "c.py"],
|
|
414
|
+
total_warnings=3,
|
|
415
|
+
sensitivity="balanced"
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
self.assertEqual(result.total_warnings, 3)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
class TestGapDetector(unittest.TestCase):
|
|
422
|
+
"""Tests for GapDetector class."""
|
|
423
|
+
|
|
424
|
+
def setUp(self):
|
|
425
|
+
"""Set up test fixtures."""
|
|
426
|
+
self.config = DocCoverageConfig.default()
|
|
427
|
+
|
|
428
|
+
def test_sensitivity_thresholds(self):
|
|
429
|
+
"""Test sensitivity threshold configuration."""
|
|
430
|
+
# Aggressive - 0 days threshold
|
|
431
|
+
detector = GapDetector(self.config, sensitivity="aggressive")
|
|
432
|
+
self.assertEqual(detector.sensitivity, "aggressive")
|
|
433
|
+
self.assertEqual(GapDetector.SENSITIVITY_THRESHOLDS["aggressive"], 0)
|
|
434
|
+
|
|
435
|
+
# Balanced - 7 days threshold
|
|
436
|
+
detector = GapDetector(self.config, sensitivity="balanced")
|
|
437
|
+
self.assertEqual(GapDetector.SENSITIVITY_THRESHOLDS["balanced"], 7)
|
|
438
|
+
|
|
439
|
+
# Quiet - 30 days threshold
|
|
440
|
+
detector = GapDetector(self.config, sensitivity="quiet")
|
|
441
|
+
self.assertEqual(GapDetector.SENSITIVITY_THRESHOLDS["quiet"], 30)
|
|
442
|
+
|
|
443
|
+
def test_analyze_returns_result(self):
|
|
444
|
+
"""Test that analyze returns a GapDetectionResult."""
|
|
445
|
+
detector = GapDetector(self.config, sensitivity="quiet")
|
|
446
|
+
result = detector.analyze(".")
|
|
447
|
+
|
|
448
|
+
self.assertIsInstance(result, GapDetectionResult)
|
|
449
|
+
self.assertEqual(result.sensitivity, "quiet")
|
|
450
|
+
self.assertIsInstance(result.stale_docs, list)
|
|
451
|
+
self.assertIsInstance(result.changed_files_without_docs, list)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
class TestGapDetectionService(unittest.TestCase):
|
|
455
|
+
"""Tests for gap detection service methods."""
|
|
456
|
+
|
|
457
|
+
def test_detect_stale_docs_returns_result(self):
|
|
458
|
+
"""Test detect_stale_docs returns a GapDetectionResult."""
|
|
459
|
+
service = DocCoverageService(project_path=".")
|
|
460
|
+
result = service.detect_stale_docs()
|
|
461
|
+
|
|
462
|
+
self.assertIsInstance(result, GapDetectionResult)
|
|
463
|
+
|
|
464
|
+
def test_detect_stale_docs_respects_sensitivity(self):
|
|
465
|
+
"""Test detect_stale_docs uses provided sensitivity."""
|
|
466
|
+
service = DocCoverageService(project_path=".")
|
|
467
|
+
|
|
468
|
+
result_quiet = service.detect_stale_docs(sensitivity="quiet")
|
|
469
|
+
self.assertEqual(result_quiet.sensitivity, "quiet")
|
|
470
|
+
|
|
471
|
+
result_aggressive = service.detect_stale_docs(sensitivity="aggressive")
|
|
472
|
+
self.assertEqual(result_aggressive.sensitivity, "aggressive")
|
|
473
|
+
|
|
474
|
+
def test_detect_stale_docs_disabled_via_config(self):
|
|
475
|
+
"""Test that gap detection returns empty when disabled."""
|
|
476
|
+
config = DocCoverageConfig.default()
|
|
477
|
+
config.gap_detection_enabled = False
|
|
478
|
+
|
|
479
|
+
service = DocCoverageService(project_path=".", config=config)
|
|
480
|
+
result = service.detect_stale_docs()
|
|
481
|
+
|
|
482
|
+
# Should return empty result when disabled
|
|
483
|
+
self.assertEqual(result.total_warnings, 0)
|
|
484
|
+
self.assertEqual(len(result.stale_docs), 0)
|
|
485
|
+
self.assertEqual(len(result.changed_files_without_docs), 0)
|
|
486
|
+
|
|
487
|
+
def test_get_session_warnings_returns_string(self):
|
|
488
|
+
"""Test get_session_warnings returns a string."""
|
|
489
|
+
service = DocCoverageService(project_path=".")
|
|
490
|
+
warnings = service.get_session_warnings()
|
|
491
|
+
|
|
492
|
+
self.assertIsInstance(warnings, str)
|
|
493
|
+
|
|
494
|
+
def test_get_session_warnings_empty_when_disabled(self):
|
|
495
|
+
"""Test get_session_warnings returns empty when disabled."""
|
|
496
|
+
config = DocCoverageConfig.default()
|
|
497
|
+
config.gap_detection_enabled = False
|
|
498
|
+
|
|
499
|
+
service = DocCoverageService(project_path=".", config=config)
|
|
500
|
+
warnings = service.get_session_warnings()
|
|
501
|
+
|
|
502
|
+
self.assertEqual(warnings, "")
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
class TestGapDetectionConfig(unittest.TestCase):
|
|
506
|
+
"""Tests for gap detection configuration."""
|
|
507
|
+
|
|
508
|
+
def test_default_config_has_gap_detection_enabled(self):
|
|
509
|
+
"""Test default config has gap detection enabled."""
|
|
510
|
+
config = DocCoverageConfig.default()
|
|
511
|
+
self.assertTrue(config.gap_detection_enabled)
|
|
512
|
+
|
|
513
|
+
def test_default_config_has_sensitivity(self):
|
|
514
|
+
"""Test default config has sensitivity setting."""
|
|
515
|
+
config = DocCoverageConfig.default()
|
|
516
|
+
self.assertEqual(config.sensitivity, "balanced")
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
if __name__ == "__main__":
|
|
520
|
+
unittest.main()
|