Kaynağa Gözat

Add brainstorm-to-plan handoff regression test

Tests that Claude invokes writing-plans skill (not EnterPlanMode) after
brainstorming. Includes --without-fix mode that strips the fix from a
plugin copy to attempt failure reproduction.
Jesse Vincent 7 ay önce
ebeveyn
işleme
bdd852d945

+ 2 - 0
tests/claude-code/run-skill-tests.sh

@@ -58,6 +58,7 @@ while [[ $# -gt 0 ]]; do
             echo ""
             echo "Tests:"
             echo "  test-subagent-driven-development.sh  Test skill loading and requirements"
+            echo "  test-brainstorm-handoff.sh           Test brainstorm→writing-plans handoff"
             echo ""
             echo "Integration Tests (use --integration):"
             echo "  test-subagent-driven-development-integration.sh  Full workflow execution"
@@ -74,6 +75,7 @@ done
 # List of skill tests to run (fast unit tests)
 tests=(
     "test-subagent-driven-development.sh"
+    "test-brainstorm-handoff.sh"
 )
 
 # Integration tests (slow, full execution)

+ 317 - 0
tests/claude-code/test-brainstorm-handoff.sh

@@ -0,0 +1,317 @@
+#!/usr/bin/env bash
+# Test: Brainstorm-to-plan handoff
+#
+# Verifies that after brainstorming, Claude invokes the writing-plans skill
+# instead of using EnterPlanMode.
+#
+# The failure mode this catches:
+#   User says "build it" after brainstorming -> Claude calls EnterPlanMode
+#   (because the system prompt's planning guidance overpowers the brainstorming
+#   skill's instructions, which were loaded many turns ago)
+#
+# PASS: Skill tool invoked with "writing-plans" AND EnterPlanMode NOT invoked
+# FAIL: EnterPlanMode invoked OR writing-plans not invoked
+#
+# Usage:
+#   ./test-brainstorm-handoff.sh                  # Normal test (expects PASS)
+#   ./test-brainstorm-handoff.sh --without-fix    # Strip fix, reproduce failure
+#   ./test-brainstorm-handoff.sh --verbose        # Show full output
+#
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PLUGIN_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
+
+# Parse flags
+VERBOSE=false
+WITHOUT_FIX=false
+while [[ $# -gt 0 ]]; do
+    case $1 in
+        --verbose|-v) VERBOSE=true; shift ;;
+        --without-fix) WITHOUT_FIX=true; shift ;;
+        *) echo "Unknown flag: $1"; exit 1 ;;
+    esac
+done
+
+TIMESTAMP=$(date +%s)
+OUTPUT_DIR="/tmp/superpowers-tests/${TIMESTAMP}/brainstorm-handoff"
+mkdir -p "$OUTPUT_DIR"
+
+echo "=== Brainstorm-to-Plan Handoff Test ==="
+echo "Mode: $([ "$WITHOUT_FIX" = true ] && echo "WITHOUT FIX (expect failure)" || echo "WITH FIX (expect pass)")"
+echo "Output: $OUTPUT_DIR"
+echo ""
+
+# --- Project Setup ---
+
+PROJECT_DIR="$OUTPUT_DIR/project"
+mkdir -p "$PROJECT_DIR/src"
+mkdir -p "$PROJECT_DIR/docs/superpowers/specs"
+
+cat > "$PROJECT_DIR/package.json" << 'PROJ_EOF'
+{
+  "name": "my-express-app",
+  "version": "1.0.0",
+  "type": "module",
+  "dependencies": {
+    "express": "^4.18.0",
+    "better-sqlite3": "^9.0.0"
+  }
+}
+PROJ_EOF
+
+cat > "$PROJECT_DIR/src/index.js" << 'PROJ_EOF'
+import express from 'express';
+const app = express();
+app.use(express.json());
+
+app.get('/health', (req, res) => res.json({ status: 'ok' }));
+
+const PORT = process.env.PORT || 3000;
+app.listen(PORT, () => console.log(`Listening on ${PORT}`));
+PROJ_EOF
+
+# Pre-create a spec document (simulating completed brainstorming)
+cat > "$PROJECT_DIR/docs/superpowers/specs/2025-01-15-url-shortener-design.md" << 'SPEC_EOF'
+# URL Shortener Design Spec
+
+## Overview
+Add URL shortening capability to the existing Express.js API.
+
+## Features
+- POST /api/shorten accepts { url } and returns { shortCode, shortUrl }
+- GET /:code redirects to the original URL (302)
+- GET /api/stats/:code returns { clicks, createdAt, originalUrl }
+
+## Technical Design
+
+### Database
+Single SQLite table via better-sqlite3:
+```sql
+CREATE TABLE urls (
+  id INTEGER PRIMARY KEY AUTOINCREMENT,
+  short_code TEXT UNIQUE NOT NULL,
+  original_url TEXT NOT NULL,
+  clicks INTEGER DEFAULT 0,
+  created_at TEXT DEFAULT (datetime('now'))
+);
+CREATE INDEX idx_short_code ON urls(short_code);
+```
+
+### File Structure
+- `src/index.js` — modified to mount new routes
+- `src/db.js` — database initialization and query functions
+- `src/shorten.js` — route handlers for all three endpoints
+- `src/code-generator.js` — random 6-char alphanumeric code generation
+
+### Code Generation
+Random 6-character alphanumeric codes using crypto.randomBytes.
+Check for collisions and retry (astronomically unlikely with 36^6 space).
+
+### Validation
+- URL must be present and start with http:// or https://
+- Return 400 with { error: "..." } for invalid input
+
+### Error Handling
+- 404 with { error: "Not found" } for unknown short codes
+- 500 with { error: "Internal server error" } for database failures
+
+## Decisions
+- 302 redirects (not 301) so browsers don't cache and we always track clicks
+- Database path configurable via DATABASE_PATH env var, defaults to ./data/urls.db
+- No auth, no custom codes, no expiry — keeping it simple
+SPEC_EOF
+
+# Initialize git so brainstorming can inspect project state
+cd "$PROJECT_DIR"
+git init -q
+git add -A
+git commit -q -m "Initial commit with URL shortener spec"
+
+# --- Plugin Setup ---
+
+EFFECTIVE_PLUGIN_DIR="$PLUGIN_DIR"
+
+if [ "$WITHOUT_FIX" = true ]; then
+    echo "Creating plugin copy without the handoff fix..."
+    EFFECTIVE_PLUGIN_DIR="$OUTPUT_DIR/plugin-without-fix"
+    cp -R "$PLUGIN_DIR" "$EFFECTIVE_PLUGIN_DIR"
+
+    # Strip fix from brainstorming SKILL.md: revert to old implementation section
+    python3 << PYEOF
+import pathlib
+p = pathlib.Path('$EFFECTIVE_PLUGIN_DIR/skills/brainstorming/SKILL.md')
+content = p.read_text()
+content = content.replace(
+    '**Implementation (if continuing):**\nWhen the user approves the design and wants to build:\n1. **Invoke \`superpowers:writing-plans\` using the Skill tool.** Not EnterPlanMode. Not plan mode. Not direct implementation. The Skill tool.\n2. After the plan is written, use superpowers:using-git-worktrees to create an isolated workspace for implementation.',
+    '**Implementation (if continuing):**\n- Ask: "Ready to set up for implementation?"\n- Use superpowers:using-git-worktrees to create isolated workspace\n- **REQUIRED:** Use superpowers:writing-plans to create detailed implementation plan'
+)
+p.write_text(content)
+PYEOF
+
+    # Strip fix from using-superpowers: remove EnterPlanMode red flag
+    python3 << PYEOF
+import pathlib
+p = pathlib.Path('$EFFECTIVE_PLUGIN_DIR/skills/using-superpowers/SKILL.md')
+lines = p.read_text().splitlines(keepends=True)
+lines = [l for l in lines if 'I should use EnterPlanMode' not in l]
+p.write_text(''.join(lines))
+PYEOF
+
+    # Strip fix from writing-plans: revert description and context
+    python3 << PYEOF
+import pathlib
+p = pathlib.Path('$EFFECTIVE_PLUGIN_DIR/skills/writing-plans/SKILL.md')
+content = p.read_text()
+content = content.replace(
+    'description: Use when you have a spec or requirements for a multi-step task, before touching code. After brainstorming, ALWAYS use this — not EnterPlanMode or plan mode.',
+    'description: Use when you have a spec or requirements for a multi-step task, before touching code'
+)
+content = content.replace(
+    '**Context:** This runs in the main workspace after brainstorming, while context is fresh. The worktree is created afterward for implementation.',
+    '**Context:** This should be run in a dedicated worktree (created by brainstorming skill).'
+)
+p.write_text(content)
+PYEOF
+    echo "Plugin copy created at $EFFECTIVE_PLUGIN_DIR"
+    echo ""
+fi
+
+# --- Run Conversation ---
+
+cd "$PROJECT_DIR"
+
+# Turn 1: Load brainstorming and establish that we finished the design
+# The key is that brainstorming gets loaded into context, and we're at the handoff point
+echo ">>> Turn 1: Loading brainstorming skill and establishing context..."
+TURN1_LOG="$OUTPUT_DIR/turn1.json"
+
+TURN1_PROMPT='I want to add URL shortening to this Express app. I already have the full design worked out and written to docs/superpowers/specs/2025-01-15-url-shortener-design.md. Please read the spec.'
+
+timeout 300 claude -p "$TURN1_PROMPT" \
+    --plugin-dir "$EFFECTIVE_PLUGIN_DIR" \
+    --dangerously-skip-permissions \
+    --max-turns 5 \
+    --output-format stream-json \
+    > "$TURN1_LOG" 2>&1 || true
+
+echo "Turn 1 complete."
+if [ "$VERBOSE" = true ]; then
+    echo "---"
+    grep '"type":"assistant"' "$TURN1_LOG" | tail -1 | jq -r '.message.content[0].text // empty' 2>/dev/null | head -c 800 || true
+    echo ""
+    echo "---"
+fi
+echo ""
+
+# Turn 2: Approve and ask to build - this is the critical handoff moment
+echo ">>> Turn 2: 'The spec is done. Build it.' (critical handoff)..."
+TURN2_LOG="$OUTPUT_DIR/turn2.json"
+
+TURN2_PROMPT='The spec is complete and I am happy with the design. Build it.'
+
+timeout 300 claude -p "$TURN2_PROMPT" \
+    --continue \
+    --plugin-dir "$EFFECTIVE_PLUGIN_DIR" \
+    --dangerously-skip-permissions \
+    --max-turns 5 \
+    --output-format stream-json \
+    > "$TURN2_LOG" 2>&1 || true
+
+echo "Turn 2 complete."
+if [ "$VERBOSE" = true ]; then
+    echo "---"
+    grep '"type":"assistant"' "$TURN2_LOG" | tail -1 | jq -r '.message.content[0].text // empty' 2>/dev/null | head -c 800 || true
+    echo ""
+    echo "---"
+fi
+echo ""
+
+# --- Assertions ---
+
+echo "=== Results ==="
+echo ""
+
+# Combine all turn logs for analysis
+ALL_LOGS="$OUTPUT_DIR/all-turns.json"
+cat "$TURN1_LOG" "$TURN2_LOG" > "$ALL_LOGS"
+
+# Detection: writing-plans skill invoked?
+HAS_WRITING_PLANS=false
+if grep -q '"name":"Skill"' "$ALL_LOGS" 2>/dev/null && grep -q 'writing-plans' "$ALL_LOGS" 2>/dev/null; then
+    HAS_WRITING_PLANS=true
+fi
+
+# Detection: EnterPlanMode invoked?
+HAS_ENTER_PLAN_MODE=false
+if grep -q '"name":"EnterPlanMode"' "$ALL_LOGS" 2>/dev/null; then
+    HAS_ENTER_PLAN_MODE=true
+fi
+
+# Report what skills were invoked
+echo "Skills invoked:"
+grep -o '"skill":"[^"]*"' "$ALL_LOGS" 2>/dev/null | sort -u || echo "  (none)"
+echo ""
+
+echo "Notable tools invoked:"
+grep -o '"name":"[A-Z][^"]*"' "$ALL_LOGS" 2>/dev/null | sort | uniq -c | sort -rn | head -10 || echo "  (none)"
+echo ""
+
+# Determine result
+PASSED=false
+if [ "$WITHOUT_FIX" = true ]; then
+    # In without-fix mode, we EXPECT the failure (EnterPlanMode)
+    echo "--- Without-Fix Mode (reproducing failure) ---"
+    if [ "$HAS_ENTER_PLAN_MODE" = true ]; then
+        echo "REPRODUCED: Claude used EnterPlanMode (the bug we're fixing)"
+        PASSED=true
+    elif [ "$HAS_WRITING_PLANS" = true ]; then
+        echo "NOT REPRODUCED: Claude used writing-plans even without the fix"
+        echo "(The model may have followed the old guidance anyway)"
+        PASSED=false
+    else
+        echo "INCONCLUSIVE: Claude used neither writing-plans nor EnterPlanMode"
+        echo "The brainstorming flow may not have reached the handoff point."
+        PASSED=false
+    fi
+else
+    # Normal mode: expect writing-plans, not EnterPlanMode
+    echo "--- With-Fix Mode (verifying fix) ---"
+    if [ "$HAS_WRITING_PLANS" = true ] && [ "$HAS_ENTER_PLAN_MODE" = false ]; then
+        echo "PASS: Claude used writing-plans skill (correct handoff)"
+        PASSED=true
+    elif [ "$HAS_ENTER_PLAN_MODE" = true ]; then
+        echo "FAIL: Claude used EnterPlanMode instead of writing-plans"
+        PASSED=false
+    elif [ "$HAS_WRITING_PLANS" = true ] && [ "$HAS_ENTER_PLAN_MODE" = true ]; then
+        echo "FAIL: Claude used BOTH writing-plans AND EnterPlanMode"
+        PASSED=false
+    else
+        echo "INCONCLUSIVE: Claude used neither writing-plans nor EnterPlanMode"
+        echo "The brainstorming flow may not have reached the handoff point."
+        echo "Check logs - brainstorming may still be asking questions."
+        PASSED=false
+    fi
+fi
+
+echo ""
+
+# Show the critical turn 2 response
+echo "Turn 2 response (first 500 chars):"
+grep '"type":"assistant"' "$TURN2_LOG" 2>/dev/null | tail -1 | \
+    jq -r '.message.content[0].text // .message.content' 2>/dev/null | \
+    head -c 500 || echo "  (could not extract)"
+echo ""
+
+echo ""
+echo "Logs:"
+echo "  Turn 1: $TURN1_LOG"
+echo "  Turn 2: $TURN2_LOG"
+echo "  Combined: $ALL_LOGS"
+echo ""
+
+if [ "$PASSED" = true ]; then
+    exit 0
+else
+    exit 1
+fi