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.
Files changed (190) hide show
  1. package/README.md +719 -0
  2. package/VERSION +1 -0
  3. package/docs/ANVIL-REPO-IMPLEMENTATION-PLAN.md +441 -0
  4. package/docs/FIRST-SKILL-TUTORIAL.md +408 -0
  5. package/docs/INSTALLATION-RETRO-NOTES.md +458 -0
  6. package/docs/INSTALLATION.md +984 -0
  7. package/docs/anvil-hud.md +469 -0
  8. package/docs/anvil-init.md +255 -0
  9. package/docs/anvil-state.md +210 -0
  10. package/docs/boris-cherny-ralph-wiggum-insights.md +608 -0
  11. package/docs/command-reference.md +2022 -0
  12. package/docs/hooks-tts.md +368 -0
  13. package/docs/implementation-guide.md +810 -0
  14. package/docs/linear-github-integration.md +247 -0
  15. package/docs/local-issues.md +677 -0
  16. package/docs/patterns/README.md +419 -0
  17. package/docs/planning-responsibilities.md +139 -0
  18. package/docs/session-workflow.md +573 -0
  19. package/docs/simplification-plan-template.md +297 -0
  20. package/docs/simplification-principles.md +129 -0
  21. package/docs/specifications/CCS-RALPH-INTEGRATION-DESIGN.md +633 -0
  22. package/docs/specifications/CCS-RESEARCH-REPORT.md +169 -0
  23. package/docs/specifications/PLAN-ANV-verification-ralph-wiggum.md +403 -0
  24. package/docs/specifications/PLAN-parallel-tracks-anvil-memory-ccs.md +494 -0
  25. package/docs/specifications/SPEC-ANV-VRW/component-01-verify.md +208 -0
  26. package/docs/specifications/SPEC-ANV-VRW/component-02-stop-gate.md +226 -0
  27. package/docs/specifications/SPEC-ANV-VRW/component-03-posttooluse.md +209 -0
  28. package/docs/specifications/SPEC-ANV-VRW/component-04-ralph-wiggum.md +604 -0
  29. package/docs/specifications/SPEC-ANV-VRW/component-05-atomic-actions.md +311 -0
  30. package/docs/specifications/SPEC-ANV-VRW/component-06-verify-subagent.md +264 -0
  31. package/docs/specifications/SPEC-ANV-VRW/component-07-claude-md.md +363 -0
  32. package/docs/specifications/SPEC-ANV-VRW/index.md +182 -0
  33. package/docs/specifications/SPEC-ANV-anvil-memory.md +573 -0
  34. package/docs/specifications/SPEC-ANV-context-checkpoints.md +781 -0
  35. package/docs/specifications/SPEC-ANV-verification-ralph-wiggum.md +789 -0
  36. package/docs/sync.md +122 -0
  37. package/global/CLAUDE.md +140 -0
  38. package/global/agents/verify-app.md +164 -0
  39. package/global/commands/anvil-settings.md +527 -0
  40. package/global/commands/anvil-sync.md +121 -0
  41. package/global/commands/change.md +197 -0
  42. package/global/commands/clarify.md +252 -0
  43. package/global/commands/cleanup.md +292 -0
  44. package/global/commands/commit-push-pr.md +207 -0
  45. package/global/commands/decay-review.md +127 -0
  46. package/global/commands/discover.md +158 -0
  47. package/global/commands/doc-coverage.md +122 -0
  48. package/global/commands/evidence.md +307 -0
  49. package/global/commands/explore.md +121 -0
  50. package/global/commands/force-exit.md +135 -0
  51. package/global/commands/handoff.md +191 -0
  52. package/global/commands/healthcheck.md +302 -0
  53. package/global/commands/hud.md +84 -0
  54. package/global/commands/insights.md +319 -0
  55. package/global/commands/linear-setup.md +184 -0
  56. package/global/commands/lint-fix.md +198 -0
  57. package/global/commands/orient.md +510 -0
  58. package/global/commands/plan.md +228 -0
  59. package/global/commands/ralph.md +346 -0
  60. package/global/commands/ready.md +182 -0
  61. package/global/commands/release.md +305 -0
  62. package/global/commands/retro.md +96 -0
  63. package/global/commands/shard.md +166 -0
  64. package/global/commands/spec.md +227 -0
  65. package/global/commands/sprint.md +184 -0
  66. package/global/commands/tasks.md +228 -0
  67. package/global/commands/test-and-commit.md +151 -0
  68. package/global/commands/validate.md +132 -0
  69. package/global/commands/verify.md +251 -0
  70. package/global/commands/weekly-review.md +156 -0
  71. package/global/hooks/__pycache__/ralph_context_monitor.cpython-314.pyc +0 -0
  72. package/global/hooks/__pycache__/statusline_agent_sync.cpython-314.pyc +0 -0
  73. package/global/hooks/anvil_memory_observe.ts +322 -0
  74. package/global/hooks/anvil_memory_session.ts +166 -0
  75. package/global/hooks/anvil_memory_stop.ts +187 -0
  76. package/global/hooks/parse_transcript.py +116 -0
  77. package/global/hooks/post_merge_cleanup.sh +132 -0
  78. package/global/hooks/post_tool_format.sh +215 -0
  79. package/global/hooks/ralph_context_monitor.py +240 -0
  80. package/global/hooks/ralph_stop.sh +502 -0
  81. package/global/hooks/statusline.sh +1110 -0
  82. package/global/hooks/statusline_agent_sync.py +224 -0
  83. package/global/hooks/stop_gate.sh +250 -0
  84. package/global/lib/.claude/anvil-state.json +21 -0
  85. package/global/lib/__pycache__/agent_registry.cpython-314.pyc +0 -0
  86. package/global/lib/__pycache__/claim_service.cpython-314.pyc +0 -0
  87. package/global/lib/__pycache__/coderabbit_service.cpython-314.pyc +0 -0
  88. package/global/lib/__pycache__/config_service.cpython-314.pyc +0 -0
  89. package/global/lib/__pycache__/coordination_service.cpython-314.pyc +0 -0
  90. package/global/lib/__pycache__/doc_coverage_service.cpython-314.pyc +0 -0
  91. package/global/lib/__pycache__/gate_logger.cpython-314.pyc +0 -0
  92. package/global/lib/__pycache__/github_service.cpython-314.pyc +0 -0
  93. package/global/lib/__pycache__/hygiene_service.cpython-314.pyc +0 -0
  94. package/global/lib/__pycache__/issue_models.cpython-314.pyc +0 -0
  95. package/global/lib/__pycache__/issue_provider.cpython-314.pyc +0 -0
  96. package/global/lib/__pycache__/linear_data_service.cpython-314.pyc +0 -0
  97. package/global/lib/__pycache__/linear_provider.cpython-314.pyc +0 -0
  98. package/global/lib/__pycache__/local_provider.cpython-314.pyc +0 -0
  99. package/global/lib/__pycache__/quality_service.cpython-314.pyc +0 -0
  100. package/global/lib/__pycache__/ralph_state.cpython-314.pyc +0 -0
  101. package/global/lib/__pycache__/state_manager.cpython-314.pyc +0 -0
  102. package/global/lib/__pycache__/transcript_parser.cpython-314.pyc +0 -0
  103. package/global/lib/__pycache__/verification_runner.cpython-314.pyc +0 -0
  104. package/global/lib/__pycache__/verify_iteration.cpython-314.pyc +0 -0
  105. package/global/lib/__pycache__/verify_subagent.cpython-314.pyc +0 -0
  106. package/global/lib/agent_registry.py +995 -0
  107. package/global/lib/anvil-state.sh +435 -0
  108. package/global/lib/claim_service.py +515 -0
  109. package/global/lib/coderabbit_service.py +314 -0
  110. package/global/lib/config_service.py +423 -0
  111. package/global/lib/coordination_service.py +331 -0
  112. package/global/lib/doc_coverage_service.py +1305 -0
  113. package/global/lib/gate_logger.py +316 -0
  114. package/global/lib/github_service.py +310 -0
  115. package/global/lib/handoff_generator.py +775 -0
  116. package/global/lib/hygiene_service.py +712 -0
  117. package/global/lib/issue_models.py +257 -0
  118. package/global/lib/issue_provider.py +339 -0
  119. package/global/lib/linear_data_service.py +210 -0
  120. package/global/lib/linear_provider.py +987 -0
  121. package/global/lib/linear_provider.py.backup +671 -0
  122. package/global/lib/local_provider.py +486 -0
  123. package/global/lib/orient_fast.py +457 -0
  124. package/global/lib/quality_service.py +470 -0
  125. package/global/lib/ralph_prompt_generator.py +563 -0
  126. package/global/lib/ralph_state.py +1202 -0
  127. package/global/lib/state_manager.py +417 -0
  128. package/global/lib/transcript_parser.py +597 -0
  129. package/global/lib/verification_runner.py +557 -0
  130. package/global/lib/verify_iteration.py +490 -0
  131. package/global/lib/verify_subagent.py +250 -0
  132. package/global/skills/README.md +155 -0
  133. package/global/skills/quality-gates/SKILL.md +252 -0
  134. package/global/skills/skill-template/SKILL.md +109 -0
  135. package/global/skills/testing-strategies/SKILL.md +337 -0
  136. package/global/templates/CHANGE-template.md +105 -0
  137. package/global/templates/HANDOFF-template.md +63 -0
  138. package/global/templates/PLAN-template.md +111 -0
  139. package/global/templates/SPEC-template.md +93 -0
  140. package/global/templates/ralph/PROMPT.md.template +89 -0
  141. package/global/templates/ralph/fix_plan.md.template +31 -0
  142. package/global/templates/ralph/progress.txt.template +23 -0
  143. package/global/tests/__pycache__/test_doc_coverage.cpython-314.pyc +0 -0
  144. package/global/tests/test_doc_coverage.py +520 -0
  145. package/global/tests/test_issue_models.py +299 -0
  146. package/global/tests/test_local_provider.py +323 -0
  147. package/global/tools/README.md +178 -0
  148. package/global/tools/__pycache__/anvil-hud.cpython-314.pyc +0 -0
  149. package/global/tools/anvil-hud.py +3622 -0
  150. package/global/tools/anvil-hud.py.bak +3318 -0
  151. package/global/tools/anvil-issue.py +432 -0
  152. package/global/tools/anvil-memory/CLAUDE.md +49 -0
  153. package/global/tools/anvil-memory/README.md +42 -0
  154. package/global/tools/anvil-memory/bun.lock +25 -0
  155. package/global/tools/anvil-memory/bunfig.toml +9 -0
  156. package/global/tools/anvil-memory/package.json +23 -0
  157. package/global/tools/anvil-memory/src/__tests__/ccs/context-monitor.test.ts +535 -0
  158. package/global/tools/anvil-memory/src/__tests__/ccs/edge-cases.test.ts +645 -0
  159. package/global/tools/anvil-memory/src/__tests__/ccs/fixtures.ts +363 -0
  160. package/global/tools/anvil-memory/src/__tests__/ccs/index.ts +8 -0
  161. package/global/tools/anvil-memory/src/__tests__/ccs/integration.test.ts +417 -0
  162. package/global/tools/anvil-memory/src/__tests__/ccs/prompt-generator.test.ts +571 -0
  163. package/global/tools/anvil-memory/src/__tests__/ccs/ralph-stop.test.ts +440 -0
  164. package/global/tools/anvil-memory/src/__tests__/ccs/test-utils.ts +252 -0
  165. package/global/tools/anvil-memory/src/__tests__/commands.test.ts +657 -0
  166. package/global/tools/anvil-memory/src/__tests__/db.test.ts +641 -0
  167. package/global/tools/anvil-memory/src/__tests__/hooks.test.ts +272 -0
  168. package/global/tools/anvil-memory/src/__tests__/performance.test.ts +427 -0
  169. package/global/tools/anvil-memory/src/__tests__/test-utils.ts +113 -0
  170. package/global/tools/anvil-memory/src/commands/checkpoint.ts +197 -0
  171. package/global/tools/anvil-memory/src/commands/get.ts +115 -0
  172. package/global/tools/anvil-memory/src/commands/init.ts +94 -0
  173. package/global/tools/anvil-memory/src/commands/observe.ts +163 -0
  174. package/global/tools/anvil-memory/src/commands/search.ts +112 -0
  175. package/global/tools/anvil-memory/src/db.ts +638 -0
  176. package/global/tools/anvil-memory/src/index.ts +205 -0
  177. package/global/tools/anvil-memory/src/types.ts +122 -0
  178. package/global/tools/anvil-memory/tsconfig.json +29 -0
  179. package/global/tools/ralph-loop.sh +359 -0
  180. package/package.json +45 -0
  181. package/scripts/anvil +822 -0
  182. package/scripts/extract_patterns.py +222 -0
  183. package/scripts/init-project.sh +541 -0
  184. package/scripts/install.sh +229 -0
  185. package/scripts/postinstall.js +41 -0
  186. package/scripts/rollback.sh +188 -0
  187. package/scripts/sync.sh +623 -0
  188. package/scripts/test-statusline.sh +248 -0
  189. package/scripts/update_claude_md.py +224 -0
  190. 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()