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.
- package/CLAUDE.md +120 -0
- package/package.json +9 -1
- package/src/CLAUDE.md +699 -0
- package/src/claude/scripts/README.md +396 -0
- package/src/claude/scripts/audit-command-skill-overlap.sh +67 -0
- package/src/claude/scripts/check-hooks-fast.sh +70 -0
- package/src/claude/scripts/devforgeai-validate +6 -0
- package/src/claude/scripts/devforgeai_cli/README.md +531 -0
- package/src/claude/scripts/devforgeai_cli/__init__.py +12 -0
- package/src/claude/scripts/devforgeai_cli/cli.py +716 -0
- package/src/claude/scripts/devforgeai_cli/commands/__init__.py +1 -0
- package/src/claude/scripts/devforgeai_cli/commands/check_hooks.py +384 -0
- package/src/claude/scripts/devforgeai_cli/commands/invoke_hooks.py +149 -0
- package/src/claude/scripts/devforgeai_cli/commands/phase_commands.py +731 -0
- package/src/claude/scripts/devforgeai_cli/commands/validate_installation.py +412 -0
- package/src/claude/scripts/devforgeai_cli/context_extraction.py +426 -0
- package/src/claude/scripts/devforgeai_cli/feedback/AC_TO_TEST_MAPPING.md +636 -0
- package/src/claude/scripts/devforgeai_cli/feedback/DELIVERY_SUMMARY.txt +329 -0
- package/src/claude/scripts/devforgeai_cli/feedback/README_TEST_SPECS.md +486 -0
- package/src/claude/scripts/devforgeai_cli/feedback/TEST_IMPLEMENTATION_GUIDE.md +529 -0
- package/src/claude/scripts/devforgeai_cli/feedback/TEST_SPECIFICATIONS.md +2652 -0
- package/src/claude/scripts/devforgeai_cli/feedback/TEST_SPECS_INDEX.md +398 -0
- package/src/claude/scripts/devforgeai_cli/feedback/__init__.py +34 -0
- package/src/claude/scripts/devforgeai_cli/feedback/adaptive_questioning_engine.py +581 -0
- package/src/claude/scripts/devforgeai_cli/feedback/aggregation.py +179 -0
- package/src/claude/scripts/devforgeai_cli/feedback/commands.py +535 -0
- package/src/claude/scripts/devforgeai_cli/feedback/config_defaults.py +58 -0
- package/src/claude/scripts/devforgeai_cli/feedback/config_manager.py +423 -0
- package/src/claude/scripts/devforgeai_cli/feedback/config_models.py +192 -0
- package/src/claude/scripts/devforgeai_cli/feedback/config_schema.py +140 -0
- package/src/claude/scripts/devforgeai_cli/feedback/coverage.json +1 -0
- package/src/claude/scripts/devforgeai_cli/feedback/feature_flag.py +152 -0
- package/src/claude/scripts/devforgeai_cli/feedback/feedback_indexer.py +394 -0
- package/src/claude/scripts/devforgeai_cli/feedback/hot_reload.py +226 -0
- package/src/claude/scripts/devforgeai_cli/feedback/longitudinal.py +115 -0
- package/src/claude/scripts/devforgeai_cli/feedback/models.py +67 -0
- package/src/claude/scripts/devforgeai_cli/feedback/question_router.py +236 -0
- package/src/claude/scripts/devforgeai_cli/feedback/retrospective.py +233 -0
- package/src/claude/scripts/devforgeai_cli/feedback/skip_tracker.py +177 -0
- package/src/claude/scripts/devforgeai_cli/feedback/skip_tracking.py +221 -0
- package/src/claude/scripts/devforgeai_cli/feedback/template_engine.py +549 -0
- package/src/claude/scripts/devforgeai_cli/feedback/validation.py +163 -0
- package/src/claude/scripts/devforgeai_cli/headless/__init__.py +30 -0
- package/src/claude/scripts/devforgeai_cli/headless/answer_models.py +206 -0
- package/src/claude/scripts/devforgeai_cli/headless/answer_resolver.py +204 -0
- package/src/claude/scripts/devforgeai_cli/headless/exceptions.py +36 -0
- package/src/claude/scripts/devforgeai_cli/headless/pattern_matcher.py +156 -0
- package/src/claude/scripts/devforgeai_cli/hooks.py +313 -0
- package/src/claude/scripts/devforgeai_cli/metrics/__init__.py +46 -0
- package/src/claude/scripts/devforgeai_cli/metrics/command_metrics.py +142 -0
- package/src/claude/scripts/devforgeai_cli/metrics/failure_modes.py +152 -0
- package/src/claude/scripts/devforgeai_cli/metrics/story_segmentation.py +181 -0
- package/src/claude/scripts/devforgeai_cli/orchestrate_hooks.py +780 -0
- package/src/claude/scripts/devforgeai_cli/phase_state.py +1229 -0
- package/src/claude/scripts/devforgeai_cli/session/__init__.py +30 -0
- package/src/claude/scripts/devforgeai_cli/session/checkpoint.py +268 -0
- package/src/claude/scripts/devforgeai_cli/tests/__init__.py +1 -0
- package/src/claude/scripts/devforgeai_cli/tests/conftest.py +29 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/TEST_EXECUTION_GUIDE.md +298 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/__init__.py +3 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_adaptive_questioning_engine.py +2171 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_aggregation.py +476 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_defaults.py +133 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_manager.py +592 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_models.py +373 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_schema.py +130 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_configuration_management.py +1355 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_edge_cases.py +308 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_feature_flag.py +307 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_feedback_indexer.py +384 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_hot_reload.py +580 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_integration.py +402 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_models.py +105 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_question_routing.py +262 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_retrospective.py +333 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracker.py +410 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracking.py +159 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracking_integration.py +1155 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_template_engine.py +1389 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_validation_comprehensive.py +210 -0
- package/src/claude/scripts/devforgeai_cli/tests/fixtures/autonomous-deferral-story.md +46 -0
- package/src/claude/scripts/devforgeai_cli/tests/fixtures/missing-impl-notes.md +31 -0
- package/src/claude/scripts/devforgeai_cli/tests/fixtures/valid-deferral-story.md +46 -0
- package/src/claude/scripts/devforgeai_cli/tests/fixtures/valid-story-complete.md +48 -0
- package/src/claude/scripts/devforgeai_cli/tests/manual_test_invoke_hooks.sh +200 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/DELIVERABLES.md +518 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/TEST_SUMMARY.md +468 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/__init__.py +6 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/corrupted-checkpoint.json +1 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/missing-fields-checkpoint.json +4 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/valid-checkpoint.json +15 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/test_checkpoint.py +851 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_check_hooks.py +1886 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_depends_on_normalizer.py +171 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_dod_validator.py +97 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_invoke_hooks.py +1902 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands.py +320 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands_error_handling.py +1021 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands_import.py +697 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_phase_state.py +2187 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_skip_tracking.py +2141 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_skip_tracking_coverage_gap.py +195 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_subagent_enforcement.py +539 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_validate_installation.py +361 -0
- package/src/claude/scripts/devforgeai_cli/utils/__init__.py +11 -0
- package/src/claude/scripts/devforgeai_cli/utils/depends_on_normalizer.py +149 -0
- package/src/claude/scripts/devforgeai_cli/utils/markdown_parser.py +219 -0
- package/src/claude/scripts/devforgeai_cli/utils/story_analyzer.py +249 -0
- package/src/claude/scripts/devforgeai_cli/utils/yaml_parser.py +152 -0
- package/src/claude/scripts/devforgeai_cli/validators/__init__.py +27 -0
- package/src/claude/scripts/devforgeai_cli/validators/ast_grep_validator.py +373 -0
- package/src/claude/scripts/devforgeai_cli/validators/context_validator.py +180 -0
- package/src/claude/scripts/devforgeai_cli/validators/dod_validator.py +309 -0
- package/src/claude/scripts/devforgeai_cli/validators/git_validator.py +107 -0
- package/src/claude/scripts/devforgeai_cli/validators/grep_fallback.py +300 -0
- package/src/claude/scripts/install_hooks.sh +186 -0
- package/src/claude/scripts/invoke_feedback_hooks.sh +59 -0
- package/src/claude/scripts/migrate-ac-headers.sh +122 -0
- package/src/claude/scripts/plan_file_kb.sh +704 -0
- package/src/claude/scripts/requirements.txt +8 -0
- package/src/claude/scripts/session_catalog.sh +543 -0
- package/src/claude/scripts/setup.py +55 -0
- package/src/claude/scripts/start-devforgeai.sh +16 -0
- package/src/claude/scripts/statusline.sh +27 -0
- package/src/claude/scripts/validate_deferrals.py +344 -0
- package/src/claude/skills/designing-systems/scripts/__pycache__/detect_anti_patterns.cpython-312.pyc +0 -0
- package/src/claude/skills/designing-systems/scripts/__pycache__/validate_all_context.cpython-312.pyc +0 -0
- package/src/claude/skills/designing-systems/scripts/__pycache__/validate_architecture.cpython-312.pyc +0 -0
- package/src/claude/skills/designing-systems/scripts/__pycache__/validate_dependencies.cpython-312.pyc +0 -0
- package/src/claude/skills/devforgeai-story-creation/scripts/__pycache__/migrate_story_v1_to_v2.cpython-312.pyc +0 -0
- 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
|