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,539 @@
|
|
|
1
|
+
"""
|
|
2
|
+
STORY-306: Subagent Enforcement Tests
|
|
3
|
+
|
|
4
|
+
Unit tests for SubagentEnforcementError and PHASE_REQUIRED_SUBAGENTS validation.
|
|
5
|
+
Tests the enforcement mechanism that prevents phases from completing without
|
|
6
|
+
required subagents being invoked.
|
|
7
|
+
|
|
8
|
+
Test Coverage:
|
|
9
|
+
- AC1: PHASE_REQUIRED_SUBAGENTS constant validation
|
|
10
|
+
- AC2: subagents_required population on state creation
|
|
11
|
+
- AC3: complete_phase blocking when missing subagents
|
|
12
|
+
- AC4: complete_phase success when all subagents invoked
|
|
13
|
+
- AC5: Escape hatch (checkpoint_passed=False)
|
|
14
|
+
- AC6: OR logic for Phase 03 subagents
|
|
15
|
+
- AC8: Backward compatibility for legacy state files
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import tempfile
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Generator
|
|
22
|
+
|
|
23
|
+
import pytest
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# =============================================================================
|
|
27
|
+
# Test Setup and Fixtures
|
|
28
|
+
# =============================================================================
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.fixture
|
|
32
|
+
def temp_project_root() -> Generator[Path, None, None]:
|
|
33
|
+
"""Create a temporary project root directory for testing."""
|
|
34
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
35
|
+
project_root = Path(tmpdir)
|
|
36
|
+
yield project_root
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.fixture
|
|
40
|
+
def phase_state(temp_project_root: Path):
|
|
41
|
+
"""Create a PhaseState instance with temporary project root."""
|
|
42
|
+
from devforgeai_cli.phase_state import PhaseState
|
|
43
|
+
return PhaseState(project_root=temp_project_root)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# =============================================================================
|
|
47
|
+
# AC1: PHASE_REQUIRED_SUBAGENTS constant
|
|
48
|
+
# =============================================================================
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class TestAC1PhaseRequiredSubagentsConstant:
|
|
52
|
+
"""Tests for AC1: PHASE_REQUIRED_SUBAGENTS constant validation."""
|
|
53
|
+
|
|
54
|
+
def test_constant_exists(self):
|
|
55
|
+
"""
|
|
56
|
+
Given: The phase_state module
|
|
57
|
+
When: PHASE_REQUIRED_SUBAGENTS is imported
|
|
58
|
+
Then: The constant exists and is a dictionary
|
|
59
|
+
"""
|
|
60
|
+
from devforgeai_cli.phase_state import PHASE_REQUIRED_SUBAGENTS
|
|
61
|
+
|
|
62
|
+
assert isinstance(PHASE_REQUIRED_SUBAGENTS, dict)
|
|
63
|
+
|
|
64
|
+
def test_constant_contains_all_12_phases(self):
|
|
65
|
+
"""
|
|
66
|
+
Given: PHASE_REQUIRED_SUBAGENTS constant
|
|
67
|
+
When: Inspecting the keys
|
|
68
|
+
Then: Contains all 12 valid phases (01-10, 4.5, 5.5)
|
|
69
|
+
"""
|
|
70
|
+
from devforgeai_cli.phase_state import PHASE_REQUIRED_SUBAGENTS
|
|
71
|
+
|
|
72
|
+
expected_phases = ["01", "02", "03", "04", "4.5", "05", "5.5",
|
|
73
|
+
"06", "07", "08", "09", "10"]
|
|
74
|
+
|
|
75
|
+
for phase in expected_phases:
|
|
76
|
+
assert phase in PHASE_REQUIRED_SUBAGENTS, f"Missing phase: {phase}"
|
|
77
|
+
|
|
78
|
+
def test_phase_09_contains_framework_analyst(self):
|
|
79
|
+
"""
|
|
80
|
+
Given: PHASE_REQUIRED_SUBAGENTS constant
|
|
81
|
+
When: Inspecting Phase 09 entry
|
|
82
|
+
Then: Contains 'framework-analyst'
|
|
83
|
+
"""
|
|
84
|
+
from devforgeai_cli.phase_state import PHASE_REQUIRED_SUBAGENTS
|
|
85
|
+
|
|
86
|
+
assert "framework-analyst" in PHASE_REQUIRED_SUBAGENTS["09"]
|
|
87
|
+
|
|
88
|
+
def test_phase_03_uses_tuple_for_or_logic(self):
|
|
89
|
+
"""
|
|
90
|
+
Given: PHASE_REQUIRED_SUBAGENTS constant
|
|
91
|
+
When: Inspecting Phase 03 entry
|
|
92
|
+
Then: Uses tuple for OR logic (backend-architect, frontend-developer)
|
|
93
|
+
"""
|
|
94
|
+
from devforgeai_cli.phase_state import PHASE_REQUIRED_SUBAGENTS
|
|
95
|
+
|
|
96
|
+
phase_03_requirements = PHASE_REQUIRED_SUBAGENTS["03"]
|
|
97
|
+
|
|
98
|
+
# Find the OR group (tuple)
|
|
99
|
+
or_group = None
|
|
100
|
+
for requirement in phase_03_requirements:
|
|
101
|
+
if isinstance(requirement, tuple):
|
|
102
|
+
or_group = requirement
|
|
103
|
+
break
|
|
104
|
+
|
|
105
|
+
assert or_group is not None, "Phase 03 should have a tuple for OR logic"
|
|
106
|
+
assert "backend-architect" in or_group
|
|
107
|
+
assert "frontend-developer" in or_group
|
|
108
|
+
|
|
109
|
+
def test_phase_01_contains_git_validator_and_tech_stack_detector(self):
|
|
110
|
+
"""
|
|
111
|
+
Given: PHASE_REQUIRED_SUBAGENTS constant
|
|
112
|
+
When: Inspecting Phase 01 entry
|
|
113
|
+
Then: Contains git-validator and tech-stack-detector
|
|
114
|
+
"""
|
|
115
|
+
from devforgeai_cli.phase_state import PHASE_REQUIRED_SUBAGENTS
|
|
116
|
+
|
|
117
|
+
assert "git-validator" in PHASE_REQUIRED_SUBAGENTS["01"]
|
|
118
|
+
assert "tech-stack-detector" in PHASE_REQUIRED_SUBAGENTS["01"]
|
|
119
|
+
|
|
120
|
+
def test_phase_02_contains_test_automator(self):
|
|
121
|
+
"""
|
|
122
|
+
Given: PHASE_REQUIRED_SUBAGENTS constant
|
|
123
|
+
When: Inspecting Phase 02 entry
|
|
124
|
+
Then: Contains test-automator
|
|
125
|
+
"""
|
|
126
|
+
from devforgeai_cli.phase_state import PHASE_REQUIRED_SUBAGENTS
|
|
127
|
+
|
|
128
|
+
assert "test-automator" in PHASE_REQUIRED_SUBAGENTS["02"]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# =============================================================================
|
|
132
|
+
# AC2: subagents_required populated on state creation
|
|
133
|
+
# =============================================================================
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class TestAC2SubagentsRequiredPopulation:
|
|
137
|
+
"""Tests for AC2: subagents_required populated from PHASE_REQUIRED_SUBAGENTS."""
|
|
138
|
+
|
|
139
|
+
def test_new_state_has_populated_subagents_required(self, phase_state):
|
|
140
|
+
"""
|
|
141
|
+
Given: A new phase state is created
|
|
142
|
+
When: create() is called
|
|
143
|
+
Then: Each phase has non-empty subagents_required (where applicable)
|
|
144
|
+
"""
|
|
145
|
+
state = phase_state.create("STORY-001")
|
|
146
|
+
|
|
147
|
+
# Phase 02 should have test-automator
|
|
148
|
+
assert "test-automator" in state["phases"]["02"]["subagents_required"]
|
|
149
|
+
|
|
150
|
+
def test_phase_02_subagents_required_contains_test_automator(self, phase_state):
|
|
151
|
+
"""
|
|
152
|
+
Given: A new phase state
|
|
153
|
+
When: Inspecting phases.02.subagents_required
|
|
154
|
+
Then: Contains 'test-automator'
|
|
155
|
+
"""
|
|
156
|
+
state = phase_state.create("STORY-001")
|
|
157
|
+
|
|
158
|
+
assert "test-automator" in state["phases"]["02"]["subagents_required"]
|
|
159
|
+
|
|
160
|
+
def test_phase_09_subagents_required_contains_framework_analyst(self, phase_state):
|
|
161
|
+
"""
|
|
162
|
+
Given: A new phase state
|
|
163
|
+
When: Inspecting phases.09.subagents_required
|
|
164
|
+
Then: Contains 'framework-analyst'
|
|
165
|
+
"""
|
|
166
|
+
state = phase_state.create("STORY-001")
|
|
167
|
+
|
|
168
|
+
assert "framework-analyst" in state["phases"]["09"]["subagents_required"]
|
|
169
|
+
|
|
170
|
+
def test_phase_07_has_empty_subagents_required(self, phase_state):
|
|
171
|
+
"""
|
|
172
|
+
Given: A new phase state
|
|
173
|
+
When: Inspecting Phase 07 (no required subagents)
|
|
174
|
+
Then: subagents_required is empty
|
|
175
|
+
"""
|
|
176
|
+
state = phase_state.create("STORY-001")
|
|
177
|
+
|
|
178
|
+
assert state["phases"]["07"]["subagents_required"] == []
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# =============================================================================
|
|
182
|
+
# AC3: complete_phase blocks when missing subagents
|
|
183
|
+
# =============================================================================
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class TestAC3BlockingOnMissingSubagents:
|
|
187
|
+
"""Tests for AC3: complete_phase blocks when required subagents not invoked."""
|
|
188
|
+
|
|
189
|
+
def test_raises_subagent_enforcement_error_when_missing(self, phase_state):
|
|
190
|
+
"""
|
|
191
|
+
Given: Phase 02 requires test-automator, not invoked
|
|
192
|
+
When: complete_phase with checkpoint_passed=True
|
|
193
|
+
Then: SubagentEnforcementError is raised
|
|
194
|
+
"""
|
|
195
|
+
from devforgeai_cli.phase_state import SubagentEnforcementError
|
|
196
|
+
|
|
197
|
+
phase_state.create("STORY-001")
|
|
198
|
+
# Complete phase 01 first
|
|
199
|
+
phase_state.record_subagent("STORY-001", "01", "git-validator")
|
|
200
|
+
phase_state.record_subagent("STORY-001", "01", "tech-stack-detector")
|
|
201
|
+
phase_state.complete_phase("STORY-001", "01", checkpoint_passed=True)
|
|
202
|
+
|
|
203
|
+
# Try to complete phase 02 without invoking test-automator
|
|
204
|
+
with pytest.raises(SubagentEnforcementError) as exc_info:
|
|
205
|
+
phase_state.complete_phase("STORY-001", "02", checkpoint_passed=True)
|
|
206
|
+
|
|
207
|
+
assert "test-automator" in str(exc_info.value)
|
|
208
|
+
|
|
209
|
+
def test_error_message_identifies_missing_subagent(self, phase_state):
|
|
210
|
+
"""
|
|
211
|
+
Given: Phase 02 missing test-automator
|
|
212
|
+
When: SubagentEnforcementError is raised
|
|
213
|
+
Then: Error message contains 'test-automator'
|
|
214
|
+
"""
|
|
215
|
+
from devforgeai_cli.phase_state import SubagentEnforcementError
|
|
216
|
+
|
|
217
|
+
phase_state.create("STORY-001")
|
|
218
|
+
phase_state.record_subagent("STORY-001", "01", "git-validator")
|
|
219
|
+
phase_state.record_subagent("STORY-001", "01", "tech-stack-detector")
|
|
220
|
+
phase_state.complete_phase("STORY-001", "01", checkpoint_passed=True)
|
|
221
|
+
|
|
222
|
+
with pytest.raises(SubagentEnforcementError) as exc_info:
|
|
223
|
+
phase_state.complete_phase("STORY-001", "02", checkpoint_passed=True)
|
|
224
|
+
|
|
225
|
+
error = exc_info.value
|
|
226
|
+
assert "test-automator" in error.missing_subagents
|
|
227
|
+
|
|
228
|
+
def test_error_has_story_id_phase_and_missing_attributes(self, phase_state):
|
|
229
|
+
"""
|
|
230
|
+
Given: SubagentEnforcementError is raised
|
|
231
|
+
When: Inspecting exception attributes
|
|
232
|
+
Then: Contains story_id, phase, and missing_subagents
|
|
233
|
+
"""
|
|
234
|
+
from devforgeai_cli.phase_state import SubagentEnforcementError
|
|
235
|
+
|
|
236
|
+
phase_state.create("STORY-001")
|
|
237
|
+
phase_state.record_subagent("STORY-001", "01", "git-validator")
|
|
238
|
+
phase_state.record_subagent("STORY-001", "01", "tech-stack-detector")
|
|
239
|
+
phase_state.complete_phase("STORY-001", "01", checkpoint_passed=True)
|
|
240
|
+
|
|
241
|
+
with pytest.raises(SubagentEnforcementError) as exc_info:
|
|
242
|
+
phase_state.complete_phase("STORY-001", "02", checkpoint_passed=True)
|
|
243
|
+
|
|
244
|
+
error = exc_info.value
|
|
245
|
+
assert error.story_id == "STORY-001"
|
|
246
|
+
assert error.phase == "02"
|
|
247
|
+
assert isinstance(error.missing_subagents, list)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# =============================================================================
|
|
251
|
+
# AC4: complete_phase succeeds when all subagents invoked
|
|
252
|
+
# =============================================================================
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class TestAC4SuccessWhenSubagentsInvoked:
|
|
256
|
+
"""Tests for AC4: complete_phase succeeds when all required subagents invoked."""
|
|
257
|
+
|
|
258
|
+
def test_phase_completes_when_required_subagents_recorded(self, phase_state):
|
|
259
|
+
"""
|
|
260
|
+
Given: Phase 02 requires test-automator, invoked
|
|
261
|
+
When: complete_phase with checkpoint_passed=True
|
|
262
|
+
Then: Phase status is 'completed'
|
|
263
|
+
"""
|
|
264
|
+
phase_state.create("STORY-001")
|
|
265
|
+
phase_state.record_subagent("STORY-001", "01", "git-validator")
|
|
266
|
+
phase_state.record_subagent("STORY-001", "01", "tech-stack-detector")
|
|
267
|
+
phase_state.complete_phase("STORY-001", "01", checkpoint_passed=True)
|
|
268
|
+
|
|
269
|
+
# Record test-automator for phase 02
|
|
270
|
+
phase_state.record_subagent("STORY-001", "02", "test-automator")
|
|
271
|
+
state = phase_state.complete_phase("STORY-001", "02", checkpoint_passed=True)
|
|
272
|
+
|
|
273
|
+
assert state["phases"]["02"]["status"] == "completed"
|
|
274
|
+
|
|
275
|
+
def test_current_phase_advances_after_success(self, phase_state):
|
|
276
|
+
"""
|
|
277
|
+
Given: Phase 02 completed successfully
|
|
278
|
+
When: complete_phase returns
|
|
279
|
+
Then: current_phase advances to '03'
|
|
280
|
+
"""
|
|
281
|
+
phase_state.create("STORY-001")
|
|
282
|
+
phase_state.record_subagent("STORY-001", "01", "git-validator")
|
|
283
|
+
phase_state.record_subagent("STORY-001", "01", "tech-stack-detector")
|
|
284
|
+
phase_state.complete_phase("STORY-001", "01", checkpoint_passed=True)
|
|
285
|
+
|
|
286
|
+
phase_state.record_subagent("STORY-001", "02", "test-automator")
|
|
287
|
+
state = phase_state.complete_phase("STORY-001", "02", checkpoint_passed=True)
|
|
288
|
+
|
|
289
|
+
assert state["current_phase"] == "03"
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
# =============================================================================
|
|
293
|
+
# AC5: Escape hatch (checkpoint_passed=False)
|
|
294
|
+
# =============================================================================
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class TestAC5EscapeHatch:
|
|
298
|
+
"""Tests for AC5: Escape hatch allows completion without subagent validation."""
|
|
299
|
+
|
|
300
|
+
def test_completes_without_subagents_when_checkpoint_passed_false(self, phase_state):
|
|
301
|
+
"""
|
|
302
|
+
Given: Phase 02 missing required subagents
|
|
303
|
+
When: complete_phase with checkpoint_passed=False
|
|
304
|
+
Then: Phase completes without error
|
|
305
|
+
"""
|
|
306
|
+
phase_state.create("STORY-001")
|
|
307
|
+
phase_state.record_subagent("STORY-001", "01", "git-validator")
|
|
308
|
+
phase_state.record_subagent("STORY-001", "01", "tech-stack-detector")
|
|
309
|
+
phase_state.complete_phase("STORY-001", "01", checkpoint_passed=True)
|
|
310
|
+
|
|
311
|
+
# Complete phase 02 without invoking test-automator using escape hatch
|
|
312
|
+
state = phase_state.complete_phase("STORY-001", "02", checkpoint_passed=False)
|
|
313
|
+
|
|
314
|
+
assert state["phases"]["02"]["status"] == "completed"
|
|
315
|
+
|
|
316
|
+
def test_checkpoint_passed_stored_as_false(self, phase_state):
|
|
317
|
+
"""
|
|
318
|
+
Given: Escape hatch used
|
|
319
|
+
When: Phase completed with checkpoint_passed=False
|
|
320
|
+
Then: checkpoint_passed is False in state file
|
|
321
|
+
"""
|
|
322
|
+
phase_state.create("STORY-001")
|
|
323
|
+
phase_state.record_subagent("STORY-001", "01", "git-validator")
|
|
324
|
+
phase_state.record_subagent("STORY-001", "01", "tech-stack-detector")
|
|
325
|
+
phase_state.complete_phase("STORY-001", "01", checkpoint_passed=True)
|
|
326
|
+
|
|
327
|
+
state = phase_state.complete_phase("STORY-001", "02", checkpoint_passed=False)
|
|
328
|
+
|
|
329
|
+
assert state["phases"]["02"]["checkpoint_passed"] is False
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
# =============================================================================
|
|
333
|
+
# AC6: OR logic for Phase 03 subagents
|
|
334
|
+
# =============================================================================
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
class TestAC6ORLogicPhase03:
|
|
338
|
+
"""Tests for AC6: OR logic for Phase 03 subagents."""
|
|
339
|
+
|
|
340
|
+
def test_completes_with_backend_architect_only(self, phase_state):
|
|
341
|
+
"""
|
|
342
|
+
Given: Phase 03 requires backend-architect OR frontend-developer
|
|
343
|
+
When: Only backend-architect invoked (plus context-validator)
|
|
344
|
+
Then: Phase 03 completes successfully
|
|
345
|
+
"""
|
|
346
|
+
phase_state.create("STORY-001")
|
|
347
|
+
|
|
348
|
+
# Complete phases 01-02 first
|
|
349
|
+
phase_state.record_subagent("STORY-001", "01", "git-validator")
|
|
350
|
+
phase_state.record_subagent("STORY-001", "01", "tech-stack-detector")
|
|
351
|
+
phase_state.complete_phase("STORY-001", "01", checkpoint_passed=True)
|
|
352
|
+
|
|
353
|
+
phase_state.record_subagent("STORY-001", "02", "test-automator")
|
|
354
|
+
phase_state.complete_phase("STORY-001", "02", checkpoint_passed=True)
|
|
355
|
+
|
|
356
|
+
# Complete phase 03 with only backend-architect
|
|
357
|
+
phase_state.record_subagent("STORY-001", "03", "backend-architect")
|
|
358
|
+
phase_state.record_subagent("STORY-001", "03", "context-validator")
|
|
359
|
+
state = phase_state.complete_phase("STORY-001", "03", checkpoint_passed=True)
|
|
360
|
+
|
|
361
|
+
assert state["phases"]["03"]["status"] == "completed"
|
|
362
|
+
|
|
363
|
+
def test_completes_with_frontend_developer_only(self, phase_state):
|
|
364
|
+
"""
|
|
365
|
+
Given: Phase 03 requires backend-architect OR frontend-developer
|
|
366
|
+
When: Only frontend-developer invoked (plus context-validator)
|
|
367
|
+
Then: Phase 03 completes successfully
|
|
368
|
+
"""
|
|
369
|
+
phase_state.create("STORY-001")
|
|
370
|
+
|
|
371
|
+
# Complete phases 01-02 first
|
|
372
|
+
phase_state.record_subagent("STORY-001", "01", "git-validator")
|
|
373
|
+
phase_state.record_subagent("STORY-001", "01", "tech-stack-detector")
|
|
374
|
+
phase_state.complete_phase("STORY-001", "01", checkpoint_passed=True)
|
|
375
|
+
|
|
376
|
+
phase_state.record_subagent("STORY-001", "02", "test-automator")
|
|
377
|
+
phase_state.complete_phase("STORY-001", "02", checkpoint_passed=True)
|
|
378
|
+
|
|
379
|
+
# Complete phase 03 with only frontend-developer
|
|
380
|
+
phase_state.record_subagent("STORY-001", "03", "frontend-developer")
|
|
381
|
+
phase_state.record_subagent("STORY-001", "03", "context-validator")
|
|
382
|
+
state = phase_state.complete_phase("STORY-001", "03", checkpoint_passed=True)
|
|
383
|
+
|
|
384
|
+
assert state["phases"]["03"]["status"] == "completed"
|
|
385
|
+
|
|
386
|
+
def test_blocks_with_neither_or_subagent(self, phase_state):
|
|
387
|
+
"""
|
|
388
|
+
Given: Phase 03 requires backend-architect OR frontend-developer
|
|
389
|
+
When: Neither invoked (only context-validator)
|
|
390
|
+
Then: SubagentEnforcementError raised
|
|
391
|
+
"""
|
|
392
|
+
from devforgeai_cli.phase_state import SubagentEnforcementError
|
|
393
|
+
|
|
394
|
+
phase_state.create("STORY-001")
|
|
395
|
+
|
|
396
|
+
# Complete phases 01-02 first
|
|
397
|
+
phase_state.record_subagent("STORY-001", "01", "git-validator")
|
|
398
|
+
phase_state.record_subagent("STORY-001", "01", "tech-stack-detector")
|
|
399
|
+
phase_state.complete_phase("STORY-001", "01", checkpoint_passed=True)
|
|
400
|
+
|
|
401
|
+
phase_state.record_subagent("STORY-001", "02", "test-automator")
|
|
402
|
+
phase_state.complete_phase("STORY-001", "02", checkpoint_passed=True)
|
|
403
|
+
|
|
404
|
+
# Try to complete phase 03 with only context-validator
|
|
405
|
+
phase_state.record_subagent("STORY-001", "03", "context-validator")
|
|
406
|
+
|
|
407
|
+
with pytest.raises(SubagentEnforcementError):
|
|
408
|
+
phase_state.complete_phase("STORY-001", "03", checkpoint_passed=True)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
# =============================================================================
|
|
412
|
+
# AC8: Backward compatibility for legacy state files
|
|
413
|
+
# =============================================================================
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
class TestAC8BackwardCompatibility:
|
|
417
|
+
"""Tests for AC8: Backward compatibility for legacy state files."""
|
|
418
|
+
|
|
419
|
+
def test_legacy_state_file_loads_successfully(self, temp_project_root):
|
|
420
|
+
"""
|
|
421
|
+
Given: A legacy state file with empty subagents_required
|
|
422
|
+
When: Loaded via read()
|
|
423
|
+
Then: No error, state is returned
|
|
424
|
+
"""
|
|
425
|
+
from devforgeai_cli.phase_state import PhaseState
|
|
426
|
+
|
|
427
|
+
# Create legacy state file manually
|
|
428
|
+
workflows_dir = temp_project_root / "devforgeai" / "workflows"
|
|
429
|
+
workflows_dir.mkdir(parents=True, exist_ok=True)
|
|
430
|
+
|
|
431
|
+
legacy_state = {
|
|
432
|
+
"story_id": "STORY-001",
|
|
433
|
+
"current_phase": "01",
|
|
434
|
+
"workflow_started": "2026-01-01T00:00:00Z",
|
|
435
|
+
"blocking_status": False,
|
|
436
|
+
"phases": {
|
|
437
|
+
f"{i:02d}": {
|
|
438
|
+
"status": "pending",
|
|
439
|
+
"subagents_required": [], # Empty - legacy format
|
|
440
|
+
"subagents_invoked": []
|
|
441
|
+
} for i in range(1, 11)
|
|
442
|
+
},
|
|
443
|
+
"validation_errors": [],
|
|
444
|
+
"observations": []
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
state_path = workflows_dir / "STORY-001-phase-state.json"
|
|
448
|
+
state_path.write_text(json.dumps(legacy_state, indent=2))
|
|
449
|
+
|
|
450
|
+
ps = PhaseState(project_root=temp_project_root)
|
|
451
|
+
state = ps.read("STORY-001")
|
|
452
|
+
|
|
453
|
+
assert state is not None
|
|
454
|
+
assert state["story_id"] == "STORY-001"
|
|
455
|
+
|
|
456
|
+
def test_legacy_state_file_gets_subagents_required_populated(self, temp_project_root):
|
|
457
|
+
"""
|
|
458
|
+
Given: A legacy state file with empty subagents_required
|
|
459
|
+
When: Loaded via read()
|
|
460
|
+
Then: subagents_required is populated from PHASE_REQUIRED_SUBAGENTS
|
|
461
|
+
"""
|
|
462
|
+
from devforgeai_cli.phase_state import PhaseState
|
|
463
|
+
|
|
464
|
+
# Create legacy state file manually
|
|
465
|
+
workflows_dir = temp_project_root / "devforgeai" / "workflows"
|
|
466
|
+
workflows_dir.mkdir(parents=True, exist_ok=True)
|
|
467
|
+
|
|
468
|
+
legacy_state = {
|
|
469
|
+
"story_id": "STORY-001",
|
|
470
|
+
"current_phase": "01",
|
|
471
|
+
"workflow_started": "2026-01-01T00:00:00Z",
|
|
472
|
+
"blocking_status": False,
|
|
473
|
+
"phases": {
|
|
474
|
+
f"{i:02d}": {
|
|
475
|
+
"status": "pending",
|
|
476
|
+
"subagents_required": [], # Empty - legacy format
|
|
477
|
+
"subagents_invoked": []
|
|
478
|
+
} for i in range(1, 11)
|
|
479
|
+
},
|
|
480
|
+
"validation_errors": [],
|
|
481
|
+
"observations": []
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
state_path = workflows_dir / "STORY-001-phase-state.json"
|
|
485
|
+
state_path.write_text(json.dumps(legacy_state, indent=2))
|
|
486
|
+
|
|
487
|
+
ps = PhaseState(project_root=temp_project_root)
|
|
488
|
+
state = ps.read("STORY-001")
|
|
489
|
+
|
|
490
|
+
# After loading, subagents_required should be populated
|
|
491
|
+
assert "test-automator" in state["phases"]["02"]["subagents_required"]
|
|
492
|
+
assert "framework-analyst" in state["phases"]["09"]["subagents_required"]
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
# =============================================================================
|
|
496
|
+
# SubagentEnforcementError Exception Tests
|
|
497
|
+
# =============================================================================
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
class TestSubagentEnforcementError:
|
|
501
|
+
"""Tests for SubagentEnforcementError exception class."""
|
|
502
|
+
|
|
503
|
+
def test_exception_inherits_from_phase_state_error(self):
|
|
504
|
+
"""SubagentEnforcementError should inherit from PhaseStateError."""
|
|
505
|
+
from devforgeai_cli.phase_state import (
|
|
506
|
+
PhaseStateError,
|
|
507
|
+
SubagentEnforcementError
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
assert issubclass(SubagentEnforcementError, PhaseStateError)
|
|
511
|
+
|
|
512
|
+
def test_exception_stores_attributes(self):
|
|
513
|
+
"""SubagentEnforcementError stores story_id, phase, missing_subagents."""
|
|
514
|
+
from devforgeai_cli.phase_state import SubagentEnforcementError
|
|
515
|
+
|
|
516
|
+
error = SubagentEnforcementError(
|
|
517
|
+
story_id="STORY-001",
|
|
518
|
+
phase="02",
|
|
519
|
+
missing_subagents=["test-automator", "code-reviewer"]
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
assert error.story_id == "STORY-001"
|
|
523
|
+
assert error.phase == "02"
|
|
524
|
+
assert error.missing_subagents == ["test-automator", "code-reviewer"]
|
|
525
|
+
|
|
526
|
+
def test_exception_message_format(self):
|
|
527
|
+
"""SubagentEnforcementError message includes all details."""
|
|
528
|
+
from devforgeai_cli.phase_state import SubagentEnforcementError
|
|
529
|
+
|
|
530
|
+
error = SubagentEnforcementError(
|
|
531
|
+
story_id="STORY-001",
|
|
532
|
+
phase="02",
|
|
533
|
+
missing_subagents=["test-automator"]
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
message = str(error)
|
|
537
|
+
assert "STORY-001" in message
|
|
538
|
+
assert "02" in message
|
|
539
|
+
assert "test-automator" in message
|