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,402 @@
1
+ """
2
+ Integration tests for retrospective feedback system
3
+
4
+ Tests the complete workflow end-to-end with real file I/O,
5
+ YAML persistence, and actual workflow scenarios.
6
+ """
7
+
8
+ import pytest
9
+ import tempfile
10
+ import shutil
11
+ from pathlib import Path
12
+ from datetime import datetime, timezone
13
+
14
+ from devforgeai_cli.feedback.retrospective import (
15
+ trigger_retrospective,
16
+ capture_feedback,
17
+ save_in_progress_state,
18
+ resume_feedback,
19
+ detect_rapid_sequence,
20
+ )
21
+ from devforgeai_cli.feedback.skip_tracking import (
22
+ increment_skip,
23
+ get_skip_count,
24
+ reset_skip_count,
25
+ check_skip_threshold,
26
+ )
27
+ from devforgeai_cli.feedback.aggregation import (
28
+ aggregate_feedback_by_story,
29
+ aggregate_feedback_by_epic,
30
+ detect_patterns,
31
+ generate_insights,
32
+ export_quarterly_insights,
33
+ )
34
+ from devforgeai_cli.feedback.longitudinal import (
35
+ correlate_feedback_across_stories,
36
+ identify_improvement_trajectories,
37
+ export_personal_journal,
38
+ )
39
+
40
+
41
+ class TestFullWorkflowIntegration:
42
+ """Test complete workflow from trigger → capture → aggregation"""
43
+
44
+ @pytest.fixture
45
+ def temp_dirs(self):
46
+ """Create temporary directories for feedback and config"""
47
+ temp_dir = tempfile.mkdtemp()
48
+ feedback_dir = Path(temp_dir) / 'feedback'
49
+ config_dir = Path(temp_dir) / 'config'
50
+ feedback_dir.mkdir(parents=True)
51
+ config_dir.mkdir(parents=True)
52
+
53
+ yield feedback_dir, config_dir
54
+
55
+ shutil.rmtree(temp_dir)
56
+
57
+ def test_successful_dev_workflow_end_to_end(self, temp_dirs):
58
+ """
59
+ GIVEN user completes /dev STORY-001 successfully
60
+ WHEN retrospective triggered, feedback captured, and aggregated
61
+ THEN complete workflow executes without errors
62
+ """
63
+ feedback_dir, config_dir = temp_dirs
64
+
65
+ # Step 1: Trigger retrospective
66
+ operation_result = {
67
+ 'status': 'success',
68
+ 'story_id': 'STORY-001',
69
+ }
70
+ questions = trigger_retrospective('dev', operation_result)
71
+
72
+ assert len(questions) >= 4
73
+ assert all(hasattr(q, 'question_id') for q in questions)
74
+
75
+ # Step 2: Simulate user responses
76
+ responses = [
77
+ {'question_id': questions[0].question_id, 'response': 5, 'skip': False},
78
+ {'question_id': questions[1].question_id, 'response': 'Green', 'skip': False},
79
+ {'question_id': questions[2].question_id, 'response': 'The workflow was very clear and well-structured.', 'skip': False},
80
+ {'question_id': questions[3].question_id, 'response': 4, 'skip': False},
81
+ ]
82
+
83
+ # Step 3: Capture feedback
84
+ result = capture_feedback(
85
+ responses=responses,
86
+ story_id='STORY-001',
87
+ workflow_type='dev',
88
+ success_status='success',
89
+ feedback_dir=feedback_dir,
90
+ epic_id='EPIC-001',
91
+ )
92
+
93
+ assert result['status'] == 'recorded'
94
+ assert 'feedback_id' in result
95
+ assert 'file_path' in result
96
+
97
+ # Verify file was written
98
+ assert Path(result['file_path']).exists()
99
+
100
+ # Step 4: Aggregate feedback
101
+ aggregated = aggregate_feedback_by_story(feedback_dir)
102
+ assert 'STORY-001' in aggregated
103
+ assert len(aggregated['STORY-001']) == 1
104
+
105
+ def test_failed_qa_workflow_with_skip_tracking(self, temp_dirs):
106
+ """
107
+ GIVEN user runs /qa that fails 3 times and skips feedback each time
108
+ WHEN skip threshold reached
109
+ THEN system detects pattern and suggests improvement
110
+ """
111
+ feedback_dir, config_dir = temp_dirs
112
+ user_id = 'test_user'
113
+
114
+ # Simulate 3 failed QA operations with feedback skipped
115
+ for i in range(3):
116
+ # Trigger retrospective
117
+ operation_result = {
118
+ 'status': 'failed',
119
+ 'story_id': f'STORY-00{i+1}',
120
+ 'failure_reason': 'Coverage below threshold',
121
+ }
122
+ questions = trigger_retrospective('qa', operation_result)
123
+
124
+ # User skips all questions
125
+ responses = [
126
+ {'question_id': q.question_id, 'response': '', 'skip': True}
127
+ for q in questions
128
+ ]
129
+
130
+ # Capture feedback (should be skipped)
131
+ result = capture_feedback(
132
+ responses=responses,
133
+ story_id=f'STORY-00{i+1}',
134
+ workflow_type='qa',
135
+ success_status='failed',
136
+ feedback_dir=feedback_dir,
137
+ allow_skip=True,
138
+ )
139
+
140
+ assert result['status'] == 'skipped'
141
+
142
+ # Increment skip count
143
+ count = increment_skip(user_id, config_dir)
144
+ assert count == i + 1
145
+
146
+ # Verify threshold detected
147
+ assert check_skip_threshold(user_id, threshold=3, config_dir=config_dir)
148
+ assert get_skip_count(user_id, config_dir) == 3
149
+
150
+ def test_network_loss_recovery(self, temp_dirs):
151
+ """
152
+ GIVEN user starts feedback but connection lost mid-way
153
+ WHEN they reconnect and resume
154
+ THEN partial responses are preserved and recovered
155
+ """
156
+ feedback_dir, config_dir = temp_dirs
157
+
158
+ # Step 1: Start feedback session
159
+ partial_responses = [
160
+ {'question_id': 'dev_success_01', 'response': 4, 'skip': False},
161
+ {'question_id': 'dev_success_02', 'response': 'Red', 'skip': False},
162
+ ]
163
+
164
+ # Save in-progress state
165
+ state_file = save_in_progress_state(
166
+ story_id='STORY-001',
167
+ workflow_type='dev',
168
+ responses=partial_responses,
169
+ feedback_dir=feedback_dir,
170
+ )
171
+
172
+ assert state_file.exists()
173
+
174
+ # Step 2: Simulate recovery
175
+ recovered_state = resume_feedback(story_id='STORY-001', feedback_dir=feedback_dir)
176
+
177
+ assert recovered_state is not None
178
+ assert recovered_state['story_id'] == 'STORY-001'
179
+ assert recovered_state['workflow_type'] == 'dev'
180
+ assert len(recovered_state['responses']) == 2
181
+ assert recovered_state['responses'][0]['response'] == 4
182
+
183
+ def test_rapid_sequence_detection(self, temp_dirs):
184
+ """
185
+ GIVEN user runs /dev STORY-001, then /dev STORY-002 within 30 seconds
186
+ WHEN checking for rapid sequence
187
+ THEN system detects rapid sequence
188
+ """
189
+ feedback_dir, config_dir = temp_dirs
190
+
191
+ # First feedback
192
+ operation_result_1 = {
193
+ 'status': 'success',
194
+ 'story_id': 'STORY-001',
195
+ }
196
+ questions_1 = trigger_retrospective('dev', operation_result_1)
197
+ responses_1 = [
198
+ {'question_id': q.question_id, 'response': 'Test', 'skip': False}
199
+ for q in questions_1
200
+ ]
201
+
202
+ result_1 = capture_feedback(
203
+ responses=responses_1,
204
+ story_id='STORY-001',
205
+ workflow_type='dev',
206
+ success_status='success',
207
+ feedback_dir=feedback_dir,
208
+ )
209
+
210
+ # Get timestamp from first feedback
211
+ import json
212
+ with open(result_1['file_path'], 'r') as f:
213
+ first_feedback = json.load(f)
214
+
215
+ first_timestamp = datetime.fromisoformat(first_feedback['timestamp'].replace('Z', '+00:00'))
216
+
217
+ # Check rapid sequence (should be detected if within 30 seconds)
218
+ is_rapid = detect_rapid_sequence(first_timestamp, threshold_seconds=30)
219
+
220
+ # Since we just created it, current time - first_timestamp should be < 30 seconds
221
+ assert is_rapid is True
222
+
223
+ def test_aggregation_and_insights_generation(self, temp_dirs):
224
+ """
225
+ GIVEN multiple users provide feedback across multiple stories
226
+ WHEN generating insights
227
+ THEN patterns are detected and actionable recommendations generated
228
+ """
229
+ feedback_dir, config_dir = temp_dirs
230
+
231
+ # Create 5 feedback sessions with common issue
232
+ for i in range(5):
233
+ operation_result = {
234
+ 'status': 'success',
235
+ 'story_id': f'STORY-00{i+1}',
236
+ }
237
+ questions = trigger_retrospective('dev', operation_result)
238
+
239
+ responses = [
240
+ {'question_id': questions[0].question_id, 'response': 3, 'skip': False},
241
+ {'question_id': questions[1].question_id, 'response': 'Red', 'skip': False},
242
+ {'question_id': questions[2].question_id, 'response': 'tests are hard to write', 'skip': False},
243
+ {'question_id': questions[3].question_id, 'response': 3, 'skip': False},
244
+ ]
245
+
246
+ capture_feedback(
247
+ responses=responses,
248
+ story_id=f'STORY-00{i+1}',
249
+ workflow_type='dev',
250
+ success_status='success',
251
+ feedback_dir=feedback_dir,
252
+ epic_id='EPIC-001',
253
+ )
254
+
255
+ # Generate insights
256
+ insights = generate_insights(feedback_dir)
257
+
258
+ assert 'recommendations' in insights
259
+ # With 5 feedback sessions all saying "tests are hard to write",
260
+ # pattern detection should find this (100% occurrence)
261
+ recommendations = insights['recommendations']
262
+ assert len(recommendations) > 0
263
+
264
+ # Export quarterly insights
265
+ output_path = export_quarterly_insights(feedback_dir)
266
+ assert output_path.exists()
267
+ assert output_path.name == 'quarterly-insights.md'
268
+
269
+ def test_longitudinal_tracking_across_stories(self, temp_dirs):
270
+ """
271
+ GIVEN user completes multiple stories over time with improving confidence
272
+ WHEN analyzing trajectory
273
+ THEN system detects improvement trend
274
+ """
275
+ feedback_dir, config_dir = temp_dirs
276
+ user_id = 'test_user'
277
+
278
+ # Create 5 feedback sessions with increasing confidence
279
+ for i in range(5):
280
+ operation_result = {
281
+ 'status': 'success',
282
+ 'story_id': f'STORY-00{i+1}',
283
+ }
284
+ questions = trigger_retrospective('dev', operation_result)
285
+
286
+ # Confidence increases: 2, 3, 3, 4, 5
287
+ confidence = min(2 + (i // 2), 5)
288
+
289
+ responses = [
290
+ {'question_id': questions[0].question_id, 'response': confidence, 'skip': False},
291
+ {'question_id': questions[1].question_id, 'response': 'Red', 'skip': False},
292
+ {'question_id': questions[2].question_id, 'response': 'Getting better!', 'skip': False},
293
+ {'question_id': questions[3].question_id, 'response': confidence, 'skip': False},
294
+ ]
295
+
296
+ capture_feedback(
297
+ responses=responses,
298
+ story_id=f'STORY-00{i+1}',
299
+ workflow_type='dev',
300
+ success_status='success',
301
+ feedback_dir=feedback_dir,
302
+ epic_id='EPIC-001',
303
+ )
304
+
305
+ # Correlate feedback across stories
306
+ correlation = correlate_feedback_across_stories(feedback_dir, user_id)
307
+ assert 'timeline' in correlation
308
+ assert len(correlation['timeline']) == 5
309
+
310
+ # Identify improvement trajectory
311
+ trajectories = identify_improvement_trajectories(feedback_dir, user_id)
312
+ assert 'metrics' in trajectories
313
+
314
+ if len(trajectories['metrics']) > 0:
315
+ metric = trajectories['metrics'][0]
316
+ assert metric['name'] == 'TDD confidence'
317
+ # Should be improving (2 → 5)
318
+ assert metric['start_value'] <= metric['end_value']
319
+ assert metric['trend'] == 'improving'
320
+
321
+ # Export personal journal
322
+ journal_path = export_personal_journal(feedback_dir, user_id)
323
+ assert journal_path.exists()
324
+ assert journal_path.name == 'journal.md'
325
+
326
+
327
+ class TestErrorHandlingIntegration:
328
+ """Test error handling in integrated scenarios"""
329
+
330
+ @pytest.fixture
331
+ def temp_dirs(self):
332
+ """Create temporary directories"""
333
+ temp_dir = tempfile.mkdtemp()
334
+ feedback_dir = Path(temp_dir) / 'feedback'
335
+ config_dir = Path(temp_dir) / 'config'
336
+ feedback_dir.mkdir(parents=True)
337
+ config_dir.mkdir(parents=True)
338
+
339
+ yield feedback_dir, config_dir
340
+
341
+ shutil.rmtree(temp_dir)
342
+
343
+ def test_invalid_story_id_rejected(self, temp_dirs):
344
+ """
345
+ GIVEN user provides invalid story ID format
346
+ WHEN capturing feedback
347
+ THEN validation fails gracefully
348
+ """
349
+ from devforgeai_cli.feedback.validation import validate_story_id
350
+
351
+ # Invalid formats
352
+ assert validate_story_id('story-001') is False # lowercase
353
+ assert validate_story_id('STORY001') is False # no hyphen
354
+ assert validate_story_id('EPIC-001') is False # wrong prefix
355
+
356
+ # Valid format
357
+ assert validate_story_id('STORY-001') is True
358
+
359
+ def test_insufficient_responses_rejected(self, temp_dirs):
360
+ """
361
+ GIVEN user provides only 1 substantive response out of 5 questions
362
+ WHEN capturing feedback
363
+ THEN validation error raised
364
+ """
365
+ feedback_dir, config_dir = temp_dirs
366
+
367
+ responses = [
368
+ {'question_id': 'q1', 'response': 'Good', 'skip': False}, # Only 1 substantive
369
+ {'question_id': 'q2', 'response': '', 'skip': True},
370
+ {'question_id': 'q3', 'response': '', 'skip': True},
371
+ {'question_id': 'q4', 'response': '', 'skip': True},
372
+ {'question_id': 'q5', 'response': '', 'skip': True},
373
+ ]
374
+
375
+ with pytest.raises(ValueError, match='At least 2 of 5 questions'):
376
+ capture_feedback(
377
+ responses=responses,
378
+ story_id='STORY-001',
379
+ workflow_type='dev',
380
+ success_status='success',
381
+ feedback_dir=feedback_dir,
382
+ )
383
+
384
+ def test_sensitive_content_detection_integration(self, temp_dirs):
385
+ """
386
+ GIVEN user accidentally includes API key in feedback
387
+ WHEN capturing feedback
388
+ THEN system detects and flags sensitive content
389
+ """
390
+ from devforgeai_cli.feedback.validation import check_sensitive_content
391
+
392
+ feedback_with_key = "The API key sk-1234567890abcdefghijklmnopqrst was exposed in logs"
393
+ is_sensitive, types = check_sensitive_content(feedback_with_key)
394
+
395
+ assert is_sensitive is True
396
+ assert 'secret' in types
397
+
398
+ feedback_normal = "The workflow was great and easy to follow"
399
+ is_sensitive, types = check_sensitive_content(feedback_normal)
400
+
401
+ assert is_sensitive is False
402
+ assert len(types) == 0
@@ -0,0 +1,105 @@
1
+ """Unit tests for models.py data models.
2
+
3
+ Tests Question and FeedbackSession dataclasses.
4
+ """
5
+
6
+ import pytest
7
+ from devforgeai_cli.feedback.models import Question, FeedbackSession
8
+
9
+
10
+ class TestQuestion:
11
+ """Test Question dataclass."""
12
+
13
+ def test_question_creation_valid(self):
14
+ """Test creating a valid question."""
15
+ q = Question(
16
+ question_id='q1',
17
+ question_text='How confident are you?',
18
+ response_type='rating',
19
+ scale='1-5'
20
+ )
21
+ assert q.question_id == 'q1'
22
+ assert q.scale == '1-5'
23
+
24
+ def test_rating_question_without_scale_raises_error(self):
25
+ """Test that rating question without scale raises ValueError (line 24)."""
26
+ with pytest.raises(ValueError, match="must have scale"):
27
+ Question(
28
+ question_id='q1',
29
+ question_text='Rate this',
30
+ response_type='rating'
31
+ # Missing scale - should raise error
32
+ )
33
+
34
+ def test_multiple_choice_without_options_raises_error(self):
35
+ """Test that multiple choice without options raises ValueError (line 27)."""
36
+ with pytest.raises(ValueError, match="must have at least 2 options"):
37
+ Question(
38
+ question_id='q2',
39
+ question_text='Choose one',
40
+ response_type='multiple_choice'
41
+ # Missing options - should raise error
42
+ )
43
+
44
+ def test_multiple_choice_with_one_option_raises_error(self):
45
+ """Test that multiple choice with only 1 option raises ValueError (line 27)."""
46
+ with pytest.raises(ValueError, match="must have at least 2 options"):
47
+ Question(
48
+ question_id='q3',
49
+ question_text='Choose one',
50
+ response_type='multiple_choice',
51
+ options=['Only one option'] # Need at least 2
52
+ )
53
+
54
+
55
+ class TestFeedbackSession:
56
+ """Test FeedbackSession dataclass."""
57
+
58
+ def test_feedback_session_creation(self):
59
+ """Test creating a FeedbackSession."""
60
+ session = FeedbackSession(
61
+ feedback_id='fb-1',
62
+ timestamp='2025-01-01T00:00:00Z',
63
+ story_id='STORY-001',
64
+ epic_id='EPIC-001',
65
+ workflow_type='dev',
66
+ success_status='success'
67
+ )
68
+ assert session.feedback_id == 'fb-1'
69
+ assert session.story_id == 'STORY-001'
70
+
71
+ def test_feedback_session_to_dict(self):
72
+ """Test converting FeedbackSession to dict."""
73
+ session = FeedbackSession(
74
+ feedback_id='fb-1',
75
+ timestamp='2025-01-01T00:00:00Z',
76
+ story_id='STORY-001'
77
+ )
78
+ data = session.to_dict()
79
+
80
+ assert data['feedback_id'] == 'fb-1'
81
+ assert data['story_id'] == 'STORY-001'
82
+ assert 'timestamp' in data
83
+
84
+ def test_feedback_session_from_dict(self):
85
+ """Test creating FeedbackSession from dict (line 58)."""
86
+ data = {
87
+ 'feedback_id': 'fb-2',
88
+ 'timestamp': '2025-01-01T12:00:00Z',
89
+ 'story_id': 'STORY-002',
90
+ 'epic_id': 'EPIC-001',
91
+ 'workflow_type': 'qa',
92
+ 'success_status': 'failed',
93
+ 'questions': [{'question_id': 'q1', 'response': 3}],
94
+ 'metadata': {'duration': 120}
95
+ }
96
+
97
+ session = FeedbackSession.from_dict(data)
98
+
99
+ assert session.feedback_id == 'fb-2'
100
+ assert session.story_id == 'STORY-002'
101
+ assert session.epic_id == 'EPIC-001'
102
+ assert session.workflow_type == 'qa'
103
+ assert session.success_status == 'failed'
104
+ assert len(session.questions) == 1
105
+ assert session.metadata['duration'] == 120