#!/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_contents ## Architecture Context (for understanding test boundaries) $arch_contents ## Design Context (if available) $design_context ## 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: ### ${story_id}-E2E-001: - Priority: P0|P1|P2 - Type: e2e|integration|unit - Given: - When: - Then: - Data: ### ${story_id}-E2E-002: - Priority: P1 - Type: e2e - Given: - When: - Then: ## AC2: ### ${story_id}-UNIT-001: ... edge_cases: - error_scenarios: - test_file_mapping: - ${story_id}-E2E-*: - ${story_id}-UNIT-*: - ${story_id}-INT-*: 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\": } \`\`\` 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 $LAST_TEST_SPEC ## Story Context $story_contents ## 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: ', () => { describe('AC1: ', () => { test('${story_id}-E2E-001: should ', async () => { // Given: const setup = await createTestFixture(); // When: const result = await performAction(setup); // Then: 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\": [\"\"], \"tests_added\": } \`\`\` 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. $LAST_TEST_SPEC ### 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 }