461 lines
13 KiB
Bash
461 lines
13 KiB
Bash
#!/bin/bash
|
|
#
|
|
# BMAD Epic Execute - TDD Flow Module
|
|
#
|
|
# Provides test-first development (TDD) workflow phases:
|
|
# 1. Test Specification - Generate test specs from acceptance criteria
|
|
# 2. Test Implementation - Create failing tests from specs
|
|
# 3. Test Verification - Verify tests fail appropriately before implementation
|
|
#
|
|
# Usage: Sourced by epic-execute.sh
|
|
#
|
|
|
|
# =============================================================================
|
|
# TDD Flow Variables
|
|
# =============================================================================
|
|
|
|
# Store test specifications for use across phases
|
|
LAST_TEST_SPEC=""
|
|
|
|
# Test spec output directory
|
|
TEST_SPEC_DIR=""
|
|
|
|
# =============================================================================
|
|
# Test Specification Phase
|
|
# =============================================================================
|
|
|
|
# Execute test specification phase
|
|
# Generates BDD-style test specifications from acceptance criteria
|
|
# Arguments:
|
|
# $1 - story_file path
|
|
execute_test_spec_phase() {
|
|
local story_file="$1"
|
|
local story_id=$(basename "$story_file" .md)
|
|
|
|
# Reset last spec
|
|
LAST_TEST_SPEC=""
|
|
|
|
log ">>> TEST SPEC PHASE: $story_id (generating test specifications)"
|
|
|
|
local story_contents=$(cat "$story_file")
|
|
|
|
# Load architecture file if available for context
|
|
local arch_contents=""
|
|
for search_path in "$PROJECT_ROOT/docs/architecture.md" "$PROJECT_ROOT/docs/architecture/architecture.md" "$PROJECT_ROOT/architecture.md"; do
|
|
if [ -f "$search_path" ]; then
|
|
arch_contents=$(cat "$search_path")
|
|
break
|
|
fi
|
|
done
|
|
|
|
# Get design context if available
|
|
local design_context=""
|
|
if type get_last_design >/dev/null 2>&1; then
|
|
design_context=$(get_last_design)
|
|
fi
|
|
|
|
local spec_prompt="You are a Test Architect (TEA) generating test specifications from acceptance criteria.
|
|
|
|
## Your Task
|
|
|
|
Generate test specifications for: $story_id
|
|
|
|
Do NOT write test code yet. Output only test specifications in BDD format.
|
|
|
|
### CRITICAL RULES
|
|
- One test specification per acceptance criterion minimum
|
|
- Use Given-When-Then format for all specifications
|
|
- Include edge cases and error scenarios
|
|
- Assign unique test IDs (format: ${story_id}-E2E-001, ${story_id}-UNIT-001)
|
|
- Map each AC explicitly to test specifications
|
|
|
|
## Story to Analyze
|
|
|
|
**Story Path:** $story_file
|
|
**Story ID:** $story_id
|
|
|
|
<story>
|
|
$story_contents
|
|
</story>
|
|
|
|
## Architecture Context (for understanding test boundaries)
|
|
|
|
<architecture>
|
|
$arch_contents
|
|
</architecture>
|
|
|
|
## Design Context (if available)
|
|
|
|
<design>
|
|
$design_context
|
|
</design>
|
|
|
|
## Exploration Commands
|
|
|
|
First, explore existing test patterns in the codebase:
|
|
\`\`\`bash
|
|
# Find existing test files
|
|
find . -type f \\( -name \"*.spec.ts\" -o -name \"*.test.ts\" -o -name \"*.spec.js\" -o -name \"*.test.js\" \\) | head -10
|
|
# Check test directory structure
|
|
ls -la test/ tests/ __tests__/ src/**/__tests__/ 2>/dev/null || true
|
|
\`\`\`
|
|
|
|
## Required Output
|
|
|
|
Output your test specifications in this exact format:
|
|
|
|
\`\`\`
|
|
TEST SPEC START
|
|
story_id: $story_id
|
|
generated: $(date '+%Y-%m-%d')
|
|
|
|
test_specifications:
|
|
|
|
## AC1: <acceptance criterion text>
|
|
|
|
### ${story_id}-E2E-001: <descriptive test name>
|
|
- Priority: P0|P1|P2
|
|
- Type: e2e|integration|unit
|
|
- Given: <precondition state>
|
|
- When: <action performed>
|
|
- Then: <expected outcome>
|
|
- Data: <test data requirements>
|
|
|
|
### ${story_id}-E2E-002: <edge case for AC1>
|
|
- Priority: P1
|
|
- Type: e2e
|
|
- Given: <edge case precondition>
|
|
- When: <action>
|
|
- Then: <expected error/behavior>
|
|
|
|
## AC2: <next acceptance criterion>
|
|
|
|
### ${story_id}-UNIT-001: <unit test name>
|
|
...
|
|
|
|
edge_cases:
|
|
- <scenario not in ACs but important>
|
|
|
|
error_scenarios:
|
|
- <error condition to test>
|
|
|
|
test_file_mapping:
|
|
- ${story_id}-E2E-*: <suggested test file path>
|
|
- ${story_id}-UNIT-*: <suggested test file path>
|
|
- ${story_id}-INT-*: <suggested test file path>
|
|
|
|
TEST SPEC END
|
|
\`\`\`
|
|
|
|
## Completion Signal
|
|
|
|
After outputting the spec block:
|
|
|
|
1. Output JSON result:
|
|
\`\`\`json
|
|
{
|
|
\"status\": \"COMPLETE\",
|
|
\"story_id\": \"$story_id\",
|
|
\"summary\": \"Generated N test specifications for M acceptance criteria\",
|
|
\"tests_added\": <number of specs>
|
|
}
|
|
\`\`\`
|
|
|
|
2. Then output: TEST SPEC COMPLETE: $story_id - Generated N specifications"
|
|
|
|
if [ "$DRY_RUN" = true ]; then
|
|
echo "[DRY RUN] Would execute test spec phase for $story_id"
|
|
return 0
|
|
fi
|
|
|
|
local result
|
|
result=$(claude --dangerously-skip-permissions -p "$spec_prompt" 2>&1) || true
|
|
|
|
echo "$result" >> "$LOG_FILE"
|
|
|
|
# Extract test spec block
|
|
LAST_TEST_SPEC=$(echo "$result" | sed -n '/TEST SPEC START/,/TEST SPEC END/p')
|
|
|
|
if [ -n "$LAST_TEST_SPEC" ]; then
|
|
# Save to spec directory
|
|
TEST_SPEC_DIR="$SPRINT_ARTIFACTS_DIR/test-specs"
|
|
mkdir -p "$TEST_SPEC_DIR"
|
|
echo "$LAST_TEST_SPEC" > "$TEST_SPEC_DIR/${story_id}-test-spec.md"
|
|
|
|
# Save to decision log
|
|
if type append_to_decision_log >/dev/null 2>&1; then
|
|
append_to_decision_log "TEST_SPEC" "$story_id" "$LAST_TEST_SPEC"
|
|
fi
|
|
|
|
log_success "Test spec phase complete: $story_id"
|
|
log "Saved to: $TEST_SPEC_DIR/${story_id}-test-spec.md"
|
|
return 0
|
|
else
|
|
log_error "Test spec phase did not produce valid output"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# =============================================================================
|
|
# Test Implementation Phase
|
|
# =============================================================================
|
|
|
|
# Execute test implementation phase
|
|
# Creates failing tests from specifications
|
|
# Arguments:
|
|
# $1 - story_file path
|
|
execute_test_impl_phase() {
|
|
local story_file="$1"
|
|
local story_id=$(basename "$story_file" .md)
|
|
|
|
log ">>> TEST IMPL PHASE: $story_id (implementing failing tests)"
|
|
|
|
# Check if we have test specs
|
|
if [ -z "$LAST_TEST_SPEC" ]; then
|
|
# Try to load from file
|
|
if [ -f "$TEST_SPEC_DIR/${story_id}-test-spec.md" ]; then
|
|
LAST_TEST_SPEC=$(cat "$TEST_SPEC_DIR/${story_id}-test-spec.md")
|
|
else
|
|
log_error "No test specifications available for $story_id"
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
local story_contents=$(cat "$story_file")
|
|
|
|
local impl_prompt="You are a Test Architect (TEA) implementing tests from specifications.
|
|
|
|
## Your Task
|
|
|
|
Implement failing tests for: $story_id
|
|
|
|
The tests MUST FAIL initially because the feature is not yet implemented.
|
|
This is Test-First Development (TDD).
|
|
|
|
### CRITICAL RULES
|
|
- Create test files based on the specifications below
|
|
- Tests should compile/parse without errors
|
|
- Tests should FAIL when run (feature not implemented yet)
|
|
- Follow existing test patterns in the codebase
|
|
- Use proper fixtures and data factories
|
|
- Do NOT implement any feature code
|
|
|
|
## Test Specifications to Implement
|
|
|
|
<test-spec>
|
|
$LAST_TEST_SPEC
|
|
</test-spec>
|
|
|
|
## Story Context
|
|
|
|
<story>
|
|
$story_contents
|
|
</story>
|
|
|
|
## Exploration Commands
|
|
|
|
First, examine existing test patterns:
|
|
\`\`\`bash
|
|
# Find existing test patterns
|
|
find . -type f \\( -name \"*.spec.ts\" -o -name \"*.test.ts\" \\) -exec head -50 {} \\; 2>/dev/null | head -100
|
|
# Check for test utilities
|
|
ls -la test/utils/ tests/helpers/ __tests__/fixtures/ 2>/dev/null || true
|
|
\`\`\`
|
|
|
|
## Implementation Guidelines
|
|
|
|
1. **File Structure**: Create test files in the appropriate directory
|
|
2. **Imports**: Use the project's test framework (jest, mocha, vitest, etc.)
|
|
3. **Describe Blocks**: Group tests by acceptance criterion
|
|
4. **Test Names**: Include test IDs from specifications
|
|
5. **BDD Format**: Use Given-When-Then comments
|
|
6. **Assertions**: Write assertions that will FAIL until feature is implemented
|
|
7. **Data**: Use factories or fixtures, no hardcoded values
|
|
|
|
## Example Test Structure
|
|
|
|
\`\`\`typescript
|
|
describe('Feature: <story description>', () => {
|
|
describe('AC1: <acceptance criterion>', () => {
|
|
test('${story_id}-E2E-001: should <expected behavior>', async () => {
|
|
// Given: <precondition>
|
|
const setup = await createTestFixture();
|
|
|
|
// When: <action>
|
|
const result = await performAction(setup);
|
|
|
|
// Then: <expected outcome>
|
|
expect(result).toBe(expectedValue); // Will FAIL - not implemented
|
|
});
|
|
});
|
|
});
|
|
\`\`\`
|
|
|
|
## Completion Signal
|
|
|
|
After implementing the tests:
|
|
|
|
1. Stage the test files: git add -A
|
|
2. Output JSON result:
|
|
\`\`\`json
|
|
{
|
|
\"status\": \"COMPLETE\",
|
|
\"story_id\": \"$story_id\",
|
|
\"summary\": \"Implemented N failing tests in M files\",
|
|
\"files_changed\": [\"<test file paths>\"],
|
|
\"tests_added\": <number>
|
|
}
|
|
\`\`\`
|
|
|
|
3. Then output: TEST IMPL COMPLETE: $story_id - Implemented N tests"
|
|
|
|
if [ "$DRY_RUN" = true ]; then
|
|
echo "[DRY RUN] Would execute test impl phase for $story_id"
|
|
return 0
|
|
fi
|
|
|
|
local result
|
|
result=$(claude --dangerously-skip-permissions -p "$impl_prompt" 2>&1) || true
|
|
|
|
echo "$result" >> "$LOG_FILE"
|
|
|
|
# Check completion
|
|
local completion_status
|
|
if type check_phase_completion >/dev/null 2>&1; then
|
|
check_phase_completion "$result" "test_gen" "$story_id"
|
|
completion_status=$?
|
|
else
|
|
if echo "$result" | grep -q "TEST IMPL COMPLETE"; then
|
|
completion_status=0
|
|
else
|
|
completion_status=2
|
|
fi
|
|
fi
|
|
|
|
case $completion_status in
|
|
0)
|
|
log_success "Test impl phase complete: $story_id"
|
|
return 0
|
|
;;
|
|
*)
|
|
log_error "Test impl phase did not complete cleanly: $story_id"
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# =============================================================================
|
|
# Test Verification Phase
|
|
# =============================================================================
|
|
|
|
# Execute test verification phase
|
|
# Verifies that tests fail appropriately (compile but don't pass)
|
|
# Arguments:
|
|
# $1 - story_file path
|
|
execute_test_verification_phase() {
|
|
local story_file="$1"
|
|
local story_id=$(basename "$story_file" .md)
|
|
|
|
log ">>> TEST VERIFICATION PHASE: $story_id (verifying tests fail correctly)"
|
|
|
|
if [ "$DRY_RUN" = true ]; then
|
|
echo "[DRY RUN] Would verify tests fail for $story_id"
|
|
return 0
|
|
fi
|
|
|
|
# Run tests and expect failures
|
|
local test_output=""
|
|
local test_exit_code=0
|
|
|
|
if [ -f "$PROJECT_ROOT/package.json" ]; then
|
|
# Node.js project
|
|
test_output=$(cd "$PROJECT_ROOT" && npm test 2>&1) || test_exit_code=$?
|
|
elif [ -f "$PROJECT_ROOT/Cargo.toml" ]; then
|
|
test_output=$(cd "$PROJECT_ROOT" && cargo test 2>&1) || test_exit_code=$?
|
|
elif [ -f "$PROJECT_ROOT/go.mod" ]; then
|
|
test_output=$(cd "$PROJECT_ROOT" && go test ./... 2>&1) || test_exit_code=$?
|
|
elif [ -f "$PROJECT_ROOT/requirements.txt" ] || [ -f "$PROJECT_ROOT/pyproject.toml" ]; then
|
|
if command -v pytest >/dev/null 2>&1; then
|
|
test_output=$(cd "$PROJECT_ROOT" && pytest 2>&1) || test_exit_code=$?
|
|
fi
|
|
fi
|
|
|
|
echo "$test_output" >> "$LOG_FILE"
|
|
|
|
# Analyze test output
|
|
# We expect: tests compile, tests run, tests FAIL (exit code non-zero)
|
|
|
|
# Check for compilation errors (bad - tests should at least compile)
|
|
if echo "$test_output" | grep -qiE "syntax error|cannot find module|compilation failed|parse error"; then
|
|
log_error "Tests have compilation/syntax errors - fix before proceeding"
|
|
echo "$test_output" | grep -iE "syntax error|cannot find module|compilation failed|parse error" | head -10
|
|
return 1
|
|
fi
|
|
|
|
# Check for test failures (good - expected in TDD)
|
|
if [ $test_exit_code -ne 0 ]; then
|
|
# Count failures
|
|
local failure_count=0
|
|
failure_count=$(echo "$test_output" | grep -cE "FAIL|failed|failing" || echo "0")
|
|
|
|
if [ "$failure_count" -gt 0 ]; then
|
|
log_success "Test verification passed: $failure_count test(s) failing as expected"
|
|
log "Tests compile and fail appropriately - ready for implementation"
|
|
return 0
|
|
else
|
|
log_warn "Tests exited with error but no clear failures detected"
|
|
return 0 # Proceed anyway - might be framework difference
|
|
fi
|
|
else
|
|
# Tests passed - this is unexpected in TDD before implementation
|
|
log_warn "Tests passed unexpectedly - verify tests are actually testing new functionality"
|
|
log "This may indicate tests are not properly written or feature already exists"
|
|
return 0 # Don't block, but warn
|
|
fi
|
|
}
|
|
|
|
# =============================================================================
|
|
# Helper Functions
|
|
# =============================================================================
|
|
|
|
# Get the last test specification for use in other phases
|
|
get_last_test_spec() {
|
|
echo "$LAST_TEST_SPEC"
|
|
}
|
|
|
|
# Build test spec context for dev phase prompt
|
|
build_test_spec_context_for_dev() {
|
|
local story_id="$1"
|
|
|
|
if [ -z "$LAST_TEST_SPEC" ]; then
|
|
# Try to load from file
|
|
if [ -n "$TEST_SPEC_DIR" ] && [ -f "$TEST_SPEC_DIR/${story_id}-test-spec.md" ]; then
|
|
LAST_TEST_SPEC=$(cat "$TEST_SPEC_DIR/${story_id}-test-spec.md")
|
|
fi
|
|
fi
|
|
|
|
if [ -z "$LAST_TEST_SPEC" ]; then
|
|
echo ""
|
|
return
|
|
fi
|
|
|
|
cat << EOF
|
|
|
|
## Test Specifications (TDD)
|
|
|
|
The following tests have been written and are FAILING. Your implementation must make these tests pass.
|
|
|
|
<test-specifications>
|
|
$LAST_TEST_SPEC
|
|
</test-specifications>
|
|
|
|
### TDD Implementation Guidelines
|
|
|
|
1. Run tests frequently: \`npm test\` (or equivalent)
|
|
2. Implement just enough code to make the next test pass
|
|
3. Do NOT modify the test files - only implement the feature code
|
|
4. All tests in the specification must pass when implementation is complete
|
|
|
|
EOF
|
|
}
|