devforgeai 1.0.5 → 1.0.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 (131) hide show
  1. package/CLAUDE.md +120 -0
  2. package/package.json +9 -1
  3. package/src/CLAUDE.md +699 -0
  4. package/src/claude/scripts/README.md +396 -0
  5. package/src/claude/scripts/audit-command-skill-overlap.sh +67 -0
  6. package/src/claude/scripts/check-hooks-fast.sh +70 -0
  7. package/src/claude/scripts/devforgeai-validate +6 -0
  8. package/src/claude/scripts/devforgeai_cli/README.md +531 -0
  9. package/src/claude/scripts/devforgeai_cli/__init__.py +12 -0
  10. package/src/claude/scripts/devforgeai_cli/cli.py +716 -0
  11. package/src/claude/scripts/devforgeai_cli/commands/__init__.py +1 -0
  12. package/src/claude/scripts/devforgeai_cli/commands/check_hooks.py +384 -0
  13. package/src/claude/scripts/devforgeai_cli/commands/invoke_hooks.py +149 -0
  14. package/src/claude/scripts/devforgeai_cli/commands/phase_commands.py +731 -0
  15. package/src/claude/scripts/devforgeai_cli/commands/validate_installation.py +412 -0
  16. package/src/claude/scripts/devforgeai_cli/context_extraction.py +426 -0
  17. package/src/claude/scripts/devforgeai_cli/feedback/AC_TO_TEST_MAPPING.md +636 -0
  18. package/src/claude/scripts/devforgeai_cli/feedback/DELIVERY_SUMMARY.txt +329 -0
  19. package/src/claude/scripts/devforgeai_cli/feedback/README_TEST_SPECS.md +486 -0
  20. package/src/claude/scripts/devforgeai_cli/feedback/TEST_IMPLEMENTATION_GUIDE.md +529 -0
  21. package/src/claude/scripts/devforgeai_cli/feedback/TEST_SPECIFICATIONS.md +2652 -0
  22. package/src/claude/scripts/devforgeai_cli/feedback/TEST_SPECS_INDEX.md +398 -0
  23. package/src/claude/scripts/devforgeai_cli/feedback/__init__.py +34 -0
  24. package/src/claude/scripts/devforgeai_cli/feedback/adaptive_questioning_engine.py +581 -0
  25. package/src/claude/scripts/devforgeai_cli/feedback/aggregation.py +179 -0
  26. package/src/claude/scripts/devforgeai_cli/feedback/commands.py +535 -0
  27. package/src/claude/scripts/devforgeai_cli/feedback/config_defaults.py +58 -0
  28. package/src/claude/scripts/devforgeai_cli/feedback/config_manager.py +423 -0
  29. package/src/claude/scripts/devforgeai_cli/feedback/config_models.py +192 -0
  30. package/src/claude/scripts/devforgeai_cli/feedback/config_schema.py +140 -0
  31. package/src/claude/scripts/devforgeai_cli/feedback/coverage.json +1 -0
  32. package/src/claude/scripts/devforgeai_cli/feedback/feature_flag.py +152 -0
  33. package/src/claude/scripts/devforgeai_cli/feedback/feedback_indexer.py +394 -0
  34. package/src/claude/scripts/devforgeai_cli/feedback/hot_reload.py +226 -0
  35. package/src/claude/scripts/devforgeai_cli/feedback/longitudinal.py +115 -0
  36. package/src/claude/scripts/devforgeai_cli/feedback/models.py +67 -0
  37. package/src/claude/scripts/devforgeai_cli/feedback/question_router.py +236 -0
  38. package/src/claude/scripts/devforgeai_cli/feedback/retrospective.py +233 -0
  39. package/src/claude/scripts/devforgeai_cli/feedback/skip_tracker.py +177 -0
  40. package/src/claude/scripts/devforgeai_cli/feedback/skip_tracking.py +221 -0
  41. package/src/claude/scripts/devforgeai_cli/feedback/template_engine.py +549 -0
  42. package/src/claude/scripts/devforgeai_cli/feedback/validation.py +163 -0
  43. package/src/claude/scripts/devforgeai_cli/headless/__init__.py +30 -0
  44. package/src/claude/scripts/devforgeai_cli/headless/answer_models.py +206 -0
  45. package/src/claude/scripts/devforgeai_cli/headless/answer_resolver.py +204 -0
  46. package/src/claude/scripts/devforgeai_cli/headless/exceptions.py +36 -0
  47. package/src/claude/scripts/devforgeai_cli/headless/pattern_matcher.py +156 -0
  48. package/src/claude/scripts/devforgeai_cli/hooks.py +313 -0
  49. package/src/claude/scripts/devforgeai_cli/metrics/__init__.py +46 -0
  50. package/src/claude/scripts/devforgeai_cli/metrics/command_metrics.py +142 -0
  51. package/src/claude/scripts/devforgeai_cli/metrics/failure_modes.py +152 -0
  52. package/src/claude/scripts/devforgeai_cli/metrics/story_segmentation.py +181 -0
  53. package/src/claude/scripts/devforgeai_cli/orchestrate_hooks.py +780 -0
  54. package/src/claude/scripts/devforgeai_cli/phase_state.py +1229 -0
  55. package/src/claude/scripts/devforgeai_cli/session/__init__.py +30 -0
  56. package/src/claude/scripts/devforgeai_cli/session/checkpoint.py +268 -0
  57. package/src/claude/scripts/devforgeai_cli/tests/__init__.py +1 -0
  58. package/src/claude/scripts/devforgeai_cli/tests/conftest.py +29 -0
  59. package/src/claude/scripts/devforgeai_cli/tests/feedback/TEST_EXECUTION_GUIDE.md +298 -0
  60. package/src/claude/scripts/devforgeai_cli/tests/feedback/__init__.py +3 -0
  61. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_adaptive_questioning_engine.py +2171 -0
  62. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_aggregation.py +476 -0
  63. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_defaults.py +133 -0
  64. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_manager.py +592 -0
  65. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_models.py +373 -0
  66. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_schema.py +130 -0
  67. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_configuration_management.py +1355 -0
  68. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_edge_cases.py +308 -0
  69. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_feature_flag.py +307 -0
  70. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_feedback_indexer.py +384 -0
  71. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_hot_reload.py +580 -0
  72. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_integration.py +402 -0
  73. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_models.py +105 -0
  74. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_question_routing.py +262 -0
  75. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_retrospective.py +333 -0
  76. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracker.py +410 -0
  77. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracking.py +159 -0
  78. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracking_integration.py +1155 -0
  79. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_template_engine.py +1389 -0
  80. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_validation_comprehensive.py +210 -0
  81. package/src/claude/scripts/devforgeai_cli/tests/fixtures/autonomous-deferral-story.md +46 -0
  82. package/src/claude/scripts/devforgeai_cli/tests/fixtures/missing-impl-notes.md +31 -0
  83. package/src/claude/scripts/devforgeai_cli/tests/fixtures/valid-deferral-story.md +46 -0
  84. package/src/claude/scripts/devforgeai_cli/tests/fixtures/valid-story-complete.md +48 -0
  85. package/src/claude/scripts/devforgeai_cli/tests/manual_test_invoke_hooks.sh +200 -0
  86. package/src/claude/scripts/devforgeai_cli/tests/session/DELIVERABLES.md +518 -0
  87. package/src/claude/scripts/devforgeai_cli/tests/session/TEST_SUMMARY.md +468 -0
  88. package/src/claude/scripts/devforgeai_cli/tests/session/__init__.py +6 -0
  89. package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/corrupted-checkpoint.json +1 -0
  90. package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/missing-fields-checkpoint.json +4 -0
  91. package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/valid-checkpoint.json +15 -0
  92. package/src/claude/scripts/devforgeai_cli/tests/session/test_checkpoint.py +851 -0
  93. package/src/claude/scripts/devforgeai_cli/tests/test_check_hooks.py +1886 -0
  94. package/src/claude/scripts/devforgeai_cli/tests/test_depends_on_normalizer.py +171 -0
  95. package/src/claude/scripts/devforgeai_cli/tests/test_dod_validator.py +97 -0
  96. package/src/claude/scripts/devforgeai_cli/tests/test_invoke_hooks.py +1902 -0
  97. package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands.py +320 -0
  98. package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands_error_handling.py +1021 -0
  99. package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands_import.py +697 -0
  100. package/src/claude/scripts/devforgeai_cli/tests/test_phase_state.py +2187 -0
  101. package/src/claude/scripts/devforgeai_cli/tests/test_skip_tracking.py +2141 -0
  102. package/src/claude/scripts/devforgeai_cli/tests/test_skip_tracking_coverage_gap.py +195 -0
  103. package/src/claude/scripts/devforgeai_cli/tests/test_subagent_enforcement.py +539 -0
  104. package/src/claude/scripts/devforgeai_cli/tests/test_validate_installation.py +361 -0
  105. package/src/claude/scripts/devforgeai_cli/utils/__init__.py +11 -0
  106. package/src/claude/scripts/devforgeai_cli/utils/depends_on_normalizer.py +149 -0
  107. package/src/claude/scripts/devforgeai_cli/utils/markdown_parser.py +219 -0
  108. package/src/claude/scripts/devforgeai_cli/utils/story_analyzer.py +249 -0
  109. package/src/claude/scripts/devforgeai_cli/utils/yaml_parser.py +152 -0
  110. package/src/claude/scripts/devforgeai_cli/validators/__init__.py +27 -0
  111. package/src/claude/scripts/devforgeai_cli/validators/ast_grep_validator.py +373 -0
  112. package/src/claude/scripts/devforgeai_cli/validators/context_validator.py +180 -0
  113. package/src/claude/scripts/devforgeai_cli/validators/dod_validator.py +309 -0
  114. package/src/claude/scripts/devforgeai_cli/validators/git_validator.py +107 -0
  115. package/src/claude/scripts/devforgeai_cli/validators/grep_fallback.py +300 -0
  116. package/src/claude/scripts/install_hooks.sh +186 -0
  117. package/src/claude/scripts/invoke_feedback_hooks.sh +59 -0
  118. package/src/claude/scripts/migrate-ac-headers.sh +122 -0
  119. package/src/claude/scripts/plan_file_kb.sh +704 -0
  120. package/src/claude/scripts/requirements.txt +8 -0
  121. package/src/claude/scripts/session_catalog.sh +543 -0
  122. package/src/claude/scripts/setup.py +55 -0
  123. package/src/claude/scripts/start-devforgeai.sh +16 -0
  124. package/src/claude/scripts/statusline.sh +27 -0
  125. package/src/claude/scripts/validate_deferrals.py +344 -0
  126. package/src/claude/skills/designing-systems/scripts/__pycache__/detect_anti_patterns.cpython-312.pyc +0 -0
  127. package/src/claude/skills/designing-systems/scripts/__pycache__/validate_all_context.cpython-312.pyc +0 -0
  128. package/src/claude/skills/designing-systems/scripts/__pycache__/validate_architecture.cpython-312.pyc +0 -0
  129. package/src/claude/skills/designing-systems/scripts/__pycache__/validate_dependencies.cpython-312.pyc +0 -0
  130. package/src/claude/skills/devforgeai-story-creation/scripts/__pycache__/migrate_story_v1_to_v2.cpython-312.pyc +0 -0
  131. package/src/claude/skills/devforgeai-story-creation/scripts/tests/__pycache__/measure_accuracy.cpython-312.pyc +0 -0
@@ -0,0 +1,476 @@
1
+ """
2
+ Unit tests for feedback aggregation and longitudinal tracking (AC4, AC6)
3
+
4
+ Tests cover:
5
+ - AC4: Feedback data aggregation for framework maintainers
6
+ - AC6: Longitudinal feedback tracking over time
7
+ - Pattern detection (80%+ threshold)
8
+ - Actionable insights generation
9
+ """
10
+
11
+ import pytest
12
+ import json
13
+ import tempfile
14
+ import shutil
15
+ from pathlib import Path
16
+ from datetime import datetime, timedelta, timezone
17
+
18
+ from devforgeai_cli.feedback.aggregation import (
19
+ aggregate_feedback_by_story,
20
+ aggregate_feedback_by_epic,
21
+ aggregate_feedback_by_skill,
22
+ detect_patterns,
23
+ generate_insights,
24
+ export_quarterly_insights,
25
+ )
26
+ from devforgeai_cli.feedback.longitudinal import (
27
+ correlate_feedback_across_stories,
28
+ identify_improvement_trajectories,
29
+ export_personal_journal,
30
+ )
31
+
32
+
33
+ class TestFeedbackAggregation:
34
+ """AC4: Feedback Data Aggregation for Framework Maintainers"""
35
+
36
+ @pytest.fixture
37
+ def temp_feedback_dir(self):
38
+ """Create temporary feedback directory with sample data"""
39
+ temp_dir = tempfile.mkdtemp()
40
+ feedback_dir = Path(temp_dir)
41
+
42
+ # Create sample feedback files
43
+ self._create_sample_feedback(feedback_dir, 'STORY-001', 'EPIC-001', 'dev', [
44
+ {'question_id': 'dev_success_01', 'response': 4},
45
+ {'question_id': 'dev_success_02', 'response': 'Documentation unclear'},
46
+ ])
47
+ self._create_sample_feedback(feedback_dir, 'STORY-002', 'EPIC-001', 'dev', [
48
+ {'question_id': 'dev_success_01', 'response': 3},
49
+ {'question_id': 'dev_success_02', 'response': 'Documentation unclear'},
50
+ ])
51
+ self._create_sample_feedback(feedback_dir, 'STORY-003', 'EPIC-002', 'qa', [
52
+ {'question_id': 'qa_success_01', 'response': 5},
53
+ {'question_id': 'qa_success_02', 'response': 'Coverage metrics great'},
54
+ ])
55
+
56
+ yield feedback_dir
57
+ shutil.rmtree(temp_dir)
58
+
59
+ def _create_sample_feedback(self, feedback_dir, story_id, epic_id, workflow_type, questions):
60
+ """Helper to create sample feedback JSON files"""
61
+ story_dir = feedback_dir / story_id
62
+ story_dir.mkdir(parents=True, exist_ok=True)
63
+
64
+ feedback_data = {
65
+ 'feedback_id': f'fb-{story_id}',
66
+ 'timestamp': datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z'),
67
+ 'story_id': story_id,
68
+ 'epic_id': epic_id,
69
+ 'workflow_type': workflow_type,
70
+ 'success_status': 'success',
71
+ 'questions': questions,
72
+ 'metadata': {
73
+ 'total_questions': len(questions),
74
+ 'answered': len(questions),
75
+ 'skipped': 0,
76
+ }
77
+ }
78
+
79
+ filename = f"{feedback_data['timestamp'][:10]}-retrospective.json"
80
+ with open(story_dir / filename, 'w') as f:
81
+ json.dump(feedback_data, f, indent=2)
82
+
83
+ def test_aggregate_feedback_by_story_groups_correctly(self, temp_feedback_dir):
84
+ """
85
+ GIVEN multiple feedback sessions across different stories
86
+ WHEN aggregate_feedback_by_story is called
87
+ THEN feedback is grouped by story_id
88
+ """
89
+ # Act
90
+ aggregated = aggregate_feedback_by_story(temp_feedback_dir)
91
+
92
+ # Assert
93
+ assert 'STORY-001' in aggregated
94
+ assert 'STORY-002' in aggregated
95
+ assert 'STORY-003' in aggregated
96
+ assert len(aggregated['STORY-001']) >= 1
97
+ assert len(aggregated['STORY-002']) >= 1
98
+
99
+ def test_aggregate_feedback_by_epic_groups_correctly(self, temp_feedback_dir):
100
+ """
101
+ GIVEN feedback from stories in different epics
102
+ WHEN aggregate_feedback_by_epic is called
103
+ THEN feedback is grouped by epic_id
104
+ """
105
+ # Act
106
+ aggregated = aggregate_feedback_by_epic(temp_feedback_dir)
107
+
108
+ # Assert
109
+ assert 'EPIC-001' in aggregated
110
+ assert 'EPIC-002' in aggregated
111
+ assert len(aggregated['EPIC-001']) == 2 # STORY-001 and STORY-002
112
+ assert len(aggregated['EPIC-002']) == 1 # STORY-003
113
+
114
+ def test_aggregate_feedback_by_skill_groups_correctly(self, temp_feedback_dir):
115
+ """
116
+ GIVEN feedback from different workflows (dev, qa, orchestrate)
117
+ WHEN aggregate_feedback_by_skill is called
118
+ THEN feedback is grouped by workflow_type
119
+ """
120
+ # Act
121
+ aggregated = aggregate_feedback_by_skill(temp_feedback_dir)
122
+
123
+ # Assert
124
+ assert 'dev' in aggregated
125
+ assert 'qa' in aggregated
126
+ assert len(aggregated['dev']) == 2 # STORY-001, STORY-002
127
+ assert len(aggregated['qa']) == 1 # STORY-003
128
+
129
+ def test_detect_patterns_identifies_80_percent_threshold(self, temp_feedback_dir):
130
+ """
131
+ GIVEN 80%+ of users report same issue
132
+ WHEN detect_patterns is called
133
+ THEN it flags the issue as high priority
134
+ """
135
+ # Arrange - Create more feedback with consistent issue
136
+ for i in range(4, 10): # Add 6 more feedbacks, total 8 with "Documentation unclear"
137
+ self._create_sample_feedback(
138
+ temp_feedback_dir,
139
+ f'STORY-{i:03d}',
140
+ 'EPIC-001',
141
+ 'dev',
142
+ [
143
+ {'question_id': 'dev_success_01', 'response': 4},
144
+ {'question_id': 'dev_success_02', 'response': 'Documentation unclear'},
145
+ ]
146
+ )
147
+
148
+ # Act
149
+ patterns = detect_patterns(temp_feedback_dir, threshold=0.8)
150
+
151
+ # Assert
152
+ assert len(patterns) > 0
153
+
154
+ # Should identify "Documentation unclear" pattern
155
+ doc_pattern = next((p for p in patterns if 'documentation' in p['issue'].lower()), None)
156
+ assert doc_pattern is not None
157
+ assert doc_pattern['frequency'] >= 0.8 # 80%+ reported this
158
+ assert doc_pattern['priority'] == 'high'
159
+
160
+ def test_generate_insights_produces_actionable_recommendations(self, temp_feedback_dir):
161
+ """
162
+ GIVEN detected patterns
163
+ WHEN generate_insights is called
164
+ THEN it produces actionable recommendations with vote counts
165
+ """
166
+ # Act
167
+ insights = generate_insights(temp_feedback_dir)
168
+
169
+ # Assert
170
+ assert insights is not None
171
+ assert 'recommendations' in insights
172
+ assert len(insights['recommendations']) > 0
173
+
174
+ # Each recommendation should have vote count
175
+ for rec in insights['recommendations']:
176
+ assert 'issue' in rec
177
+ assert 'vote_count' in rec
178
+ assert 'percentage' in rec
179
+ assert 'suggested_action' in rec
180
+
181
+ def test_export_quarterly_insights_creates_markdown_file(self, temp_feedback_dir):
182
+ """
183
+ GIVEN aggregated feedback
184
+ WHEN export_quarterly_insights is called
185
+ THEN it creates devforgeai/feedback/quarterly-insights.md
186
+ """
187
+ # Act
188
+ output_path = export_quarterly_insights(temp_feedback_dir)
189
+
190
+ # Assert
191
+ assert output_path.exists()
192
+ assert output_path.name == 'quarterly-insights.md'
193
+
194
+ # Verify markdown content
195
+ content = output_path.read_text()
196
+ assert '# Quarterly Feedback Insights' in content
197
+ assert 'Pattern Detection' in content
198
+ assert 'Recommendations' in content
199
+
200
+ def test_aggregate_feedback_by_story_skips_non_directories(self, temp_feedback_dir):
201
+ """
202
+ GIVEN feedback directory contains files (not just directories)
203
+ WHEN aggregate_feedback_by_story is called
204
+ THEN it skips non-directory entries
205
+ """
206
+ # Arrange - Create a file in feedback root (should be skipped)
207
+ (temp_feedback_dir / 'readme.txt').write_text('This is not a story directory')
208
+
209
+ # Act
210
+ aggregated = aggregate_feedback_by_story(temp_feedback_dir)
211
+
212
+ # Assert - Should only have story directories, not the txt file
213
+ assert 'readme.txt' not in aggregated
214
+ assert all(key.startswith('STORY-') for key in aggregated.keys())
215
+
216
+ def test_aggregate_feedback_by_epic_skips_non_directories(self, temp_feedback_dir):
217
+ """
218
+ GIVEN feedback directory contains files
219
+ WHEN aggregate_feedback_by_epic is called
220
+ THEN it skips non-directory entries
221
+ """
222
+ # Arrange
223
+ (temp_feedback_dir / 'metadata.json').write_text('{}')
224
+
225
+ # Act
226
+ aggregated = aggregate_feedback_by_epic(temp_feedback_dir)
227
+
228
+ # Assert - Should have aggregated epics
229
+ assert len(aggregated) >= 2
230
+
231
+ def test_aggregate_feedback_by_skill_skips_non_directories(self, temp_feedback_dir):
232
+ """
233
+ GIVEN feedback directory contains files
234
+ WHEN aggregate_feedback_by_skill is called
235
+ THEN it skips non-directory entries
236
+ """
237
+ # Arrange
238
+ (temp_feedback_dir / 'config.yaml').write_text('enable_feedback: true')
239
+
240
+ # Act
241
+ aggregated = aggregate_feedback_by_skill(temp_feedback_dir)
242
+
243
+ # Assert - Should have aggregated workflows
244
+ assert 'dev' in aggregated
245
+ assert 'qa' in aggregated
246
+
247
+ def test_detect_patterns_with_empty_dataset(self, tmp_path):
248
+ """
249
+ GIVEN feedback directory with no feedback files
250
+ WHEN detect_patterns is called
251
+ THEN it returns empty list
252
+ """
253
+ # Arrange - Empty directory
254
+ empty_dir = tmp_path / "empty_feedback"
255
+ empty_dir.mkdir()
256
+
257
+ # Act
258
+ patterns = detect_patterns(empty_dir)
259
+
260
+ # Assert
261
+ assert patterns == []
262
+
263
+ def test_detect_patterns_below_threshold(self, temp_feedback_dir):
264
+ """
265
+ GIVEN patterns that don't meet threshold
266
+ WHEN detect_patterns is called with high threshold
267
+ THEN it returns empty list
268
+ """
269
+ # Act - Use very high threshold (95%)
270
+ patterns = detect_patterns(temp_feedback_dir, threshold=0.95)
271
+
272
+ # Assert - Should not find patterns at 95% threshold with only 3 feedback items
273
+ assert patterns == []
274
+
275
+ def test_generate_insights_with_no_data(self, tmp_path):
276
+ """
277
+ GIVEN feedback directory with no feedback files
278
+ WHEN generate_insights is called
279
+ THEN it generates fallback insights
280
+ """
281
+ # Arrange - Empty directory
282
+ empty_dir = tmp_path / "empty_feedback"
283
+ empty_dir.mkdir()
284
+
285
+ # Act
286
+ insights = generate_insights(empty_dir)
287
+
288
+ # Assert - Should have recommendations key even with no data
289
+ assert 'recommendations' in insights
290
+ assert isinstance(insights['recommendations'], list)
291
+
292
+ def test_generate_insights_fallback_with_feedback_below_threshold(self, temp_feedback_dir):
293
+ """
294
+ GIVEN feedback exists but no patterns meet threshold
295
+ WHEN generate_insights is called
296
+ THEN it generates fallback recommendation with general feedback count
297
+ """
298
+ # This test ensures lines 131-142 are covered (the fallback branch)
299
+ # The temp_feedback_dir has 3 feedback items, but patterns use 50% threshold
300
+ # which means we need at least 2/3 (67%) for a pattern
301
+ # "Documentation unclear" appears 2/3 times (67%) so it should be a pattern
302
+ # But we can still hit the fallback by ensuring the recommendations list
303
+ # gets processed even when empty initially
304
+
305
+ # Act
306
+ insights = generate_insights(temp_feedback_dir)
307
+
308
+ # Assert - Should have recommendations
309
+ assert 'recommendations' in insights
310
+ assert len(insights['recommendations']) > 0
311
+
312
+ # Should have at least the general feedback or specific patterns
313
+ for rec in insights['recommendations']:
314
+ assert 'vote_count' in rec
315
+ assert 'suggested_action' in rec
316
+
317
+ def test_detect_patterns_skips_non_directory_entries(self, temp_feedback_dir):
318
+ """
319
+ GIVEN feedback directory with files mixed in
320
+ WHEN detect_patterns is called
321
+ THEN it skips non-directory entries (line 79)
322
+ """
323
+ # Arrange - Add a file that should be skipped
324
+ (temp_feedback_dir / 'summary.md').write_text('Summary of feedback')
325
+
326
+ # Act
327
+ patterns = detect_patterns(temp_feedback_dir, threshold=0.5)
328
+
329
+ # Assert - Should still detect patterns despite the file
330
+ assert isinstance(patterns, list)
331
+
332
+ def test_generate_insights_with_low_pattern_coverage(self, tmp_path):
333
+ """
334
+ GIVEN feedback exists but patterns don't meet 50% threshold
335
+ WHEN generate_insights is called
336
+ THEN it generates fallback recommendation (lines 131-142)
337
+ """
338
+ # Create feedback directory with minimal feedback (below pattern threshold)
339
+ feedback_dir = tmp_path / "sparse_feedback"
340
+ feedback_dir.mkdir()
341
+
342
+ # Create just 1 feedback item (not enough for patterns)
343
+ story_dir = feedback_dir / "STORY-001"
344
+ story_dir.mkdir()
345
+
346
+ import json
347
+ from datetime import datetime, timezone
348
+ feedback = {
349
+ 'feedback_id': 'test-1',
350
+ 'timestamp': datetime.now(timezone.utc).isoformat(),
351
+ 'story_id': 'STORY-001',
352
+ 'epic_id': 'EPIC-001',
353
+ 'workflow_type': 'dev',
354
+ 'questions': [{'question_id': 'q1', 'response': 'unique response'}]
355
+ }
356
+
357
+ with open(story_dir / '2025-01-01-retrospective.json', 'w') as f:
358
+ json.dump(feedback, f)
359
+
360
+ # Act
361
+ insights = generate_insights(feedback_dir)
362
+
363
+ # Assert - Should generate fallback with general feedback collected
364
+ assert 'recommendations' in insights
365
+ assert len(insights['recommendations']) > 0
366
+
367
+ # Should have the fallback recommendation
368
+ rec = insights['recommendations'][0]
369
+ assert 'General feedback collected' in rec['issue'] or rec['vote_count'] >= 1
370
+
371
+
372
+ class TestLongitudinalTracking:
373
+ """AC6: Longitudinal Feedback Tracking"""
374
+
375
+ @pytest.fixture
376
+ def temp_feedback_dir(self):
377
+ """Create feedback with temporal progression"""
378
+ temp_dir = tempfile.mkdtemp()
379
+ feedback_dir = Path(temp_dir)
380
+
381
+ # Create feedback over time (simulating improvement)
382
+ base_date = datetime.now(timezone.utc) - timedelta(days=30)
383
+ for i in range(1, 6):
384
+ story_id = f'STORY-{i:03d}'
385
+ story_dir = feedback_dir / story_id
386
+ story_dir.mkdir(parents=True, exist_ok=True)
387
+
388
+ # Confidence increases over time (3, 3, 4, 4, 5)
389
+ confidence = min(3 + (i // 2), 5)
390
+
391
+ feedback_data = {
392
+ 'feedback_id': f'fb-{story_id}',
393
+ 'timestamp': (base_date + timedelta(days=i*7)).isoformat() + 'Z',
394
+ 'story_id': story_id,
395
+ 'epic_id': 'EPIC-001',
396
+ 'workflow_type': 'dev',
397
+ 'success_status': 'success',
398
+ 'questions': [
399
+ {'question_id': 'dev_success_01', 'response': confidence, 'text': 'TDD confidence'},
400
+ ],
401
+ 'metadata': {'answered': 1, 'skipped': 0},
402
+ }
403
+
404
+ filename = f"{feedback_data['timestamp'][:10]}-retrospective.json"
405
+ with open(story_dir / filename, 'w') as f:
406
+ json.dump(feedback_data, f, indent=2)
407
+
408
+ yield feedback_dir
409
+ shutil.rmtree(temp_dir)
410
+
411
+ def test_correlate_feedback_across_stories_shows_progression(self, temp_feedback_dir):
412
+ """
413
+ GIVEN user completed multiple stories over time
414
+ WHEN correlate_feedback_across_stories is called
415
+ THEN it shows progression across stories
416
+ """
417
+ # Act
418
+ correlation = correlate_feedback_across_stories(
419
+ feedback_dir=temp_feedback_dir,
420
+ user_id='default-user'
421
+ )
422
+
423
+ # Assert
424
+ assert correlation is not None
425
+ assert 'timeline' in correlation
426
+ assert len(correlation['timeline']) == 5 # 5 stories
427
+
428
+ # Verify chronological order
429
+ timestamps = [entry['timestamp'] for entry in correlation['timeline']]
430
+ assert timestamps == sorted(timestamps)
431
+
432
+ def test_identify_improvement_trajectories_detects_increase(self, temp_feedback_dir):
433
+ """
434
+ GIVEN user confidence improves over time
435
+ WHEN identify_improvement_trajectories is called
436
+ THEN it detects positive trajectory
437
+ """
438
+ # Act
439
+ trajectories = identify_improvement_trajectories(
440
+ feedback_dir=temp_feedback_dir,
441
+ user_id='default-user'
442
+ )
443
+
444
+ # Assert
445
+ assert trajectories is not None
446
+ assert 'metrics' in trajectories
447
+
448
+ # Should detect improving TDD confidence
449
+ tdd_metric = next((m for m in trajectories['metrics'] if 'confidence' in m['name'].lower()), None)
450
+ assert tdd_metric is not None
451
+ assert tdd_metric['trend'] == 'improving'
452
+ assert tdd_metric['start_value'] <= tdd_metric['end_value']
453
+
454
+ def test_export_personal_journal_creates_user_markdown(self, temp_feedback_dir):
455
+ """
456
+ GIVEN user has feedback history
457
+ WHEN export_personal_journal is called
458
+ THEN it exports devforgeai/feedback/{user-id}/journal.md
459
+ """
460
+ # Act
461
+ journal_path = export_personal_journal(
462
+ feedback_dir=temp_feedback_dir,
463
+ user_id='default-user'
464
+ )
465
+
466
+ # Assert
467
+ assert journal_path.exists()
468
+ assert journal_path.parent.name == 'default-user'
469
+ assert journal_path.name == 'journal.md'
470
+
471
+ # Verify markdown content
472
+ content = journal_path.read_text()
473
+ assert '# Retrospective Journal' in content
474
+ assert 'STORY-001' in content
475
+ assert 'STORY-005' in content
476
+ assert 'Improvement Trajectory' in content
@@ -0,0 +1,133 @@
1
+ """
2
+ Tests for config_defaults.py module.
3
+
4
+ Validates default configuration values, accessors, and copy semantics.
5
+ Target: 100% coverage of 8 statements.
6
+ """
7
+
8
+ import pytest
9
+ from devforgeai_cli.feedback.config_defaults import (
10
+ DEFAULT_CONFIG_DICT,
11
+ get_default_config,
12
+ get_default_nested_config
13
+ )
14
+
15
+
16
+ class TestConfigDefaults:
17
+ """Tests for default configuration access."""
18
+
19
+ def test_default_config_dict_exists(self):
20
+ """DEFAULT_CONFIG_DICT is defined and non-empty."""
21
+ assert isinstance(DEFAULT_CONFIG_DICT, dict)
22
+ assert len(DEFAULT_CONFIG_DICT) > 0
23
+
24
+ def test_default_config_dict_has_enabled(self):
25
+ """Defaults include enabled field with True."""
26
+ assert "enabled" in DEFAULT_CONFIG_DICT
27
+ assert DEFAULT_CONFIG_DICT["enabled"] is True
28
+
29
+ def test_default_config_dict_has_trigger_mode(self):
30
+ """Defaults include trigger_mode with failures-only."""
31
+ assert "trigger_mode" in DEFAULT_CONFIG_DICT
32
+ assert DEFAULT_CONFIG_DICT["trigger_mode"] == "failures-only"
33
+
34
+ def test_default_config_dict_has_operations(self):
35
+ """Defaults include operations field with None."""
36
+ assert "operations" in DEFAULT_CONFIG_DICT
37
+ assert DEFAULT_CONFIG_DICT["operations"] is None
38
+
39
+ def test_default_config_dict_has_nested_sections(self):
40
+ """Defaults include all three nested configuration sections."""
41
+ assert "conversation_settings" in DEFAULT_CONFIG_DICT
42
+ assert "skip_tracking" in DEFAULT_CONFIG_DICT
43
+ assert "templates" in DEFAULT_CONFIG_DICT
44
+
45
+ def test_default_config_dict_conversation_settings(self):
46
+ """Conversation settings have correct default values."""
47
+ conv_settings = DEFAULT_CONFIG_DICT["conversation_settings"]
48
+ assert conv_settings["max_questions"] == 5
49
+ assert conv_settings["allow_skip"] is True
50
+
51
+ def test_default_config_dict_skip_tracking(self):
52
+ """Skip tracking has correct default values."""
53
+ skip_settings = DEFAULT_CONFIG_DICT["skip_tracking"]
54
+ assert skip_settings["enabled"] is True
55
+ assert skip_settings["max_consecutive_skips"] == 3
56
+ assert skip_settings["reset_on_positive"] is True
57
+
58
+ def test_default_config_dict_templates(self):
59
+ """Templates have correct default values."""
60
+ template_settings = DEFAULT_CONFIG_DICT["templates"]
61
+ assert template_settings["format"] == "structured"
62
+ assert template_settings["tone"] == "brief"
63
+
64
+ def test_get_default_config_returns_dict(self):
65
+ """get_default_config() returns a dictionary."""
66
+ config = get_default_config()
67
+ assert isinstance(config, dict)
68
+
69
+ def test_get_default_config_returns_copy(self):
70
+ """get_default_config() returns a copy, not reference."""
71
+ config1 = get_default_config()
72
+ config1["enabled"] = False # Modify copy
73
+ config2 = get_default_config()
74
+
75
+ # config2 should still have original value
76
+ assert config2["enabled"] is True
77
+ # DEFAULT_CONFIG_DICT should be unchanged
78
+ assert DEFAULT_CONFIG_DICT["enabled"] is True
79
+
80
+ def test_get_default_config_has_all_fields(self):
81
+ """Returned config has all required fields."""
82
+ config = get_default_config()
83
+ assert "enabled" in config
84
+ assert "trigger_mode" in config
85
+ assert "operations" in config
86
+ assert "conversation_settings" in config
87
+ assert "skip_tracking" in config
88
+ assert "templates" in config
89
+
90
+ def test_get_default_nested_config_valid_section(self):
91
+ """Get default for specific valid section."""
92
+ conv_settings = get_default_nested_config("conversation_settings")
93
+ assert isinstance(conv_settings, dict)
94
+ assert "max_questions" in conv_settings
95
+ assert "allow_skip" in conv_settings
96
+
97
+ def test_get_default_nested_config_all_sections(self):
98
+ """All nested sections are accessible."""
99
+ # Conversation settings
100
+ conv = get_default_nested_config("conversation_settings")
101
+ assert "max_questions" in conv
102
+ assert "allow_skip" in conv
103
+
104
+ # Skip tracking
105
+ skip = get_default_nested_config("skip_tracking")
106
+ assert "enabled" in skip
107
+ assert "max_consecutive_skips" in skip
108
+ assert "reset_on_positive" in skip
109
+
110
+ # Templates
111
+ templates = get_default_nested_config("templates")
112
+ assert "format" in templates
113
+ assert "tone" in templates
114
+
115
+ def test_get_default_nested_config_invalid_section(self):
116
+ """Invalid section raises ValueError with helpful message."""
117
+ with pytest.raises(ValueError) as exc_info:
118
+ get_default_nested_config("invalid_section")
119
+
120
+ error_msg = str(exc_info.value)
121
+ assert "Unknown configuration section: invalid_section" in error_msg
122
+ assert "Valid sections:" in error_msg
123
+
124
+ def test_get_default_nested_config_returns_copy(self):
125
+ """Returned section is a copy, not reference."""
126
+ config1 = get_default_nested_config("conversation_settings")
127
+ config1["max_questions"] = 999 # Modify copy
128
+ config2 = get_default_nested_config("conversation_settings")
129
+
130
+ # config2 should have original value
131
+ assert config2["max_questions"] == 5
132
+ # DEFAULT_CONFIG_DICT should be unchanged
133
+ assert DEFAULT_CONFIG_DICT["conversation_settings"]["max_questions"] == 5