devforgeai 1.0.5 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +120 -0
- package/bin/devforgeai.js +0 -0
- package/package.json +9 -1
- package/src/CLAUDE.md +699 -0
- package/src/claude/hooks/phase-completion-gate.sh +0 -0
- package/src/claude/scripts/README.md +396 -0
- package/src/claude/scripts/audit-command-skill-overlap.sh +67 -0
- package/src/claude/scripts/check-hooks-fast.sh +70 -0
- package/src/claude/scripts/devforgeai-validate +6 -0
- package/src/claude/scripts/devforgeai_cli/README.md +531 -0
- package/src/claude/scripts/devforgeai_cli/__init__.py +12 -0
- package/src/claude/scripts/devforgeai_cli/cli.py +716 -0
- package/src/claude/scripts/devforgeai_cli/commands/__init__.py +1 -0
- package/src/claude/scripts/devforgeai_cli/commands/check_hooks.py +384 -0
- package/src/claude/scripts/devforgeai_cli/commands/invoke_hooks.py +149 -0
- package/src/claude/scripts/devforgeai_cli/commands/phase_commands.py +731 -0
- package/src/claude/scripts/devforgeai_cli/commands/validate_installation.py +412 -0
- package/src/claude/scripts/devforgeai_cli/context_extraction.py +426 -0
- package/src/claude/scripts/devforgeai_cli/feedback/AC_TO_TEST_MAPPING.md +636 -0
- package/src/claude/scripts/devforgeai_cli/feedback/DELIVERY_SUMMARY.txt +329 -0
- package/src/claude/scripts/devforgeai_cli/feedback/README_TEST_SPECS.md +486 -0
- package/src/claude/scripts/devforgeai_cli/feedback/TEST_IMPLEMENTATION_GUIDE.md +529 -0
- package/src/claude/scripts/devforgeai_cli/feedback/TEST_SPECIFICATIONS.md +2652 -0
- package/src/claude/scripts/devforgeai_cli/feedback/TEST_SPECS_INDEX.md +398 -0
- package/src/claude/scripts/devforgeai_cli/feedback/__init__.py +34 -0
- package/src/claude/scripts/devforgeai_cli/feedback/adaptive_questioning_engine.py +581 -0
- package/src/claude/scripts/devforgeai_cli/feedback/aggregation.py +179 -0
- package/src/claude/scripts/devforgeai_cli/feedback/commands.py +535 -0
- package/src/claude/scripts/devforgeai_cli/feedback/config_defaults.py +58 -0
- package/src/claude/scripts/devforgeai_cli/feedback/config_manager.py +423 -0
- package/src/claude/scripts/devforgeai_cli/feedback/config_models.py +192 -0
- package/src/claude/scripts/devforgeai_cli/feedback/config_schema.py +140 -0
- package/src/claude/scripts/devforgeai_cli/feedback/coverage.json +1 -0
- package/src/claude/scripts/devforgeai_cli/feedback/feature_flag.py +152 -0
- package/src/claude/scripts/devforgeai_cli/feedback/feedback_indexer.py +394 -0
- package/src/claude/scripts/devforgeai_cli/feedback/hot_reload.py +226 -0
- package/src/claude/scripts/devforgeai_cli/feedback/longitudinal.py +115 -0
- package/src/claude/scripts/devforgeai_cli/feedback/models.py +67 -0
- package/src/claude/scripts/devforgeai_cli/feedback/question_router.py +236 -0
- package/src/claude/scripts/devforgeai_cli/feedback/retrospective.py +233 -0
- package/src/claude/scripts/devforgeai_cli/feedback/skip_tracker.py +177 -0
- package/src/claude/scripts/devforgeai_cli/feedback/skip_tracking.py +221 -0
- package/src/claude/scripts/devforgeai_cli/feedback/template_engine.py +549 -0
- package/src/claude/scripts/devforgeai_cli/feedback/validation.py +163 -0
- package/src/claude/scripts/devforgeai_cli/headless/__init__.py +30 -0
- package/src/claude/scripts/devforgeai_cli/headless/answer_models.py +206 -0
- package/src/claude/scripts/devforgeai_cli/headless/answer_resolver.py +204 -0
- package/src/claude/scripts/devforgeai_cli/headless/exceptions.py +36 -0
- package/src/claude/scripts/devforgeai_cli/headless/pattern_matcher.py +156 -0
- package/src/claude/scripts/devforgeai_cli/hooks.py +313 -0
- package/src/claude/scripts/devforgeai_cli/metrics/__init__.py +46 -0
- package/src/claude/scripts/devforgeai_cli/metrics/command_metrics.py +142 -0
- package/src/claude/scripts/devforgeai_cli/metrics/failure_modes.py +152 -0
- package/src/claude/scripts/devforgeai_cli/metrics/story_segmentation.py +181 -0
- package/src/claude/scripts/devforgeai_cli/orchestrate_hooks.py +780 -0
- package/src/claude/scripts/devforgeai_cli/phase_state.py +1229 -0
- package/src/claude/scripts/devforgeai_cli/session/__init__.py +30 -0
- package/src/claude/scripts/devforgeai_cli/session/checkpoint.py +268 -0
- package/src/claude/scripts/devforgeai_cli/tests/__init__.py +1 -0
- package/src/claude/scripts/devforgeai_cli/tests/conftest.py +29 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/TEST_EXECUTION_GUIDE.md +298 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/__init__.py +3 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_adaptive_questioning_engine.py +2171 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_aggregation.py +476 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_defaults.py +133 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_manager.py +592 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_models.py +373 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_config_schema.py +130 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_configuration_management.py +1355 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_edge_cases.py +308 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_feature_flag.py +307 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_feedback_indexer.py +384 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_hot_reload.py +580 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_integration.py +402 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_models.py +105 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_question_routing.py +262 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_retrospective.py +333 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracker.py +410 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracking.py +159 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_skip_tracking_integration.py +1155 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_template_engine.py +1389 -0
- package/src/claude/scripts/devforgeai_cli/tests/feedback/test_validation_comprehensive.py +210 -0
- package/src/claude/scripts/devforgeai_cli/tests/fixtures/autonomous-deferral-story.md +46 -0
- package/src/claude/scripts/devforgeai_cli/tests/fixtures/missing-impl-notes.md +31 -0
- package/src/claude/scripts/devforgeai_cli/tests/fixtures/valid-deferral-story.md +46 -0
- package/src/claude/scripts/devforgeai_cli/tests/fixtures/valid-story-complete.md +48 -0
- package/src/claude/scripts/devforgeai_cli/tests/manual_test_invoke_hooks.sh +200 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/DELIVERABLES.md +518 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/TEST_SUMMARY.md +468 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/__init__.py +6 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/corrupted-checkpoint.json +1 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/missing-fields-checkpoint.json +4 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/fixtures/valid-checkpoint.json +15 -0
- package/src/claude/scripts/devforgeai_cli/tests/session/test_checkpoint.py +851 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_check_hooks.py +1886 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_depends_on_normalizer.py +171 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_dod_validator.py +97 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_invoke_hooks.py +1902 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands.py +320 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands_error_handling.py +1021 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_phase_commands_import.py +697 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_phase_state.py +2187 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_skip_tracking.py +2141 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_skip_tracking_coverage_gap.py +195 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_subagent_enforcement.py +539 -0
- package/src/claude/scripts/devforgeai_cli/tests/test_validate_installation.py +361 -0
- package/src/claude/scripts/devforgeai_cli/utils/__init__.py +11 -0
- package/src/claude/scripts/devforgeai_cli/utils/depends_on_normalizer.py +149 -0
- package/src/claude/scripts/devforgeai_cli/utils/markdown_parser.py +219 -0
- package/src/claude/scripts/devforgeai_cli/utils/story_analyzer.py +249 -0
- package/src/claude/scripts/devforgeai_cli/utils/yaml_parser.py +152 -0
- package/src/claude/scripts/devforgeai_cli/validators/__init__.py +27 -0
- package/src/claude/scripts/devforgeai_cli/validators/ast_grep_validator.py +373 -0
- package/src/claude/scripts/devforgeai_cli/validators/context_validator.py +180 -0
- package/src/claude/scripts/devforgeai_cli/validators/dod_validator.py +309 -0
- package/src/claude/scripts/devforgeai_cli/validators/git_validator.py +107 -0
- package/src/claude/scripts/devforgeai_cli/validators/grep_fallback.py +300 -0
- package/src/claude/scripts/install_hooks.sh +186 -0
- package/src/claude/scripts/invoke_feedback_hooks.sh +59 -0
- package/src/claude/scripts/migrate-ac-headers.sh +122 -0
- package/src/claude/scripts/plan_file_kb.sh +704 -0
- package/src/claude/scripts/requirements.txt +8 -0
- package/src/claude/scripts/session_catalog.sh +543 -0
- package/src/claude/scripts/setup.py +55 -0
- package/src/claude/scripts/start-devforgeai.sh +16 -0
- package/src/claude/scripts/statusline.sh +27 -0
- package/src/claude/scripts/validate_deferrals.py +344 -0
- package/src/claude/skills/designing-systems/scripts/__pycache__/detect_anti_patterns.cpython-312.pyc +0 -0
- package/src/claude/skills/designing-systems/scripts/__pycache__/validate_all_context.cpython-312.pyc +0 -0
- package/src/claude/skills/designing-systems/scripts/__pycache__/validate_architecture.cpython-312.pyc +0 -0
- package/src/claude/skills/designing-systems/scripts/__pycache__/validate_dependencies.cpython-312.pyc +0 -0
- package/src/claude/skills/devforgeai-story-creation/scripts/__pycache__/migrate_story_v1_to_v2.cpython-312.pyc +0 -0
- package/src/claude/skills/devforgeai-story-creation/scripts/tests/__pycache__/measure_accuracy.cpython-312.pyc +0 -0
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for STORY-254: Update phase_commands.py Import.
|
|
3
|
+
|
|
4
|
+
TDD Red Phase: These tests verify the import refactoring from sys.path
|
|
5
|
+
manipulation to relative imports (from ..phase_state import PhaseState).
|
|
6
|
+
|
|
7
|
+
Acceptance Criteria:
|
|
8
|
+
- AC#1: Relative import replaces sys.path manipulation
|
|
9
|
+
- AC#2: Function behavior remains unchanged
|
|
10
|
+
- AC#3: All phase commands still work correctly
|
|
11
|
+
|
|
12
|
+
Technical Specification Requirements:
|
|
13
|
+
- SVC-001: Replace sys.path manipulation with relative import (Critical)
|
|
14
|
+
- SVC-002: Function returns PhaseState instance unchanged (Critical)
|
|
15
|
+
- SVC-003: All 6 phase commands work after refactor (Critical)
|
|
16
|
+
- SVC-004: Remove unused sys import if applicable (Low)
|
|
17
|
+
- BR-001: Import path must be `from ..phase_state` (Critical)
|
|
18
|
+
- NFR-001: Import latency < 5ms (Medium)
|
|
19
|
+
- NFR-002: Function reduced from 8 lines to 3 lines (High)
|
|
20
|
+
|
|
21
|
+
Test Framework: pytest (per tech-stack.md)
|
|
22
|
+
Test Naming: test_<function>_<scenario>_<expected>
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import ast
|
|
26
|
+
import inspect
|
|
27
|
+
import json
|
|
28
|
+
import re
|
|
29
|
+
import sys
|
|
30
|
+
import tempfile
|
|
31
|
+
import time
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from typing import Callable, List
|
|
34
|
+
from unittest.mock import patch, MagicMock
|
|
35
|
+
|
|
36
|
+
import pytest
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# =============================================================================
|
|
40
|
+
# Test Fixtures
|
|
41
|
+
# =============================================================================
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@pytest.fixture
|
|
45
|
+
def phase_commands_source() -> str:
|
|
46
|
+
"""Read the source code of phase_commands.py."""
|
|
47
|
+
source_path = Path(__file__).parent.parent / "commands" / "phase_commands.py"
|
|
48
|
+
return source_path.read_text()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@pytest.fixture
|
|
52
|
+
def get_phase_state_function_source(phase_commands_source: str) -> str:
|
|
53
|
+
"""Extract the _get_phase_state function source code."""
|
|
54
|
+
# Parse the AST to find the function
|
|
55
|
+
tree = ast.parse(phase_commands_source)
|
|
56
|
+
|
|
57
|
+
for node in ast.walk(tree):
|
|
58
|
+
if isinstance(node, ast.FunctionDef) and node.name == "_get_phase_state":
|
|
59
|
+
# Get the line range
|
|
60
|
+
start_line = node.lineno
|
|
61
|
+
end_line = node.end_lineno
|
|
62
|
+
|
|
63
|
+
# Extract lines
|
|
64
|
+
lines = phase_commands_source.split('\n')
|
|
65
|
+
function_lines = lines[start_line - 1:end_line]
|
|
66
|
+
return '\n'.join(function_lines)
|
|
67
|
+
|
|
68
|
+
raise ValueError("_get_phase_state function not found in phase_commands.py")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@pytest.fixture
|
|
72
|
+
def temp_project_dir():
|
|
73
|
+
"""Create a temporary project directory with required structure."""
|
|
74
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
75
|
+
project_root = Path(tmpdir)
|
|
76
|
+
# Create devforgeai/workflows directory
|
|
77
|
+
workflows_dir = project_root / "devforgeai" / "workflows"
|
|
78
|
+
workflows_dir.mkdir(parents=True, exist_ok=True)
|
|
79
|
+
yield project_root
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@pytest.fixture
|
|
83
|
+
def existing_state(temp_project_dir):
|
|
84
|
+
"""Create an existing phase state file for testing commands."""
|
|
85
|
+
state = {
|
|
86
|
+
"story_id": "STORY-001",
|
|
87
|
+
"workflow_started": "2026-01-12T10:00:00Z",
|
|
88
|
+
"current_phase": "02",
|
|
89
|
+
"blocking_status": False,
|
|
90
|
+
"phases": {
|
|
91
|
+
"01": {
|
|
92
|
+
"status": "completed",
|
|
93
|
+
"started_at": "2026-01-12T10:00:00Z",
|
|
94
|
+
"completed_at": "2026-01-12T10:05:00Z",
|
|
95
|
+
"subagents_required": ["git-validator", "tech-stack-detector"],
|
|
96
|
+
"subagents_invoked": ["git-validator", "tech-stack-detector"],
|
|
97
|
+
"checkpoint_passed": True
|
|
98
|
+
},
|
|
99
|
+
"02": {
|
|
100
|
+
"status": "pending",
|
|
101
|
+
"subagents_required": ["test-automator"],
|
|
102
|
+
"subagents_invoked": []
|
|
103
|
+
},
|
|
104
|
+
**{f"{i:02d}": {"status": "pending", "subagents_required": [], "subagents_invoked": []}
|
|
105
|
+
for i in range(3, 11)}
|
|
106
|
+
},
|
|
107
|
+
"validation_errors": [],
|
|
108
|
+
"observations": []
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
state_file = temp_project_dir / "devforgeai" / "workflows" / "STORY-001-phase-state.json"
|
|
112
|
+
state_file.write_text(json.dumps(state, indent=2))
|
|
113
|
+
return state_file
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# =============================================================================
|
|
117
|
+
# AC#1: Relative import replaces sys.path manipulation
|
|
118
|
+
# =============================================================================
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class TestAC1_RelativeImport:
|
|
122
|
+
"""
|
|
123
|
+
AC#1: Relative import replaces sys.path manipulation
|
|
124
|
+
|
|
125
|
+
Given: STORY-253 has been completed and PhaseState is available
|
|
126
|
+
When: The _get_phase_state() function in phase_commands.py is updated
|
|
127
|
+
Then: The function uses `from ..phase_state import PhaseState` instead of
|
|
128
|
+
sys.path.insert() and `from installer.phase_state import PhaseState`
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
def test_get_phase_state_uses_relative_import_statement(
|
|
132
|
+
self,
|
|
133
|
+
get_phase_state_function_source: str
|
|
134
|
+
):
|
|
135
|
+
"""
|
|
136
|
+
SVC-001 / BR-001: Import uses `from ..phase_state` relative path.
|
|
137
|
+
|
|
138
|
+
The function should contain:
|
|
139
|
+
from ..phase_state import PhaseState
|
|
140
|
+
|
|
141
|
+
NOT:
|
|
142
|
+
from installer.phase_state import PhaseState
|
|
143
|
+
"""
|
|
144
|
+
# Check for relative import pattern
|
|
145
|
+
relative_import_pattern = r'from\s+\.\.phase_state\s+import\s+PhaseState'
|
|
146
|
+
has_relative_import = bool(
|
|
147
|
+
re.search(relative_import_pattern, get_phase_state_function_source)
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
assert has_relative_import, (
|
|
151
|
+
"FAIL (TDD Red): _get_phase_state() should use relative import "
|
|
152
|
+
"'from ..phase_state import PhaseState' but does not.\n"
|
|
153
|
+
f"Current function:\n{get_phase_state_function_source}"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def test_get_phase_state_no_sys_path_insert(
|
|
157
|
+
self,
|
|
158
|
+
get_phase_state_function_source: str
|
|
159
|
+
):
|
|
160
|
+
"""
|
|
161
|
+
SVC-001: No sys.path.insert() calls in the function.
|
|
162
|
+
|
|
163
|
+
The function should NOT contain:
|
|
164
|
+
sys.path.insert(...)
|
|
165
|
+
"""
|
|
166
|
+
sys_path_pattern = r'sys\.path\.insert'
|
|
167
|
+
has_sys_path_insert = bool(
|
|
168
|
+
re.search(sys_path_pattern, get_phase_state_function_source)
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
assert not has_sys_path_insert, (
|
|
172
|
+
"FAIL (TDD Red): _get_phase_state() should NOT use sys.path.insert() "
|
|
173
|
+
"but it does.\n"
|
|
174
|
+
f"Current function:\n{get_phase_state_function_source}"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def test_get_phase_state_no_installer_import_path(
|
|
178
|
+
self,
|
|
179
|
+
get_phase_state_function_source: str
|
|
180
|
+
):
|
|
181
|
+
"""
|
|
182
|
+
SVC-001: No 'from installer.phase_state' import path.
|
|
183
|
+
|
|
184
|
+
The function should NOT contain:
|
|
185
|
+
from installer.phase_state import PhaseState
|
|
186
|
+
"""
|
|
187
|
+
installer_import_pattern = r'from\s+installer\.phase_state\s+import'
|
|
188
|
+
has_installer_import = bool(
|
|
189
|
+
re.search(installer_import_pattern, get_phase_state_function_source)
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
assert not has_installer_import, (
|
|
193
|
+
"FAIL (TDD Red): _get_phase_state() should NOT use "
|
|
194
|
+
"'from installer.phase_state import PhaseState' "
|
|
195
|
+
"but should use relative import instead.\n"
|
|
196
|
+
f"Current function:\n{get_phase_state_function_source}"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
def test_get_phase_state_no_installer_path_construction(
|
|
200
|
+
self,
|
|
201
|
+
get_phase_state_function_source: str
|
|
202
|
+
):
|
|
203
|
+
"""
|
|
204
|
+
SVC-001: No installer path construction (no Path(project_root) / "installer").
|
|
205
|
+
|
|
206
|
+
The function should NOT contain:
|
|
207
|
+
installer_path = Path(project_root) / "installer"
|
|
208
|
+
"""
|
|
209
|
+
path_construction_pattern = r'installer_path\s*=.*["\']installer["\']'
|
|
210
|
+
has_path_construction = bool(
|
|
211
|
+
re.search(path_construction_pattern, get_phase_state_function_source)
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
assert not has_path_construction, (
|
|
215
|
+
"FAIL (TDD Red): _get_phase_state() should NOT construct installer path "
|
|
216
|
+
"but it does.\n"
|
|
217
|
+
f"Current function:\n{get_phase_state_function_source}"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
# =============================================================================
|
|
222
|
+
# AC#2: Function behavior remains unchanged
|
|
223
|
+
# =============================================================================
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class TestAC2_FunctionBehaviorUnchanged:
|
|
227
|
+
"""
|
|
228
|
+
AC#2: Function behavior remains unchanged
|
|
229
|
+
|
|
230
|
+
Given: The refactored _get_phase_state() function
|
|
231
|
+
When: Called with a valid project_root parameter
|
|
232
|
+
Then: It returns a PhaseState instance with the same behavior as before
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
def test_get_phase_state_returns_phase_state_instance(self, temp_project_dir):
|
|
236
|
+
"""
|
|
237
|
+
SVC-002: Function returns PhaseState instance unchanged.
|
|
238
|
+
|
|
239
|
+
_get_phase_state(project_root) should return a PhaseState instance.
|
|
240
|
+
"""
|
|
241
|
+
# Import the module to test
|
|
242
|
+
from devforgeai_cli.commands.phase_commands import _get_phase_state
|
|
243
|
+
|
|
244
|
+
result = _get_phase_state(str(temp_project_dir))
|
|
245
|
+
|
|
246
|
+
# The result should be a PhaseState instance
|
|
247
|
+
# We check the class name since import paths may differ
|
|
248
|
+
assert result.__class__.__name__ == "PhaseState", (
|
|
249
|
+
f"FAIL (TDD Red): _get_phase_state() should return PhaseState instance, "
|
|
250
|
+
f"got {type(result).__name__}"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
def test_get_phase_state_sets_project_root(self, temp_project_dir):
|
|
254
|
+
"""
|
|
255
|
+
SVC-002: PhaseState instance has correct project_root.
|
|
256
|
+
"""
|
|
257
|
+
from devforgeai_cli.commands.phase_commands import _get_phase_state
|
|
258
|
+
|
|
259
|
+
result = _get_phase_state(str(temp_project_dir))
|
|
260
|
+
|
|
261
|
+
# PhaseState should have project_root attribute
|
|
262
|
+
assert hasattr(result, 'project_root'), (
|
|
263
|
+
"FAIL (TDD Red): PhaseState instance should have project_root attribute"
|
|
264
|
+
)
|
|
265
|
+
assert result.project_root == Path(temp_project_dir), (
|
|
266
|
+
f"FAIL (TDD Red): PhaseState.project_root should be {temp_project_dir}, "
|
|
267
|
+
f"got {result.project_root}"
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
def test_get_phase_state_can_create_workflow_state(self, temp_project_dir):
|
|
271
|
+
"""
|
|
272
|
+
SVC-002: Returned PhaseState can create workflow state files.
|
|
273
|
+
"""
|
|
274
|
+
from devforgeai_cli.commands.phase_commands import _get_phase_state
|
|
275
|
+
|
|
276
|
+
ps = _get_phase_state(str(temp_project_dir))
|
|
277
|
+
|
|
278
|
+
# Should be able to create a state
|
|
279
|
+
state = ps.create("STORY-099")
|
|
280
|
+
|
|
281
|
+
assert state is not None
|
|
282
|
+
assert state["story_id"] == "STORY-099"
|
|
283
|
+
assert state["current_phase"] == "01"
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
# =============================================================================
|
|
287
|
+
# AC#3: All phase commands still work correctly
|
|
288
|
+
# =============================================================================
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class TestAC3_AllPhaseCommandsWork:
|
|
292
|
+
"""
|
|
293
|
+
AC#3: All phase commands still work correctly
|
|
294
|
+
|
|
295
|
+
Given: The updated import in _get_phase_state()
|
|
296
|
+
When: All phase command functions are invoked
|
|
297
|
+
Then: All commands execute without import errors and maintain existing functionality
|
|
298
|
+
|
|
299
|
+
Commands tested:
|
|
300
|
+
- phase_init_command
|
|
301
|
+
- phase_check_command
|
|
302
|
+
- phase_complete_command
|
|
303
|
+
- phase_status_command
|
|
304
|
+
- phase_record_command
|
|
305
|
+
- phase_observe_command
|
|
306
|
+
"""
|
|
307
|
+
|
|
308
|
+
def test_phase_init_command_works_after_import_refactor(self, temp_project_dir):
|
|
309
|
+
"""
|
|
310
|
+
SVC-003: phase_init command works with relative import.
|
|
311
|
+
"""
|
|
312
|
+
from devforgeai_cli.commands.phase_commands import phase_init_command
|
|
313
|
+
|
|
314
|
+
# Should not raise ImportError
|
|
315
|
+
exit_code = phase_init_command(
|
|
316
|
+
story_id="STORY-100",
|
|
317
|
+
project_root=str(temp_project_dir),
|
|
318
|
+
format="text"
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
assert exit_code == 0, (
|
|
322
|
+
f"FAIL (TDD Red): phase_init_command should return 0, got {exit_code}"
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
# State file should be created
|
|
326
|
+
state_file = temp_project_dir / "devforgeai" / "workflows" / "STORY-100-phase-state.json"
|
|
327
|
+
assert state_file.exists(), "State file should be created"
|
|
328
|
+
|
|
329
|
+
def test_phase_check_command_works_after_import_refactor(
|
|
330
|
+
self,
|
|
331
|
+
temp_project_dir,
|
|
332
|
+
existing_state
|
|
333
|
+
):
|
|
334
|
+
"""
|
|
335
|
+
SVC-003: phase_check command works with relative import.
|
|
336
|
+
"""
|
|
337
|
+
from devforgeai_cli.commands.phase_commands import phase_check_command
|
|
338
|
+
|
|
339
|
+
# Should not raise ImportError
|
|
340
|
+
exit_code = phase_check_command(
|
|
341
|
+
story_id="STORY-001",
|
|
342
|
+
from_phase="01",
|
|
343
|
+
to_phase="02",
|
|
344
|
+
project_root=str(temp_project_dir),
|
|
345
|
+
format="text"
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
assert exit_code == 0, (
|
|
349
|
+
f"FAIL (TDD Red): phase_check_command should return 0, got {exit_code}"
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
def test_phase_complete_command_works_after_import_refactor(
|
|
353
|
+
self,
|
|
354
|
+
temp_project_dir,
|
|
355
|
+
existing_state
|
|
356
|
+
):
|
|
357
|
+
"""
|
|
358
|
+
SVC-003: phase_complete command works with relative import.
|
|
359
|
+
"""
|
|
360
|
+
from devforgeai_cli.commands.phase_commands import phase_complete_command
|
|
361
|
+
|
|
362
|
+
# Should not raise ImportError
|
|
363
|
+
exit_code = phase_complete_command(
|
|
364
|
+
story_id="STORY-001",
|
|
365
|
+
phase="02",
|
|
366
|
+
checkpoint_passed=True,
|
|
367
|
+
project_root=str(temp_project_dir),
|
|
368
|
+
format="text"
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
assert exit_code == 0, (
|
|
372
|
+
f"FAIL (TDD Red): phase_complete_command should return 0, got {exit_code}"
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
def test_phase_status_command_works_after_import_refactor(
|
|
376
|
+
self,
|
|
377
|
+
temp_project_dir,
|
|
378
|
+
existing_state
|
|
379
|
+
):
|
|
380
|
+
"""
|
|
381
|
+
SVC-003: phase_status command works with relative import.
|
|
382
|
+
"""
|
|
383
|
+
from devforgeai_cli.commands.phase_commands import phase_status_command
|
|
384
|
+
|
|
385
|
+
# Should not raise ImportError
|
|
386
|
+
exit_code = phase_status_command(
|
|
387
|
+
story_id="STORY-001",
|
|
388
|
+
project_root=str(temp_project_dir),
|
|
389
|
+
format="text"
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
assert exit_code == 0, (
|
|
393
|
+
f"FAIL (TDD Red): phase_status_command should return 0, got {exit_code}"
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
def test_phase_record_command_works_after_import_refactor(
|
|
397
|
+
self,
|
|
398
|
+
temp_project_dir,
|
|
399
|
+
existing_state
|
|
400
|
+
):
|
|
401
|
+
"""
|
|
402
|
+
SVC-003: phase_record command works with relative import.
|
|
403
|
+
"""
|
|
404
|
+
from devforgeai_cli.commands.phase_commands import phase_record_command
|
|
405
|
+
|
|
406
|
+
# Should not raise ImportError
|
|
407
|
+
exit_code = phase_record_command(
|
|
408
|
+
story_id="STORY-001",
|
|
409
|
+
phase="02",
|
|
410
|
+
subagent="test-automator",
|
|
411
|
+
project_root=str(temp_project_dir),
|
|
412
|
+
format="text"
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
assert exit_code == 0, (
|
|
416
|
+
f"FAIL (TDD Red): phase_record_command should return 0, got {exit_code}"
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
def test_phase_observe_command_works_after_import_refactor(
|
|
420
|
+
self,
|
|
421
|
+
temp_project_dir,
|
|
422
|
+
existing_state
|
|
423
|
+
):
|
|
424
|
+
"""
|
|
425
|
+
SVC-003: phase_observe command works with relative import.
|
|
426
|
+
"""
|
|
427
|
+
from devforgeai_cli.commands.phase_commands import phase_observe_command
|
|
428
|
+
|
|
429
|
+
# Should not raise ImportError
|
|
430
|
+
exit_code = phase_observe_command(
|
|
431
|
+
story_id="STORY-001",
|
|
432
|
+
phase="02",
|
|
433
|
+
category="success",
|
|
434
|
+
note="Test observation",
|
|
435
|
+
severity="medium",
|
|
436
|
+
project_root=str(temp_project_dir),
|
|
437
|
+
format="text"
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
assert exit_code == 0, (
|
|
441
|
+
f"FAIL (TDD Red): phase_observe_command should return 0, got {exit_code}"
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
# =============================================================================
|
|
446
|
+
# Technical Specification: Non-Functional Requirements
|
|
447
|
+
# =============================================================================
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
class TestNFR_NonFunctionalRequirements:
|
|
451
|
+
"""
|
|
452
|
+
Non-Functional Requirements from Technical Specification.
|
|
453
|
+
"""
|
|
454
|
+
|
|
455
|
+
def test_get_phase_state_function_line_count_reduced(
|
|
456
|
+
self,
|
|
457
|
+
get_phase_state_function_source: str
|
|
458
|
+
):
|
|
459
|
+
"""
|
|
460
|
+
NFR-002: Function reduced from original sys.path approach.
|
|
461
|
+
|
|
462
|
+
STORY-254: Reduced from 8 lines (sys.path) to 3 lines (relative import)
|
|
463
|
+
STORY-255: Added graceful error handling (~20 additional lines for
|
|
464
|
+
helpful diagnostic message when PhaseState module missing)
|
|
465
|
+
|
|
466
|
+
The refactored function should have:
|
|
467
|
+
- Relative import (from ..phase_state) - STORY-254
|
|
468
|
+
- try/except error handling with helpful message - STORY-255
|
|
469
|
+
- Comprehensive docstring documenting the ImportError behavior
|
|
470
|
+
"""
|
|
471
|
+
# Count non-empty, non-comment lines
|
|
472
|
+
lines = get_phase_state_function_source.split('\n')
|
|
473
|
+
code_lines = [
|
|
474
|
+
line for line in lines
|
|
475
|
+
if line.strip() and not line.strip().startswith('#')
|
|
476
|
+
and not line.strip().startswith('"""')
|
|
477
|
+
]
|
|
478
|
+
|
|
479
|
+
# STORY-255 adds error handling which increases line count legitimately:
|
|
480
|
+
# - ~18 lines for docstring
|
|
481
|
+
# - ~4 lines for try block (def, try, import, return)
|
|
482
|
+
# - ~15 lines for except block (error message)
|
|
483
|
+
# Total: ~35-40 lines maximum
|
|
484
|
+
max_expected_lines = 40 # With error handling from STORY-255
|
|
485
|
+
min_expected_lines = 15 # Minimum for proper error handling
|
|
486
|
+
|
|
487
|
+
actual_line_count = len(code_lines)
|
|
488
|
+
|
|
489
|
+
# Verify function has error handling (STORY-255) but isn't bloated
|
|
490
|
+
assert actual_line_count <= max_expected_lines, (
|
|
491
|
+
f"_get_phase_state() should have ≤{max_expected_lines} lines "
|
|
492
|
+
f"(with STORY-255 error handling), but has {actual_line_count}.\n"
|
|
493
|
+
f"Current function:\n{get_phase_state_function_source}"
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
# Verify function has minimum content for error handling
|
|
497
|
+
assert actual_line_count >= min_expected_lines, (
|
|
498
|
+
f"_get_phase_state() should have ≥{min_expected_lines} lines "
|
|
499
|
+
f"(must include STORY-255 error handling), but has {actual_line_count}.\n"
|
|
500
|
+
f"Verify try/except block and helpful error message are present."
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
def test_get_phase_state_import_latency(self, temp_project_dir):
|
|
504
|
+
"""
|
|
505
|
+
NFR-001: Import latency < 5ms per import.
|
|
506
|
+
|
|
507
|
+
The function should execute quickly without sys.path overhead.
|
|
508
|
+
"""
|
|
509
|
+
from devforgeai_cli.commands.phase_commands import _get_phase_state
|
|
510
|
+
|
|
511
|
+
# Warm up (first import may be slower)
|
|
512
|
+
_get_phase_state(str(temp_project_dir))
|
|
513
|
+
|
|
514
|
+
# Measure subsequent calls
|
|
515
|
+
iterations = 10
|
|
516
|
+
start_time = time.perf_counter()
|
|
517
|
+
|
|
518
|
+
for _ in range(iterations):
|
|
519
|
+
_get_phase_state(str(temp_project_dir))
|
|
520
|
+
|
|
521
|
+
elapsed_ms = ((time.perf_counter() - start_time) / iterations) * 1000
|
|
522
|
+
|
|
523
|
+
# Allow 5ms per call
|
|
524
|
+
assert elapsed_ms < 5.0, (
|
|
525
|
+
f"FAIL: _get_phase_state() took {elapsed_ms:.2f}ms per call, "
|
|
526
|
+
f"expected < 5ms"
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
# =============================================================================
|
|
531
|
+
# Technical Specification: SVC-004 Sys Import Removal
|
|
532
|
+
# =============================================================================
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
class TestSVC004_SysImportRemoval:
|
|
536
|
+
"""
|
|
537
|
+
SVC-004: Remove unused sys import if applicable (Low priority).
|
|
538
|
+
|
|
539
|
+
If sys is only used for sys.path.insert() in _get_phase_state(),
|
|
540
|
+
and that usage is removed, check if sys import can be removed.
|
|
541
|
+
"""
|
|
542
|
+
|
|
543
|
+
def test_sys_import_usage_check(self, phase_commands_source: str):
|
|
544
|
+
"""
|
|
545
|
+
SVC-004: Check if sys is still needed after refactoring.
|
|
546
|
+
|
|
547
|
+
After removing sys.path.insert(), verify sys usage:
|
|
548
|
+
- If sys is NOT used anywhere else, import should be removed
|
|
549
|
+
- If sys IS used elsewhere, import should remain
|
|
550
|
+
"""
|
|
551
|
+
# Count sys usages (excluding the import statement itself)
|
|
552
|
+
# Pattern matches sys.something usage
|
|
553
|
+
sys_usage_pattern = r'(?<!import\s)sys\.\w+'
|
|
554
|
+
sys_usages = re.findall(sys_usage_pattern, phase_commands_source)
|
|
555
|
+
|
|
556
|
+
# Import statement pattern
|
|
557
|
+
sys_import_pattern = r'^import\s+sys\s*$|^from\s+sys\s+import'
|
|
558
|
+
has_sys_import = bool(
|
|
559
|
+
re.search(sys_import_pattern, phase_commands_source, re.MULTILINE)
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
# If there are no sys usages (after refactoring), import should be removed
|
|
563
|
+
# For TDD Red, we expect sys.path.insert to still exist
|
|
564
|
+
if len(sys_usages) == 0 and has_sys_import:
|
|
565
|
+
pytest.fail(
|
|
566
|
+
"FAIL (TDD Red): sys is imported but not used. "
|
|
567
|
+
"Remove 'import sys' if sys.path.insert() has been removed."
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
# Note: This test passes if either:
|
|
571
|
+
# 1. sys IS used (current state before refactoring)
|
|
572
|
+
# 2. sys import is removed (after refactoring if not needed)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
# =============================================================================
|
|
576
|
+
# Integration Test: Import Chain Verification
|
|
577
|
+
# =============================================================================
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
class TestImportChainVerification:
|
|
581
|
+
"""
|
|
582
|
+
Verify the import chain works correctly after refactoring.
|
|
583
|
+
"""
|
|
584
|
+
|
|
585
|
+
def test_phase_state_module_importable_from_parent_package(self):
|
|
586
|
+
"""
|
|
587
|
+
Verify PhaseState can be imported via relative path from commands module.
|
|
588
|
+
|
|
589
|
+
This tests that the package structure supports:
|
|
590
|
+
from ..phase_state import PhaseState
|
|
591
|
+
|
|
592
|
+
When executed from .claude/scripts/devforgeai_cli/commands/phase_commands.py
|
|
593
|
+
"""
|
|
594
|
+
# Test file path: .claude/scripts/devforgeai_cli/tests/test_phase_commands_import.py
|
|
595
|
+
# devforgeai_cli path: .claude/scripts/devforgeai_cli/
|
|
596
|
+
# phase_state.py should be at: .claude/scripts/devforgeai_cli/phase_state.py
|
|
597
|
+
|
|
598
|
+
# The package structure should be:
|
|
599
|
+
# devforgeai_cli/
|
|
600
|
+
# __init__.py
|
|
601
|
+
# phase_state.py <- PhaseState lives here (STORY-253 creates this)
|
|
602
|
+
# commands/
|
|
603
|
+
# __init__.py
|
|
604
|
+
# phase_commands.py <- imports from ..phase_state
|
|
605
|
+
# tests/
|
|
606
|
+
# test_phase_commands_import.py <- This file
|
|
607
|
+
|
|
608
|
+
# Check that phase_state.py exists at the correct location
|
|
609
|
+
# Path(__file__).parent = tests/
|
|
610
|
+
# Path(__file__).parent.parent = devforgeai_cli/
|
|
611
|
+
phase_state_path = Path(__file__).parent.parent / "phase_state.py"
|
|
612
|
+
|
|
613
|
+
assert phase_state_path.exists(), (
|
|
614
|
+
f"FAIL (TDD Red): PhaseState module not found at expected location.\n"
|
|
615
|
+
f"Expected: {phase_state_path}\n"
|
|
616
|
+
"The package structure must support: from ..phase_state import PhaseState\n"
|
|
617
|
+
"STORY-253 must be completed first to create phase_state.py in devforgeai_cli/"
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
def test_package_init_files_exist(self):
|
|
621
|
+
"""
|
|
622
|
+
Verify __init__.py files exist for proper package structure.
|
|
623
|
+
"""
|
|
624
|
+
# Path(__file__).parent = tests/
|
|
625
|
+
# Path(__file__).parent.parent = devforgeai_cli/
|
|
626
|
+
devforgeai_cli_path = Path(__file__).parent.parent
|
|
627
|
+
|
|
628
|
+
required_init_files = [
|
|
629
|
+
devforgeai_cli_path / "__init__.py", # devforgeai_cli/__init__.py
|
|
630
|
+
devforgeai_cli_path / "commands" / "__init__.py", # devforgeai_cli/commands/__init__.py
|
|
631
|
+
]
|
|
632
|
+
|
|
633
|
+
missing = []
|
|
634
|
+
for init_file in required_init_files:
|
|
635
|
+
if not init_file.exists():
|
|
636
|
+
missing.append(str(init_file))
|
|
637
|
+
|
|
638
|
+
assert not missing, (
|
|
639
|
+
f"FAIL: Missing __init__.py files for package structure:\n"
|
|
640
|
+
+ "\n".join(missing)
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
# =============================================================================
|
|
645
|
+
# Error Handling Tests
|
|
646
|
+
# =============================================================================
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
class TestImportErrorHandling:
|
|
650
|
+
"""
|
|
651
|
+
Test error handling when import fails.
|
|
652
|
+
"""
|
|
653
|
+
|
|
654
|
+
def test_import_error_is_clear_when_phase_state_missing(self):
|
|
655
|
+
"""
|
|
656
|
+
When PhaseState module is not found, error should be clear.
|
|
657
|
+
|
|
658
|
+
This test documents expected behavior - if the relative import
|
|
659
|
+
fails, Python should raise ImportError with a clear message.
|
|
660
|
+
"""
|
|
661
|
+
# This test validates that if phase_state.py doesn't exist,
|
|
662
|
+
# the import will fail with ImportError, not some obscure error
|
|
663
|
+
# from sys.path manipulation
|
|
664
|
+
|
|
665
|
+
# We can't easily test this without mocking, but we document
|
|
666
|
+
# the expected behavior: ImportError raised immediately (fail-fast)
|
|
667
|
+
pass # Documentation-only test
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
# =============================================================================
|
|
671
|
+
# Code Style Tests
|
|
672
|
+
# =============================================================================
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
class TestCodeStyle:
|
|
676
|
+
"""
|
|
677
|
+
Verify code follows Python best practices (PEP 328 for imports).
|
|
678
|
+
"""
|
|
679
|
+
|
|
680
|
+
def test_no_magic_path_strings(self, get_phase_state_function_source: str):
|
|
681
|
+
"""
|
|
682
|
+
Function should not contain hardcoded path strings like "installer".
|
|
683
|
+
"""
|
|
684
|
+
# Magic strings that indicate old implementation
|
|
685
|
+
magic_strings = ['installer', 'phase_state.py']
|
|
686
|
+
|
|
687
|
+
found = []
|
|
688
|
+
for magic in magic_strings:
|
|
689
|
+
if f'"{magic}"' in get_phase_state_function_source:
|
|
690
|
+
found.append(magic)
|
|
691
|
+
if f"'{magic}'" in get_phase_state_function_source:
|
|
692
|
+
found.append(magic)
|
|
693
|
+
|
|
694
|
+
assert not found, (
|
|
695
|
+
f"FAIL (TDD Red): _get_phase_state() contains magic path strings: {found}\n"
|
|
696
|
+
"Use relative imports instead of path string manipulation."
|
|
697
|
+
)
|