devforgeai 1.0.4 → 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/devforgeai-qa/SKILL.md +1 -1
- package/src/claude/skills/researching-market/SKILL.md +2 -1
- package/src/cli/lib/copier.js +13 -1
- 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,373 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for config_models.py module.
|
|
3
|
+
|
|
4
|
+
Validates data models, enums, validation logic, and type checking.
|
|
5
|
+
Target: 95% coverage of 85 statements.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
from devforgeai_cli.feedback.config_models import (
|
|
10
|
+
TriggerMode,
|
|
11
|
+
TemplateFormat,
|
|
12
|
+
TemplateTone,
|
|
13
|
+
VALID_TEMPLATE_FORMATS,
|
|
14
|
+
VALID_TEMPLATE_TONES,
|
|
15
|
+
VALID_TRIGGER_MODES,
|
|
16
|
+
ConversationSettings,
|
|
17
|
+
SkipTrackingSettings,
|
|
18
|
+
TemplateSettings,
|
|
19
|
+
FeedbackConfiguration,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TestEnumsAndConstants:
|
|
24
|
+
"""Tests for enum definitions and validation constants."""
|
|
25
|
+
|
|
26
|
+
def test_trigger_mode_enum_values(self):
|
|
27
|
+
"""TriggerMode enum has all required values."""
|
|
28
|
+
assert TriggerMode.ALWAYS.value == "always"
|
|
29
|
+
assert TriggerMode.FAILURES_ONLY.value == "failures-only"
|
|
30
|
+
assert TriggerMode.SPECIFIC_OPS.value == "specific-operations"
|
|
31
|
+
assert TriggerMode.NEVER.value == "never"
|
|
32
|
+
|
|
33
|
+
def test_template_format_enum_values(self):
|
|
34
|
+
"""TemplateFormat enum has structured and free-text."""
|
|
35
|
+
assert TemplateFormat.STRUCTURED.value == "structured"
|
|
36
|
+
assert TemplateFormat.FREE_TEXT.value == "free-text"
|
|
37
|
+
|
|
38
|
+
def test_template_tone_enum_values(self):
|
|
39
|
+
"""TemplateTone enum has brief and detailed."""
|
|
40
|
+
assert TemplateTone.BRIEF.value == "brief"
|
|
41
|
+
assert TemplateTone.DETAILED.value == "detailed"
|
|
42
|
+
|
|
43
|
+
def test_valid_template_formats_constant(self):
|
|
44
|
+
"""VALID_TEMPLATE_FORMATS contains correct values."""
|
|
45
|
+
assert VALID_TEMPLATE_FORMATS == {"structured", "free-text"}
|
|
46
|
+
|
|
47
|
+
def test_valid_template_tones_constant(self):
|
|
48
|
+
"""VALID_TEMPLATE_TONES contains correct values."""
|
|
49
|
+
assert VALID_TEMPLATE_TONES == {"brief", "detailed"}
|
|
50
|
+
|
|
51
|
+
def test_valid_trigger_modes_constant(self):
|
|
52
|
+
"""VALID_TRIGGER_MODES contains all trigger modes."""
|
|
53
|
+
assert VALID_TRIGGER_MODES == {
|
|
54
|
+
"always",
|
|
55
|
+
"failures-only",
|
|
56
|
+
"specific-operations",
|
|
57
|
+
"never"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class TestConversationSettings:
|
|
62
|
+
"""Tests for ConversationSettings dataclass."""
|
|
63
|
+
|
|
64
|
+
def test_conversation_settings_default_values(self):
|
|
65
|
+
"""ConversationSettings has correct defaults."""
|
|
66
|
+
settings = ConversationSettings()
|
|
67
|
+
assert settings.max_questions == 5
|
|
68
|
+
assert settings.allow_skip is True
|
|
69
|
+
|
|
70
|
+
def test_conversation_settings_custom_values(self):
|
|
71
|
+
"""ConversationSettings accepts custom values."""
|
|
72
|
+
settings = ConversationSettings(max_questions=10, allow_skip=False)
|
|
73
|
+
assert settings.max_questions == 10
|
|
74
|
+
assert settings.allow_skip is False
|
|
75
|
+
|
|
76
|
+
def test_conversation_settings_zero_max_questions(self):
|
|
77
|
+
"""ConversationSettings allows 0 (unlimited)."""
|
|
78
|
+
settings = ConversationSettings(max_questions=0)
|
|
79
|
+
assert settings.max_questions == 0
|
|
80
|
+
|
|
81
|
+
def test_conversation_settings_invalid_max_questions_negative(self):
|
|
82
|
+
"""Negative max_questions raises ValueError."""
|
|
83
|
+
with pytest.raises(ValueError) as exc_info:
|
|
84
|
+
ConversationSettings(max_questions=-1)
|
|
85
|
+
assert "must be non-negative integer" in str(exc_info.value)
|
|
86
|
+
|
|
87
|
+
def test_conversation_settings_invalid_max_questions_not_int(self):
|
|
88
|
+
"""Non-integer max_questions raises ValueError."""
|
|
89
|
+
with pytest.raises(ValueError) as exc_info:
|
|
90
|
+
ConversationSettings(max_questions="invalid")
|
|
91
|
+
# Type error happens before validation
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
def test_conversation_settings_invalid_allow_skip_not_bool(self):
|
|
95
|
+
"""Non-boolean allow_skip raises ValueError."""
|
|
96
|
+
with pytest.raises(ValueError) as exc_info:
|
|
97
|
+
ConversationSettings(allow_skip="invalid")
|
|
98
|
+
# Type error happens before validation
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class TestSkipTrackingSettings:
|
|
103
|
+
"""Tests for SkipTrackingSettings dataclass."""
|
|
104
|
+
|
|
105
|
+
def test_skip_tracking_settings_default_values(self):
|
|
106
|
+
"""SkipTrackingSettings has correct defaults."""
|
|
107
|
+
settings = SkipTrackingSettings()
|
|
108
|
+
assert settings.enabled is True
|
|
109
|
+
assert settings.max_consecutive_skips == 3
|
|
110
|
+
assert settings.reset_on_positive is True
|
|
111
|
+
|
|
112
|
+
def test_skip_tracking_settings_custom_values(self):
|
|
113
|
+
"""SkipTrackingSettings accepts custom values."""
|
|
114
|
+
settings = SkipTrackingSettings(
|
|
115
|
+
enabled=False,
|
|
116
|
+
max_consecutive_skips=5,
|
|
117
|
+
reset_on_positive=False
|
|
118
|
+
)
|
|
119
|
+
assert settings.enabled is False
|
|
120
|
+
assert settings.max_consecutive_skips == 5
|
|
121
|
+
assert settings.reset_on_positive is False
|
|
122
|
+
|
|
123
|
+
def test_skip_tracking_settings_zero_max_skips(self):
|
|
124
|
+
"""SkipTrackingSettings allows 0 (unlimited)."""
|
|
125
|
+
settings = SkipTrackingSettings(max_consecutive_skips=0)
|
|
126
|
+
assert settings.max_consecutive_skips == 0
|
|
127
|
+
|
|
128
|
+
def test_skip_tracking_settings_invalid_enabled_not_bool(self):
|
|
129
|
+
"""Non-boolean enabled raises ValueError."""
|
|
130
|
+
with pytest.raises(ValueError) as exc_info:
|
|
131
|
+
SkipTrackingSettings(enabled="invalid")
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
def test_skip_tracking_settings_invalid_max_skips_negative(self):
|
|
135
|
+
"""Negative max_consecutive_skips raises ValueError."""
|
|
136
|
+
with pytest.raises(ValueError) as exc_info:
|
|
137
|
+
SkipTrackingSettings(max_consecutive_skips=-1)
|
|
138
|
+
assert "must be non-negative integer" in str(exc_info.value)
|
|
139
|
+
|
|
140
|
+
def test_skip_tracking_settings_invalid_reset_on_positive_not_bool(self):
|
|
141
|
+
"""Non-boolean reset_on_positive raises ValueError."""
|
|
142
|
+
with pytest.raises(ValueError) as exc_info:
|
|
143
|
+
SkipTrackingSettings(reset_on_positive="invalid")
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class TestTemplateSettings:
|
|
148
|
+
"""Tests for TemplateSettings dataclass."""
|
|
149
|
+
|
|
150
|
+
def test_template_settings_default_values(self):
|
|
151
|
+
"""TemplateSettings has correct defaults."""
|
|
152
|
+
settings = TemplateSettings()
|
|
153
|
+
assert settings.format == "structured"
|
|
154
|
+
assert settings.tone == "brief"
|
|
155
|
+
|
|
156
|
+
def test_template_settings_custom_values_structured_brief(self):
|
|
157
|
+
"""TemplateSettings accepts structured + brief."""
|
|
158
|
+
settings = TemplateSettings(format="structured", tone="brief")
|
|
159
|
+
assert settings.format == "structured"
|
|
160
|
+
assert settings.tone == "brief"
|
|
161
|
+
|
|
162
|
+
def test_template_settings_custom_values_free_text_detailed(self):
|
|
163
|
+
"""TemplateSettings accepts free-text + detailed."""
|
|
164
|
+
settings = TemplateSettings(format="free-text", tone="detailed")
|
|
165
|
+
assert settings.format == "free-text"
|
|
166
|
+
assert settings.tone == "detailed"
|
|
167
|
+
|
|
168
|
+
def test_template_settings_invalid_format(self):
|
|
169
|
+
"""Invalid format raises ValueError with helpful message."""
|
|
170
|
+
with pytest.raises(ValueError) as exc_info:
|
|
171
|
+
TemplateSettings(format="invalid")
|
|
172
|
+
error_msg = str(exc_info.value)
|
|
173
|
+
assert "Invalid template format: 'invalid'" in error_msg
|
|
174
|
+
assert "Must be one of:" in error_msg
|
|
175
|
+
|
|
176
|
+
def test_template_settings_invalid_tone(self):
|
|
177
|
+
"""Invalid tone raises ValueError with helpful message."""
|
|
178
|
+
with pytest.raises(ValueError) as exc_info:
|
|
179
|
+
TemplateSettings(tone="invalid")
|
|
180
|
+
error_msg = str(exc_info.value)
|
|
181
|
+
assert "Invalid template tone: 'invalid'" in error_msg
|
|
182
|
+
assert "Must be one of:" in error_msg
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class TestFeedbackConfiguration:
|
|
186
|
+
"""Tests for main FeedbackConfiguration dataclass."""
|
|
187
|
+
|
|
188
|
+
def test_feedback_configuration_default_values(self):
|
|
189
|
+
"""FeedbackConfiguration has correct defaults."""
|
|
190
|
+
config = FeedbackConfiguration()
|
|
191
|
+
assert config.enabled is True
|
|
192
|
+
assert config.trigger_mode == "failures-only"
|
|
193
|
+
assert config.operations is None
|
|
194
|
+
assert isinstance(config.conversation_settings, ConversationSettings)
|
|
195
|
+
assert isinstance(config.skip_tracking, SkipTrackingSettings)
|
|
196
|
+
assert isinstance(config.templates, TemplateSettings)
|
|
197
|
+
|
|
198
|
+
def test_feedback_configuration_custom_values(self):
|
|
199
|
+
"""FeedbackConfiguration accepts all custom values."""
|
|
200
|
+
config = FeedbackConfiguration(
|
|
201
|
+
enabled=False,
|
|
202
|
+
trigger_mode="always",
|
|
203
|
+
operations=["qa", "dev"],
|
|
204
|
+
conversation_settings=ConversationSettings(max_questions=10),
|
|
205
|
+
skip_tracking=SkipTrackingSettings(enabled=False),
|
|
206
|
+
templates=TemplateSettings(format="free-text", tone="detailed")
|
|
207
|
+
)
|
|
208
|
+
assert config.enabled is False
|
|
209
|
+
assert config.trigger_mode == "always"
|
|
210
|
+
assert config.operations == ["qa", "dev"]
|
|
211
|
+
assert config.conversation_settings.max_questions == 10
|
|
212
|
+
assert config.skip_tracking.enabled is False
|
|
213
|
+
assert config.templates.format == "free-text"
|
|
214
|
+
|
|
215
|
+
def test_feedback_configuration_normalize_conversation_settings_dict(self):
|
|
216
|
+
"""Nested dict for conversation_settings is converted to dataclass."""
|
|
217
|
+
config = FeedbackConfiguration(
|
|
218
|
+
conversation_settings={"max_questions": 8, "allow_skip": False}
|
|
219
|
+
)
|
|
220
|
+
assert isinstance(config.conversation_settings, ConversationSettings)
|
|
221
|
+
assert config.conversation_settings.max_questions == 8
|
|
222
|
+
assert config.conversation_settings.allow_skip is False
|
|
223
|
+
|
|
224
|
+
def test_feedback_configuration_normalize_skip_tracking_dict(self):
|
|
225
|
+
"""Nested dict for skip_tracking is converted to dataclass."""
|
|
226
|
+
config = FeedbackConfiguration(
|
|
227
|
+
skip_tracking={"enabled": False, "max_consecutive_skips": 10, "reset_on_positive": False}
|
|
228
|
+
)
|
|
229
|
+
assert isinstance(config.skip_tracking, SkipTrackingSettings)
|
|
230
|
+
assert config.skip_tracking.enabled is False
|
|
231
|
+
assert config.skip_tracking.max_consecutive_skips == 10
|
|
232
|
+
|
|
233
|
+
def test_feedback_configuration_normalize_templates_dict(self):
|
|
234
|
+
"""Nested dict for templates is converted to dataclass."""
|
|
235
|
+
config = FeedbackConfiguration(
|
|
236
|
+
templates={"format": "free-text", "tone": "detailed"}
|
|
237
|
+
)
|
|
238
|
+
assert isinstance(config.templates, TemplateSettings)
|
|
239
|
+
assert config.templates.format == "free-text"
|
|
240
|
+
assert config.templates.tone == "detailed"
|
|
241
|
+
|
|
242
|
+
def test_feedback_configuration_normalize_none_conversation_settings(self):
|
|
243
|
+
"""None conversation_settings is replaced with defaults."""
|
|
244
|
+
config = FeedbackConfiguration(conversation_settings=None)
|
|
245
|
+
assert isinstance(config.conversation_settings, ConversationSettings)
|
|
246
|
+
assert config.conversation_settings.max_questions == 5
|
|
247
|
+
|
|
248
|
+
def test_feedback_configuration_normalize_none_skip_tracking(self):
|
|
249
|
+
"""None skip_tracking is replaced with defaults."""
|
|
250
|
+
config = FeedbackConfiguration(skip_tracking=None)
|
|
251
|
+
assert isinstance(config.skip_tracking, SkipTrackingSettings)
|
|
252
|
+
assert config.skip_tracking.enabled is True
|
|
253
|
+
|
|
254
|
+
def test_feedback_configuration_normalize_none_templates(self):
|
|
255
|
+
"""None templates is replaced with defaults."""
|
|
256
|
+
config = FeedbackConfiguration(templates=None)
|
|
257
|
+
assert isinstance(config.templates, TemplateSettings)
|
|
258
|
+
assert config.templates.format == "structured"
|
|
259
|
+
|
|
260
|
+
def test_feedback_configuration_invalid_enabled_not_bool(self):
|
|
261
|
+
"""Non-boolean enabled raises ValueError."""
|
|
262
|
+
with pytest.raises(ValueError) as exc_info:
|
|
263
|
+
FeedbackConfiguration(enabled="invalid")
|
|
264
|
+
pass
|
|
265
|
+
|
|
266
|
+
def test_feedback_configuration_invalid_trigger_mode(self):
|
|
267
|
+
"""Invalid trigger_mode raises ValueError with helpful message."""
|
|
268
|
+
with pytest.raises(ValueError) as exc_info:
|
|
269
|
+
FeedbackConfiguration(trigger_mode="invalid-mode")
|
|
270
|
+
error_msg = str(exc_info.value)
|
|
271
|
+
assert "Invalid trigger_mode value: 'invalid-mode'" in error_msg
|
|
272
|
+
assert "Must be one of:" in error_msg
|
|
273
|
+
|
|
274
|
+
def test_feedback_configuration_trigger_mode_always(self):
|
|
275
|
+
"""Trigger mode 'always' is valid."""
|
|
276
|
+
config = FeedbackConfiguration(trigger_mode="always")
|
|
277
|
+
assert config.trigger_mode == "always"
|
|
278
|
+
|
|
279
|
+
def test_feedback_configuration_trigger_mode_failures_only(self):
|
|
280
|
+
"""Trigger mode 'failures-only' is valid."""
|
|
281
|
+
config = FeedbackConfiguration(trigger_mode="failures-only")
|
|
282
|
+
assert config.trigger_mode == "failures-only"
|
|
283
|
+
|
|
284
|
+
def test_feedback_configuration_trigger_mode_specific_operations(self):
|
|
285
|
+
"""Trigger mode 'specific-operations' is valid."""
|
|
286
|
+
config = FeedbackConfiguration(
|
|
287
|
+
trigger_mode="specific-operations",
|
|
288
|
+
operations=["qa"]
|
|
289
|
+
)
|
|
290
|
+
assert config.trigger_mode == "specific-operations"
|
|
291
|
+
|
|
292
|
+
def test_feedback_configuration_trigger_mode_never(self):
|
|
293
|
+
"""Trigger mode 'never' is valid."""
|
|
294
|
+
config = FeedbackConfiguration(trigger_mode="never")
|
|
295
|
+
assert config.trigger_mode == "never"
|
|
296
|
+
|
|
297
|
+
def test_feedback_configuration_specific_operations_requires_operations(self):
|
|
298
|
+
"""specific-operations mode requires operations list."""
|
|
299
|
+
with pytest.raises(ValueError) as exc_info:
|
|
300
|
+
FeedbackConfiguration(
|
|
301
|
+
trigger_mode="specific-operations",
|
|
302
|
+
operations=None
|
|
303
|
+
)
|
|
304
|
+
error_msg = str(exc_info.value)
|
|
305
|
+
assert "operations list must be provided" in error_msg
|
|
306
|
+
|
|
307
|
+
def test_feedback_configuration_specific_operations_requires_non_empty(self):
|
|
308
|
+
"""specific-operations mode requires non-empty operations list."""
|
|
309
|
+
with pytest.raises(ValueError) as exc_info:
|
|
310
|
+
FeedbackConfiguration(
|
|
311
|
+
trigger_mode="specific-operations",
|
|
312
|
+
operations=[]
|
|
313
|
+
)
|
|
314
|
+
error_msg = str(exc_info.value)
|
|
315
|
+
assert "non-empty" in error_msg
|
|
316
|
+
|
|
317
|
+
def test_feedback_configuration_operations_with_other_modes(self):
|
|
318
|
+
"""Operations can be None for non-specific-operations modes."""
|
|
319
|
+
config = FeedbackConfiguration(
|
|
320
|
+
trigger_mode="always",
|
|
321
|
+
operations=None
|
|
322
|
+
)
|
|
323
|
+
assert config.operations is None
|
|
324
|
+
|
|
325
|
+
def test_feedback_configuration_to_dict(self):
|
|
326
|
+
"""to_dict() converts configuration to dictionary."""
|
|
327
|
+
config = FeedbackConfiguration()
|
|
328
|
+
result = config.to_dict()
|
|
329
|
+
|
|
330
|
+
assert isinstance(result, dict)
|
|
331
|
+
assert result["enabled"] is True
|
|
332
|
+
assert result["trigger_mode"] == "failures-only"
|
|
333
|
+
assert result["operations"] is None
|
|
334
|
+
assert isinstance(result["conversation_settings"], dict)
|
|
335
|
+
assert isinstance(result["skip_tracking"], dict)
|
|
336
|
+
assert isinstance(result["templates"], dict)
|
|
337
|
+
|
|
338
|
+
def test_feedback_configuration_to_dict_nested_structure(self):
|
|
339
|
+
"""to_dict() includes all nested fields."""
|
|
340
|
+
config = FeedbackConfiguration()
|
|
341
|
+
result = config.to_dict()
|
|
342
|
+
|
|
343
|
+
# Check conversation_settings nested fields
|
|
344
|
+
assert result["conversation_settings"]["max_questions"] == 5
|
|
345
|
+
assert result["conversation_settings"]["allow_skip"] is True
|
|
346
|
+
|
|
347
|
+
# Check skip_tracking nested fields
|
|
348
|
+
assert result["skip_tracking"]["enabled"] is True
|
|
349
|
+
assert result["skip_tracking"]["max_consecutive_skips"] == 3
|
|
350
|
+
assert result["skip_tracking"]["reset_on_positive"] is True
|
|
351
|
+
|
|
352
|
+
# Check templates nested fields
|
|
353
|
+
assert result["templates"]["format"] == "structured"
|
|
354
|
+
assert result["templates"]["tone"] == "brief"
|
|
355
|
+
|
|
356
|
+
def test_feedback_configuration_to_dict_custom_values(self):
|
|
357
|
+
"""to_dict() correctly serializes custom values."""
|
|
358
|
+
config = FeedbackConfiguration(
|
|
359
|
+
enabled=False,
|
|
360
|
+
trigger_mode="specific-operations",
|
|
361
|
+
operations=["qa", "dev"],
|
|
362
|
+
conversation_settings=ConversationSettings(max_questions=10, allow_skip=False),
|
|
363
|
+
skip_tracking=SkipTrackingSettings(enabled=False, max_consecutive_skips=5, reset_on_positive=False),
|
|
364
|
+
templates=TemplateSettings(format="free-text", tone="detailed")
|
|
365
|
+
)
|
|
366
|
+
result = config.to_dict()
|
|
367
|
+
|
|
368
|
+
assert result["enabled"] is False
|
|
369
|
+
assert result["trigger_mode"] == "specific-operations"
|
|
370
|
+
assert result["operations"] == ["qa", "dev"]
|
|
371
|
+
assert result["conversation_settings"]["max_questions"] == 10
|
|
372
|
+
assert result["skip_tracking"]["enabled"] is False
|
|
373
|
+
assert result["templates"]["format"] == "free-text"
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for config_schema.py module.
|
|
3
|
+
|
|
4
|
+
Validates JSON Schema structure and accessor functionality.
|
|
5
|
+
Target: 100% coverage of 4 statements (module-level dict + 1 function).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
from devforgeai_cli.feedback.config_schema import FEEDBACK_CONFIG_SCHEMA, get_schema
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestConfigSchema:
|
|
13
|
+
"""Tests for JSON Schema structure and access."""
|
|
14
|
+
|
|
15
|
+
def test_feedback_config_schema_global_defined(self):
|
|
16
|
+
"""Global FEEDBACK_CONFIG_SCHEMA is defined and non-empty."""
|
|
17
|
+
assert isinstance(FEEDBACK_CONFIG_SCHEMA, dict)
|
|
18
|
+
assert len(FEEDBACK_CONFIG_SCHEMA) > 0
|
|
19
|
+
|
|
20
|
+
def test_get_schema_returns_dict(self):
|
|
21
|
+
"""get_schema() returns a dictionary."""
|
|
22
|
+
schema = get_schema()
|
|
23
|
+
assert isinstance(schema, dict)
|
|
24
|
+
|
|
25
|
+
def test_get_schema_returns_copy(self):
|
|
26
|
+
"""get_schema() returns a copy, not reference."""
|
|
27
|
+
schema1 = get_schema()
|
|
28
|
+
schema1["title"] = "Modified" # Modify copy
|
|
29
|
+
schema2 = get_schema()
|
|
30
|
+
|
|
31
|
+
# schema2 should have original value
|
|
32
|
+
assert schema2["title"] == "Feedback Configuration Schema"
|
|
33
|
+
# FEEDBACK_CONFIG_SCHEMA should be unchanged
|
|
34
|
+
assert FEEDBACK_CONFIG_SCHEMA["title"] == "Feedback Configuration Schema"
|
|
35
|
+
|
|
36
|
+
def test_get_schema_has_required_structure(self):
|
|
37
|
+
"""Schema contains required JSON Schema fields."""
|
|
38
|
+
schema = get_schema()
|
|
39
|
+
assert "$schema" in schema
|
|
40
|
+
assert "title" in schema
|
|
41
|
+
assert "type" in schema
|
|
42
|
+
assert "properties" in schema
|
|
43
|
+
|
|
44
|
+
def test_get_schema_json_schema_draft_07(self):
|
|
45
|
+
"""Schema specifies JSON Schema draft 07."""
|
|
46
|
+
schema = get_schema()
|
|
47
|
+
assert schema["$schema"] == "http://json-schema.org/draft-07/schema#"
|
|
48
|
+
|
|
49
|
+
def test_get_schema_type_is_object(self):
|
|
50
|
+
"""Schema type is 'object' for configuration dict."""
|
|
51
|
+
schema = get_schema()
|
|
52
|
+
assert schema["type"] == "object"
|
|
53
|
+
|
|
54
|
+
def test_get_schema_has_enabled_property(self):
|
|
55
|
+
"""Schema defines 'enabled' property."""
|
|
56
|
+
schema = get_schema()
|
|
57
|
+
assert "enabled" in schema["properties"]
|
|
58
|
+
assert schema["properties"]["enabled"]["type"] == "boolean"
|
|
59
|
+
|
|
60
|
+
def test_get_schema_has_trigger_mode_property(self):
|
|
61
|
+
"""Schema defines 'trigger_mode' property with enum."""
|
|
62
|
+
schema = get_schema()
|
|
63
|
+
assert "trigger_mode" in schema["properties"]
|
|
64
|
+
trigger_mode = schema["properties"]["trigger_mode"]
|
|
65
|
+
assert trigger_mode["type"] == "string"
|
|
66
|
+
assert "enum" in trigger_mode
|
|
67
|
+
assert set(trigger_mode["enum"]) == {"always", "failures-only", "specific-operations", "never"}
|
|
68
|
+
|
|
69
|
+
def test_get_schema_has_operations_property(self):
|
|
70
|
+
"""Schema defines 'operations' property (array or null)."""
|
|
71
|
+
schema = get_schema()
|
|
72
|
+
assert "operations" in schema["properties"]
|
|
73
|
+
operations = schema["properties"]["operations"]
|
|
74
|
+
assert set(operations["type"]) == {"array", "null"}
|
|
75
|
+
|
|
76
|
+
def test_get_schema_has_conversation_settings(self):
|
|
77
|
+
"""Schema defines 'conversation_settings' nested object."""
|
|
78
|
+
schema = get_schema()
|
|
79
|
+
assert "conversation_settings" in schema["properties"]
|
|
80
|
+
conv = schema["properties"]["conversation_settings"]
|
|
81
|
+
assert conv["type"] == "object"
|
|
82
|
+
assert "max_questions" in conv["properties"]
|
|
83
|
+
assert "allow_skip" in conv["properties"]
|
|
84
|
+
|
|
85
|
+
def test_get_schema_has_skip_tracking(self):
|
|
86
|
+
"""Schema defines 'skip_tracking' nested object."""
|
|
87
|
+
schema = get_schema()
|
|
88
|
+
assert "skip_tracking" in schema["properties"]
|
|
89
|
+
skip = schema["properties"]["skip_tracking"]
|
|
90
|
+
assert skip["type"] == "object"
|
|
91
|
+
assert "enabled" in skip["properties"]
|
|
92
|
+
assert "max_consecutive_skips" in skip["properties"]
|
|
93
|
+
assert "reset_on_positive" in skip["properties"]
|
|
94
|
+
|
|
95
|
+
def test_get_schema_has_templates(self):
|
|
96
|
+
"""Schema defines 'templates' nested object."""
|
|
97
|
+
schema = get_schema()
|
|
98
|
+
assert "templates" in schema["properties"]
|
|
99
|
+
templates = schema["properties"]["templates"]
|
|
100
|
+
assert templates["type"] == "object"
|
|
101
|
+
assert "format" in templates["properties"]
|
|
102
|
+
assert "tone" in templates["properties"]
|
|
103
|
+
|
|
104
|
+
def test_get_schema_has_default_values(self):
|
|
105
|
+
"""Schema includes default values at root level."""
|
|
106
|
+
schema = get_schema()
|
|
107
|
+
assert "default" in schema
|
|
108
|
+
defaults = schema["default"]
|
|
109
|
+
assert defaults["enabled"] is True
|
|
110
|
+
assert defaults["trigger_mode"] == "failures-only"
|
|
111
|
+
assert "conversation_settings" in defaults
|
|
112
|
+
assert "skip_tracking" in defaults
|
|
113
|
+
assert "templates" in defaults
|
|
114
|
+
|
|
115
|
+
def test_get_schema_no_additional_properties(self):
|
|
116
|
+
"""Schema disallows additional properties at root."""
|
|
117
|
+
schema = get_schema()
|
|
118
|
+
assert schema["additionalProperties"] is False
|
|
119
|
+
|
|
120
|
+
def test_get_schema_template_format_enum(self):
|
|
121
|
+
"""Template format has correct enum values."""
|
|
122
|
+
schema = get_schema()
|
|
123
|
+
template_format = schema["properties"]["templates"]["properties"]["format"]
|
|
124
|
+
assert set(template_format["enum"]) == {"structured", "free-text"}
|
|
125
|
+
|
|
126
|
+
def test_get_schema_template_tone_enum(self):
|
|
127
|
+
"""Template tone has correct enum values."""
|
|
128
|
+
schema = get_schema()
|
|
129
|
+
template_tone = schema["properties"]["templates"]["properties"]["tone"]
|
|
130
|
+
assert set(template_tone["enum"]) == {"brief", "detailed"}
|