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,361 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for validate-installation command (STORY-314).
|
|
3
|
+
|
|
4
|
+
Validates post-install checks for DevForgeAI projects.
|
|
5
|
+
|
|
6
|
+
6 Checks:
|
|
7
|
+
1. CLI availability (devforgeai-validate --version)
|
|
8
|
+
2. Context files (6 files in devforgeai/specs/context/)
|
|
9
|
+
3. Hook installation (.git/hooks/pre-commit exists)
|
|
10
|
+
4. PYTHONPATH configuration (CLI imports succeed)
|
|
11
|
+
5. Git repository (.git/ exists)
|
|
12
|
+
6. Settings file (.claude/settings.json exists)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from unittest.mock import patch, MagicMock
|
|
18
|
+
import subprocess
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# =============================================================================
|
|
22
|
+
# AC#1: All 6 checks pass on valid installation
|
|
23
|
+
# =============================================================================
|
|
24
|
+
|
|
25
|
+
class TestValidInstallation:
|
|
26
|
+
"""Tests for AC#1: All 6 checks pass on valid installation."""
|
|
27
|
+
|
|
28
|
+
def test_validate_installation_all_checks_pass_returns_zero(self, tmp_path):
|
|
29
|
+
"""Test: Valid installation returns exit code 0."""
|
|
30
|
+
# Arrange: Create valid installation structure
|
|
31
|
+
_create_valid_installation(tmp_path)
|
|
32
|
+
|
|
33
|
+
# Act
|
|
34
|
+
from devforgeai_cli.commands.validate_installation import validate_installation_command
|
|
35
|
+
exit_code = validate_installation_command(project_root=str(tmp_path))
|
|
36
|
+
|
|
37
|
+
# Assert
|
|
38
|
+
assert exit_code == 0
|
|
39
|
+
|
|
40
|
+
def test_validate_installation_reports_6_of_6_passed(self, tmp_path, capsys):
|
|
41
|
+
"""Test: Valid installation reports '6/6 checks passed'."""
|
|
42
|
+
# Arrange
|
|
43
|
+
_create_valid_installation(tmp_path)
|
|
44
|
+
|
|
45
|
+
# Act
|
|
46
|
+
from devforgeai_cli.commands.validate_installation import validate_installation_command
|
|
47
|
+
validate_installation_command(project_root=str(tmp_path))
|
|
48
|
+
captured = capsys.readouterr()
|
|
49
|
+
|
|
50
|
+
# Assert
|
|
51
|
+
assert "6/6" in captured.out or "PASS" in captured.out
|
|
52
|
+
|
|
53
|
+
def test_validate_installation_check_cli_available_success(self, tmp_path):
|
|
54
|
+
"""Test: CLI availability check passes when devforgeai-validate exists."""
|
|
55
|
+
# Arrange
|
|
56
|
+
_create_valid_installation(tmp_path)
|
|
57
|
+
|
|
58
|
+
# Act
|
|
59
|
+
from devforgeai_cli.commands.validate_installation import check_cli_available
|
|
60
|
+
result = check_cli_available()
|
|
61
|
+
|
|
62
|
+
# Assert
|
|
63
|
+
assert result["passed"] is True
|
|
64
|
+
assert "devforgeai-validate" in result["message"]
|
|
65
|
+
|
|
66
|
+
def test_validate_installation_check_context_files_success(self, tmp_path):
|
|
67
|
+
"""Test: Context files check passes when all 6 files exist."""
|
|
68
|
+
# Arrange
|
|
69
|
+
_create_valid_installation(tmp_path)
|
|
70
|
+
|
|
71
|
+
# Act
|
|
72
|
+
from devforgeai_cli.commands.validate_installation import check_context_files
|
|
73
|
+
result = check_context_files(project_root=str(tmp_path))
|
|
74
|
+
|
|
75
|
+
# Assert
|
|
76
|
+
assert result["passed"] is True
|
|
77
|
+
assert "6/6" in result["message"]
|
|
78
|
+
|
|
79
|
+
def test_validate_installation_check_hooks_success(self, tmp_path):
|
|
80
|
+
"""Test: Hook check passes when .git/hooks/pre-commit exists."""
|
|
81
|
+
# Arrange
|
|
82
|
+
_create_valid_installation(tmp_path)
|
|
83
|
+
|
|
84
|
+
# Act
|
|
85
|
+
from devforgeai_cli.commands.validate_installation import check_hooks_installed
|
|
86
|
+
result = check_hooks_installed(project_root=str(tmp_path))
|
|
87
|
+
|
|
88
|
+
# Assert
|
|
89
|
+
assert result["passed"] is True
|
|
90
|
+
|
|
91
|
+
def test_validate_installation_check_pythonpath_success(self):
|
|
92
|
+
"""Test: PYTHONPATH check passes when CLI imports succeed."""
|
|
93
|
+
# Act
|
|
94
|
+
from devforgeai_cli.commands.validate_installation import check_pythonpath
|
|
95
|
+
result = check_pythonpath()
|
|
96
|
+
|
|
97
|
+
# Assert
|
|
98
|
+
assert result["passed"] is True
|
|
99
|
+
|
|
100
|
+
def test_validate_installation_check_git_repo_success(self, tmp_path):
|
|
101
|
+
"""Test: Git check passes when .git/ directory exists."""
|
|
102
|
+
# Arrange
|
|
103
|
+
_create_valid_installation(tmp_path)
|
|
104
|
+
|
|
105
|
+
# Act
|
|
106
|
+
from devforgeai_cli.commands.validate_installation import check_git_repository
|
|
107
|
+
result = check_git_repository(project_root=str(tmp_path))
|
|
108
|
+
|
|
109
|
+
# Assert
|
|
110
|
+
assert result["passed"] is True
|
|
111
|
+
|
|
112
|
+
def test_validate_installation_check_settings_success(self, tmp_path):
|
|
113
|
+
"""Test: Settings check passes when .claude/settings.json exists."""
|
|
114
|
+
# Arrange
|
|
115
|
+
_create_valid_installation(tmp_path)
|
|
116
|
+
|
|
117
|
+
# Act
|
|
118
|
+
from devforgeai_cli.commands.validate_installation import check_settings_file
|
|
119
|
+
result = check_settings_file(project_root=str(tmp_path))
|
|
120
|
+
|
|
121
|
+
# Assert
|
|
122
|
+
assert result["passed"] is True
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# =============================================================================
|
|
126
|
+
# AC#2: Clear error for incomplete installation
|
|
127
|
+
# =============================================================================
|
|
128
|
+
|
|
129
|
+
class TestIncompleteInstallation:
|
|
130
|
+
"""Tests for AC#2: Clear error for incomplete installation."""
|
|
131
|
+
|
|
132
|
+
def test_validate_installation_missing_context_files_returns_nonzero(self, tmp_path):
|
|
133
|
+
"""Test: Missing context files returns non-zero exit code."""
|
|
134
|
+
# Arrange: Create installation missing context files
|
|
135
|
+
_create_partial_installation(tmp_path, skip_context=True)
|
|
136
|
+
|
|
137
|
+
# Act
|
|
138
|
+
from devforgeai_cli.commands.validate_installation import validate_installation_command
|
|
139
|
+
exit_code = validate_installation_command(project_root=str(tmp_path))
|
|
140
|
+
|
|
141
|
+
# Assert
|
|
142
|
+
assert exit_code != 0
|
|
143
|
+
|
|
144
|
+
def test_validate_installation_missing_hooks_returns_nonzero(self, tmp_path):
|
|
145
|
+
"""Test: Missing hooks returns non-zero exit code."""
|
|
146
|
+
# Arrange
|
|
147
|
+
_create_partial_installation(tmp_path, skip_hooks=True)
|
|
148
|
+
|
|
149
|
+
# Act
|
|
150
|
+
from devforgeai_cli.commands.validate_installation import validate_installation_command
|
|
151
|
+
exit_code = validate_installation_command(project_root=str(tmp_path))
|
|
152
|
+
|
|
153
|
+
# Assert
|
|
154
|
+
assert exit_code != 0
|
|
155
|
+
|
|
156
|
+
def test_validate_installation_missing_git_returns_nonzero(self, tmp_path):
|
|
157
|
+
"""Test: Missing .git/ returns non-zero exit code."""
|
|
158
|
+
# Arrange
|
|
159
|
+
_create_partial_installation(tmp_path, skip_git=True)
|
|
160
|
+
|
|
161
|
+
# Act
|
|
162
|
+
from devforgeai_cli.commands.validate_installation import validate_installation_command
|
|
163
|
+
exit_code = validate_installation_command(project_root=str(tmp_path))
|
|
164
|
+
|
|
165
|
+
# Assert
|
|
166
|
+
assert exit_code != 0
|
|
167
|
+
|
|
168
|
+
def test_validate_installation_error_includes_to_fix_instruction(self, tmp_path, capsys):
|
|
169
|
+
"""Test: Error output includes 'To fix:' instructions."""
|
|
170
|
+
# Arrange
|
|
171
|
+
_create_partial_installation(tmp_path, skip_context=True)
|
|
172
|
+
|
|
173
|
+
# Act
|
|
174
|
+
from devforgeai_cli.commands.validate_installation import validate_installation_command
|
|
175
|
+
validate_installation_command(project_root=str(tmp_path))
|
|
176
|
+
captured = capsys.readouterr()
|
|
177
|
+
|
|
178
|
+
# Assert
|
|
179
|
+
assert "To fix:" in captured.out or "to fix" in captured.out.lower()
|
|
180
|
+
|
|
181
|
+
def test_validate_installation_error_lists_missing_files(self, tmp_path, capsys):
|
|
182
|
+
"""Test: Error output lists which files are missing."""
|
|
183
|
+
# Arrange
|
|
184
|
+
_create_partial_installation(tmp_path, skip_context=True)
|
|
185
|
+
|
|
186
|
+
# Act
|
|
187
|
+
from devforgeai_cli.commands.validate_installation import validate_installation_command
|
|
188
|
+
validate_installation_command(project_root=str(tmp_path))
|
|
189
|
+
captured = capsys.readouterr()
|
|
190
|
+
|
|
191
|
+
# Assert: Should mention at least one missing context file
|
|
192
|
+
assert any(f in captured.out for f in [
|
|
193
|
+
"tech-stack.md", "source-tree.md", "dependencies.md",
|
|
194
|
+
"coding-standards.md", "architecture-constraints.md", "anti-patterns.md"
|
|
195
|
+
]) or "Missing" in captured.out
|
|
196
|
+
|
|
197
|
+
def test_validate_installation_check_context_files_lists_missing(self, tmp_path):
|
|
198
|
+
"""Test: Context check result includes list of missing files."""
|
|
199
|
+
# Arrange
|
|
200
|
+
context_dir = tmp_path / "devforgeai" / "specs" / "context"
|
|
201
|
+
context_dir.mkdir(parents=True)
|
|
202
|
+
(context_dir / "tech-stack.md").write_text("# Tech Stack")
|
|
203
|
+
# Only 1 of 6 files created
|
|
204
|
+
|
|
205
|
+
# Act
|
|
206
|
+
from devforgeai_cli.commands.validate_installation import check_context_files
|
|
207
|
+
result = check_context_files(project_root=str(tmp_path))
|
|
208
|
+
|
|
209
|
+
# Assert
|
|
210
|
+
assert result["passed"] is False
|
|
211
|
+
assert "missing" in result.get("details", "").lower() or len(result.get("missing", [])) > 0
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# =============================================================================
|
|
215
|
+
# Business Rules: BR-001, BR-002, BR-003
|
|
216
|
+
# =============================================================================
|
|
217
|
+
|
|
218
|
+
class TestBusinessRules:
|
|
219
|
+
"""Tests for business rules from technical specification."""
|
|
220
|
+
|
|
221
|
+
def test_br001_each_check_returns_pass_fail_with_reason(self, tmp_path):
|
|
222
|
+
"""BR-001: Each check must return pass/fail with clear reason."""
|
|
223
|
+
# Arrange
|
|
224
|
+
_create_valid_installation(tmp_path)
|
|
225
|
+
|
|
226
|
+
# Act
|
|
227
|
+
from devforgeai_cli.commands.validate_installation import check_git_repository
|
|
228
|
+
result = check_git_repository(project_root=str(tmp_path))
|
|
229
|
+
|
|
230
|
+
# Assert
|
|
231
|
+
assert "passed" in result
|
|
232
|
+
assert "message" in result
|
|
233
|
+
assert isinstance(result["passed"], bool)
|
|
234
|
+
assert isinstance(result["message"], str)
|
|
235
|
+
|
|
236
|
+
def test_br002_failed_check_suggests_fix(self, tmp_path):
|
|
237
|
+
"""BR-002: Actionable fix must be suggested for each failure."""
|
|
238
|
+
# Arrange: No .git directory
|
|
239
|
+
tmp_path.mkdir(exist_ok=True)
|
|
240
|
+
|
|
241
|
+
# Act
|
|
242
|
+
from devforgeai_cli.commands.validate_installation import check_git_repository
|
|
243
|
+
result = check_git_repository(project_root=str(tmp_path))
|
|
244
|
+
|
|
245
|
+
# Assert
|
|
246
|
+
assert result["passed"] is False
|
|
247
|
+
assert "fix" in result.get("fix", "").lower() or "To fix" in result.get("message", "")
|
|
248
|
+
|
|
249
|
+
def test_br003_exit_code_zero_only_when_all_pass(self, tmp_path):
|
|
250
|
+
"""BR-003: Exit code 0 only if all checks pass."""
|
|
251
|
+
# Arrange: Valid installation
|
|
252
|
+
_create_valid_installation(tmp_path)
|
|
253
|
+
|
|
254
|
+
# Act
|
|
255
|
+
from devforgeai_cli.commands.validate_installation import validate_installation_command
|
|
256
|
+
exit_code = validate_installation_command(project_root=str(tmp_path))
|
|
257
|
+
|
|
258
|
+
# Assert
|
|
259
|
+
assert exit_code == 0
|
|
260
|
+
|
|
261
|
+
def test_br003_exit_code_nonzero_when_any_fails(self, tmp_path):
|
|
262
|
+
"""BR-003: Any failure results in non-zero exit code."""
|
|
263
|
+
# Arrange: Missing one component
|
|
264
|
+
_create_partial_installation(tmp_path, skip_settings=True)
|
|
265
|
+
|
|
266
|
+
# Act
|
|
267
|
+
from devforgeai_cli.commands.validate_installation import validate_installation_command
|
|
268
|
+
exit_code = validate_installation_command(project_root=str(tmp_path))
|
|
269
|
+
|
|
270
|
+
# Assert
|
|
271
|
+
assert exit_code != 0
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
# =============================================================================
|
|
275
|
+
# CLI Integration Tests
|
|
276
|
+
# =============================================================================
|
|
277
|
+
|
|
278
|
+
class TestCLIIntegration:
|
|
279
|
+
"""Tests for CLI entry point integration."""
|
|
280
|
+
|
|
281
|
+
def test_cli_help_includes_validate_installation(self, capsys):
|
|
282
|
+
"""Test: CLI --help lists validate-installation command."""
|
|
283
|
+
# This tests that cli.py was updated with the new subcommand
|
|
284
|
+
import sys
|
|
285
|
+
from io import StringIO
|
|
286
|
+
|
|
287
|
+
with patch.object(sys, 'argv', ['devforgeai-validate', '--help']):
|
|
288
|
+
with pytest.raises(SystemExit) as exc_info:
|
|
289
|
+
from devforgeai_cli.cli import main
|
|
290
|
+
main()
|
|
291
|
+
|
|
292
|
+
# Should exit 0 (help displayed successfully)
|
|
293
|
+
assert exc_info.value.code == 0
|
|
294
|
+
|
|
295
|
+
captured = capsys.readouterr()
|
|
296
|
+
assert "validate-installation" in captured.out
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
# =============================================================================
|
|
300
|
+
# Helper Functions
|
|
301
|
+
# =============================================================================
|
|
302
|
+
|
|
303
|
+
def _create_valid_installation(path: Path):
|
|
304
|
+
"""Create a complete valid DevForgeAI installation structure."""
|
|
305
|
+
# Git directory
|
|
306
|
+
git_dir = path / ".git"
|
|
307
|
+
git_dir.mkdir(parents=True)
|
|
308
|
+
|
|
309
|
+
# Git hooks
|
|
310
|
+
hooks_dir = git_dir / "hooks"
|
|
311
|
+
hooks_dir.mkdir()
|
|
312
|
+
(hooks_dir / "pre-commit").write_text("#!/bin/bash\nexit 0")
|
|
313
|
+
|
|
314
|
+
# Context files (6 files)
|
|
315
|
+
context_dir = path / "devforgeai" / "specs" / "context"
|
|
316
|
+
context_dir.mkdir(parents=True)
|
|
317
|
+
for filename in [
|
|
318
|
+
"tech-stack.md", "source-tree.md", "dependencies.md",
|
|
319
|
+
"coding-standards.md", "architecture-constraints.md", "anti-patterns.md"
|
|
320
|
+
]:
|
|
321
|
+
(context_dir / filename).write_text(f"# {filename}")
|
|
322
|
+
|
|
323
|
+
# Settings file
|
|
324
|
+
claude_dir = path / ".claude"
|
|
325
|
+
claude_dir.mkdir(exist_ok=True)
|
|
326
|
+
(claude_dir / "settings.json").write_text("{}")
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _create_partial_installation(
|
|
330
|
+
path: Path,
|
|
331
|
+
skip_context: bool = False,
|
|
332
|
+
skip_hooks: bool = False,
|
|
333
|
+
skip_git: bool = False,
|
|
334
|
+
skip_settings: bool = False
|
|
335
|
+
):
|
|
336
|
+
"""Create an incomplete installation for error testing."""
|
|
337
|
+
# Git directory (unless skipped)
|
|
338
|
+
if not skip_git:
|
|
339
|
+
git_dir = path / ".git"
|
|
340
|
+
git_dir.mkdir(parents=True)
|
|
341
|
+
|
|
342
|
+
if not skip_hooks:
|
|
343
|
+
hooks_dir = git_dir / "hooks"
|
|
344
|
+
hooks_dir.mkdir()
|
|
345
|
+
(hooks_dir / "pre-commit").write_text("#!/bin/bash\nexit 0")
|
|
346
|
+
|
|
347
|
+
# Context files (unless skipped)
|
|
348
|
+
if not skip_context:
|
|
349
|
+
context_dir = path / "devforgeai" / "specs" / "context"
|
|
350
|
+
context_dir.mkdir(parents=True)
|
|
351
|
+
for filename in [
|
|
352
|
+
"tech-stack.md", "source-tree.md", "dependencies.md",
|
|
353
|
+
"coding-standards.md", "architecture-constraints.md", "anti-patterns.md"
|
|
354
|
+
]:
|
|
355
|
+
(context_dir / filename).write_text(f"# {filename}")
|
|
356
|
+
|
|
357
|
+
# Settings file (unless skipped)
|
|
358
|
+
if not skip_settings:
|
|
359
|
+
claude_dir = path / ".claude"
|
|
360
|
+
claude_dir.mkdir(exist_ok=True)
|
|
361
|
+
(claude_dir / "settings.json").write_text("{}")
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Utility modules for DevForgeAI CLI."""
|
|
2
|
+
|
|
3
|
+
from .markdown_parser import *
|
|
4
|
+
from .yaml_parser import *
|
|
5
|
+
from .story_analyzer import *
|
|
6
|
+
from .depends_on_normalizer import (
|
|
7
|
+
normalize_depends_on,
|
|
8
|
+
is_valid_story_id,
|
|
9
|
+
validate_depends_on_input,
|
|
10
|
+
STORY_ID_PATTERN
|
|
11
|
+
)
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
r"""
|
|
3
|
+
Depends On Field Normalizer
|
|
4
|
+
|
|
5
|
+
Normalizes various input formats for the depends_on field to array format.
|
|
6
|
+
Supports STORY-090 v2.2 template format.
|
|
7
|
+
|
|
8
|
+
Validation: STORY-ID format ^STORY-\d{3,4}$
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from typing import List, Tuple, Union
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Valid STORY-ID regex pattern
|
|
16
|
+
STORY_ID_PATTERN = re.compile(r'^STORY-\d{3,4}$')
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def normalize_depends_on(value: Union[str, List, None]) -> List[str]:
|
|
20
|
+
"""
|
|
21
|
+
Normalize depends_on input to array format.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
value: Input value (string, list, None, or other)
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
List of validated STORY-IDs (empty list if none valid)
|
|
28
|
+
|
|
29
|
+
Examples:
|
|
30
|
+
>>> normalize_depends_on(None)
|
|
31
|
+
[]
|
|
32
|
+
>>> normalize_depends_on("")
|
|
33
|
+
[]
|
|
34
|
+
>>> normalize_depends_on("STORY-044")
|
|
35
|
+
['STORY-044']
|
|
36
|
+
>>> normalize_depends_on("STORY-044, STORY-045")
|
|
37
|
+
['STORY-044', 'STORY-045']
|
|
38
|
+
>>> normalize_depends_on(["STORY-044", "STORY-045"])
|
|
39
|
+
['STORY-044', 'STORY-045']
|
|
40
|
+
"""
|
|
41
|
+
# Handle null/None
|
|
42
|
+
if value is None:
|
|
43
|
+
return []
|
|
44
|
+
|
|
45
|
+
# Handle empty string or "none" variants
|
|
46
|
+
if isinstance(value, str):
|
|
47
|
+
stripped = value.strip().lower()
|
|
48
|
+
if stripped in ('', 'none', 'null', '[]'):
|
|
49
|
+
return []
|
|
50
|
+
return _parse_string_input(value)
|
|
51
|
+
|
|
52
|
+
# Handle list input
|
|
53
|
+
if isinstance(value, list):
|
|
54
|
+
return _validate_list(value)
|
|
55
|
+
|
|
56
|
+
# Unknown type - return empty
|
|
57
|
+
return []
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _parse_string_input(value: str) -> List[str]:
|
|
61
|
+
"""Parse comma/space-separated string into validated list."""
|
|
62
|
+
parts = re.split(r'[,\s]+', value.strip())
|
|
63
|
+
validated = []
|
|
64
|
+
for part in parts:
|
|
65
|
+
cleaned = part.strip().upper()
|
|
66
|
+
if cleaned and is_valid_story_id(cleaned):
|
|
67
|
+
validated.append(cleaned)
|
|
68
|
+
return validated
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _validate_list(values: List) -> List[str]:
|
|
72
|
+
"""Validate list of values, filtering invalid entries."""
|
|
73
|
+
validated = []
|
|
74
|
+
for value in values:
|
|
75
|
+
if isinstance(value, str):
|
|
76
|
+
cleaned = value.strip().upper()
|
|
77
|
+
if is_valid_story_id(cleaned):
|
|
78
|
+
validated.append(cleaned)
|
|
79
|
+
return validated
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def is_valid_story_id(value: str) -> bool:
|
|
83
|
+
r"""
|
|
84
|
+
Check if string is valid STORY-ID format.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
value: String to validate
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
True if matches ^STORY-\d{3,4}$
|
|
91
|
+
"""
|
|
92
|
+
if not value or not isinstance(value, str):
|
|
93
|
+
return False
|
|
94
|
+
return bool(STORY_ID_PATTERN.match(value))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def validate_depends_on_input(value: Union[str, List, None]) -> Tuple[List[str], List[str]]:
|
|
98
|
+
"""
|
|
99
|
+
Validate input and return both valid and invalid entries.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
value: Input value to validate
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Tuple of (valid_ids, invalid_entries)
|
|
106
|
+
"""
|
|
107
|
+
if value is None:
|
|
108
|
+
return [], []
|
|
109
|
+
|
|
110
|
+
if isinstance(value, str):
|
|
111
|
+
stripped = value.strip().lower()
|
|
112
|
+
if stripped in ('', 'none', 'null', '[]'):
|
|
113
|
+
return [], []
|
|
114
|
+
|
|
115
|
+
parts = re.split(r'[,\s]+', value.strip())
|
|
116
|
+
valid, invalid = [], []
|
|
117
|
+
|
|
118
|
+
for part in parts:
|
|
119
|
+
cleaned = part.strip()
|
|
120
|
+
if cleaned:
|
|
121
|
+
upper = cleaned.upper()
|
|
122
|
+
if is_valid_story_id(upper):
|
|
123
|
+
valid.append(upper)
|
|
124
|
+
else:
|
|
125
|
+
invalid.append(cleaned)
|
|
126
|
+
return valid, invalid
|
|
127
|
+
|
|
128
|
+
if isinstance(value, list):
|
|
129
|
+
valid, invalid = [], []
|
|
130
|
+
for item in value:
|
|
131
|
+
if isinstance(item, str):
|
|
132
|
+
cleaned = item.strip()
|
|
133
|
+
if cleaned:
|
|
134
|
+
upper = cleaned.upper()
|
|
135
|
+
if is_valid_story_id(upper):
|
|
136
|
+
valid.append(upper)
|
|
137
|
+
else:
|
|
138
|
+
invalid.append(cleaned)
|
|
139
|
+
return valid, invalid
|
|
140
|
+
|
|
141
|
+
return [], []
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
__all__ = [
|
|
145
|
+
'normalize_depends_on',
|
|
146
|
+
'is_valid_story_id',
|
|
147
|
+
'validate_depends_on_input',
|
|
148
|
+
'STORY_ID_PATTERN'
|
|
149
|
+
]
|