devforgeai 1.0.5 → 1.0.7
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/bin/devforgeai.js +0 -0
- package/package.json +9 -1
- package/src/CLAUDE.md +699 -0
- package/src/claude/hooks/phase-completion-gate.sh +0 -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,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
|