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.
Files changed (134) hide show
  1. package/CLAUDE.md +120 -0
  2. package/package.json +9 -1
  3. package/src/CLAUDE.md +699 -0
  4. package/src/claude/scripts/README.md +396 -0
  5. package/src/claude/scripts/audit-command-skill-overlap.sh +67 -0
  6. package/src/claude/scripts/check-hooks-fast.sh +70 -0
  7. package/src/claude/scripts/devforgeai-validate +6 -0
  8. package/src/claude/scripts/devforgeai_cli/README.md +531 -0
  9. package/src/claude/scripts/devforgeai_cli/__init__.py +12 -0
  10. package/src/claude/scripts/devforgeai_cli/cli.py +716 -0
  11. package/src/claude/scripts/devforgeai_cli/commands/__init__.py +1 -0
  12. package/src/claude/scripts/devforgeai_cli/commands/check_hooks.py +384 -0
  13. package/src/claude/scripts/devforgeai_cli/commands/invoke_hooks.py +149 -0
  14. package/src/claude/scripts/devforgeai_cli/commands/phase_commands.py +731 -0
  15. package/src/claude/scripts/devforgeai_cli/commands/validate_installation.py +412 -0
  16. package/src/claude/scripts/devforgeai_cli/context_extraction.py +426 -0
  17. package/src/claude/scripts/devforgeai_cli/feedback/AC_TO_TEST_MAPPING.md +636 -0
  18. package/src/claude/scripts/devforgeai_cli/feedback/DELIVERY_SUMMARY.txt +329 -0
  19. package/src/claude/scripts/devforgeai_cli/feedback/README_TEST_SPECS.md +486 -0
  20. package/src/claude/scripts/devforgeai_cli/feedback/TEST_IMPLEMENTATION_GUIDE.md +529 -0
  21. package/src/claude/scripts/devforgeai_cli/feedback/TEST_SPECIFICATIONS.md +2652 -0
  22. package/src/claude/scripts/devforgeai_cli/feedback/TEST_SPECS_INDEX.md +398 -0
  23. package/src/claude/scripts/devforgeai_cli/feedback/__init__.py +34 -0
  24. package/src/claude/scripts/devforgeai_cli/feedback/adaptive_questioning_engine.py +581 -0
  25. package/src/claude/scripts/devforgeai_cli/feedback/aggregation.py +179 -0
  26. package/src/claude/scripts/devforgeai_cli/feedback/commands.py +535 -0
  27. package/src/claude/scripts/devforgeai_cli/feedback/config_defaults.py +58 -0
  28. package/src/claude/scripts/devforgeai_cli/feedback/config_manager.py +423 -0
  29. package/src/claude/scripts/devforgeai_cli/feedback/config_models.py +192 -0
  30. package/src/claude/scripts/devforgeai_cli/feedback/config_schema.py +140 -0
  31. package/src/claude/scripts/devforgeai_cli/feedback/coverage.json +1 -0
  32. package/src/claude/scripts/devforgeai_cli/feedback/feature_flag.py +152 -0
  33. package/src/claude/scripts/devforgeai_cli/feedback/feedback_indexer.py +394 -0
  34. package/src/claude/scripts/devforgeai_cli/feedback/hot_reload.py +226 -0
  35. package/src/claude/scripts/devforgeai_cli/feedback/longitudinal.py +115 -0
  36. package/src/claude/scripts/devforgeai_cli/feedback/models.py +67 -0
  37. package/src/claude/scripts/devforgeai_cli/feedback/question_router.py +236 -0
  38. package/src/claude/scripts/devforgeai_cli/feedback/retrospective.py +233 -0
  39. package/src/claude/scripts/devforgeai_cli/feedback/skip_tracker.py +177 -0
  40. package/src/claude/scripts/devforgeai_cli/feedback/skip_tracking.py +221 -0
  41. package/src/claude/scripts/devforgeai_cli/feedback/template_engine.py +549 -0
  42. package/src/claude/scripts/devforgeai_cli/feedback/validation.py +163 -0
  43. package/src/claude/scripts/devforgeai_cli/headless/__init__.py +30 -0
  44. package/src/claude/scripts/devforgeai_cli/headless/answer_models.py +206 -0
  45. package/src/claude/scripts/devforgeai_cli/headless/answer_resolver.py +204 -0
  46. package/src/claude/scripts/devforgeai_cli/headless/exceptions.py +36 -0
  47. package/src/claude/scripts/devforgeai_cli/headless/pattern_matcher.py +156 -0
  48. package/src/claude/scripts/devforgeai_cli/hooks.py +313 -0
  49. package/src/claude/scripts/devforgeai_cli/metrics/__init__.py +46 -0
  50. package/src/claude/scripts/devforgeai_cli/metrics/command_metrics.py +142 -0
  51. package/src/claude/scripts/devforgeai_cli/metrics/failure_modes.py +152 -0
  52. package/src/claude/scripts/devforgeai_cli/metrics/story_segmentation.py +181 -0
  53. package/src/claude/scripts/devforgeai_cli/orchestrate_hooks.py +780 -0
  54. package/src/claude/scripts/devforgeai_cli/phase_state.py +1229 -0
  55. package/src/claude/scripts/devforgeai_cli/session/__init__.py +30 -0
  56. package/src/claude/scripts/devforgeai_cli/session/checkpoint.py +268 -0
  57. package/src/claude/scripts/devforgeai_cli/tests/__init__.py +1 -0
  58. package/src/claude/scripts/devforgeai_cli/tests/conftest.py +29 -0
  59. package/src/claude/scripts/devforgeai_cli/tests/feedback/TEST_EXECUTION_GUIDE.md +298 -0
  60. package/src/claude/scripts/devforgeai_cli/tests/feedback/__init__.py +3 -0
  61. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_adaptive_questioning_engine.py +2171 -0
  62. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_aggregation.py +476 -0
  63. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_defaults.py +133 -0
  64. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_manager.py +592 -0
  65. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_models.py +373 -0
  66. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_schema.py +130 -0
  67. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_configuration_management.py +1355 -0
  68. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_edge_cases.py +308 -0
  69. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_feature_flag.py +307 -0
  70. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_feedback_indexer.py +384 -0
  71. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_hot_reload.py +580 -0
  72. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_integration.py +402 -0
  73. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_models.py +105 -0
  74. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_question_routing.py +262 -0
  75. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_retrospective.py +333 -0
  76. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracker.py +410 -0
  77. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracking.py +159 -0
  78. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracking_integration.py +1155 -0
  79. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_template_engine.py +1389 -0
  80. package/src/claude/scripts/devforgeai_cli/tests/feedback/test_validation_comprehensive.py +210 -0
  81. package/src/claude/scripts/devforgeai_cli/tests/fixtures/autonomous-deferral-story.md +46 -0
  82. package/src/claude/scripts/devforgeai_cli/tests/fixtures/missing-impl-notes.md +31 -0
  83. package/src/claude/scripts/devforgeai_cli/tests/fixtures/valid-deferral-story.md +46 -0
  84. package/src/claude/scripts/devforgeai_cli/tests/fixtures/valid-story-complete.md +48 -0
  85. package/src/claude/scripts/devforgeai_cli/tests/manual_test_invoke_hooks.sh +200 -0
  86. package/src/claude/scripts/devforgeai_cli/tests/session/DELIVERABLES.md +518 -0
  87. package/src/claude/scripts/devforgeai_cli/tests/session/TEST_SUMMARY.md +468 -0
  88. package/src/claude/scripts/devforgeai_cli/tests/session/__init__.py +6 -0
  89. package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/corrupted-checkpoint.json +1 -0
  90. package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/missing-fields-checkpoint.json +4 -0
  91. package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/valid-checkpoint.json +15 -0
  92. package/src/claude/scripts/devforgeai_cli/tests/session/test_checkpoint.py +851 -0
  93. package/src/claude/scripts/devforgeai_cli/tests/test_check_hooks.py +1886 -0
  94. package/src/claude/scripts/devforgeai_cli/tests/test_depends_on_normalizer.py +171 -0
  95. package/src/claude/scripts/devforgeai_cli/tests/test_dod_validator.py +97 -0
  96. package/src/claude/scripts/devforgeai_cli/tests/test_invoke_hooks.py +1902 -0
  97. package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands.py +320 -0
  98. package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands_error_handling.py +1021 -0
  99. package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands_import.py +697 -0
  100. package/src/claude/scripts/devforgeai_cli/tests/test_phase_state.py +2187 -0
  101. package/src/claude/scripts/devforgeai_cli/tests/test_skip_tracking.py +2141 -0
  102. package/src/claude/scripts/devforgeai_cli/tests/test_skip_tracking_coverage_gap.py +195 -0
  103. package/src/claude/scripts/devforgeai_cli/tests/test_subagent_enforcement.py +539 -0
  104. package/src/claude/scripts/devforgeai_cli/tests/test_validate_installation.py +361 -0
  105. package/src/claude/scripts/devforgeai_cli/utils/__init__.py +11 -0
  106. package/src/claude/scripts/devforgeai_cli/utils/depends_on_normalizer.py +149 -0
  107. package/src/claude/scripts/devforgeai_cli/utils/markdown_parser.py +219 -0
  108. package/src/claude/scripts/devforgeai_cli/utils/story_analyzer.py +249 -0
  109. package/src/claude/scripts/devforgeai_cli/utils/yaml_parser.py +152 -0
  110. package/src/claude/scripts/devforgeai_cli/validators/__init__.py +27 -0
  111. package/src/claude/scripts/devforgeai_cli/validators/ast_grep_validator.py +373 -0
  112. package/src/claude/scripts/devforgeai_cli/validators/context_validator.py +180 -0
  113. package/src/claude/scripts/devforgeai_cli/validators/dod_validator.py +309 -0
  114. package/src/claude/scripts/devforgeai_cli/validators/git_validator.py +107 -0
  115. package/src/claude/scripts/devforgeai_cli/validators/grep_fallback.py +300 -0
  116. package/src/claude/scripts/install_hooks.sh +186 -0
  117. package/src/claude/scripts/invoke_feedback_hooks.sh +59 -0
  118. package/src/claude/scripts/migrate-ac-headers.sh +122 -0
  119. package/src/claude/scripts/plan_file_kb.sh +704 -0
  120. package/src/claude/scripts/requirements.txt +8 -0
  121. package/src/claude/scripts/session_catalog.sh +543 -0
  122. package/src/claude/scripts/setup.py +55 -0
  123. package/src/claude/scripts/start-devforgeai.sh +16 -0
  124. package/src/claude/scripts/statusline.sh +27 -0
  125. package/src/claude/scripts/validate_deferrals.py +344 -0
  126. package/src/claude/skills/devforgeai-qa/SKILL.md +1 -1
  127. package/src/claude/skills/researching-market/SKILL.md +2 -1
  128. package/src/cli/lib/copier.js +13 -1
  129. package/src/claude/skills/designing-systems/scripts/__pycache__/detect_anti_patterns.cpython-312.pyc +0 -0
  130. package/src/claude/skills/designing-systems/scripts/__pycache__/validate_all_context.cpython-312.pyc +0 -0
  131. package/src/claude/skills/designing-systems/scripts/__pycache__/validate_architecture.cpython-312.pyc +0 -0
  132. package/src/claude/skills/designing-systems/scripts/__pycache__/validate_dependencies.cpython-312.pyc +0 -0
  133. package/src/claude/skills/devforgeai-story-creation/scripts/__pycache__/migrate_story_v1_to_v2.cpython-312.pyc +0 -0
  134. 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"}