specweave 1.0.39 → 1.0.41
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 +18 -18
- package/dist/src/core/living-docs/living-docs-sync.d.ts.map +1 -1
- package/dist/src/core/living-docs/living-docs-sync.js +16 -0
- package/dist/src/core/living-docs/living-docs-sync.js.map +1 -1
- package/dist/src/core/project/project-service.d.ts.map +1 -1
- package/dist/src/core/project/project-service.js +5 -6
- package/dist/src/core/project/project-service.js.map +1 -1
- package/dist/src/hooks/processor.d.ts +5 -2
- package/dist/src/hooks/processor.d.ts.map +1 -1
- package/dist/src/hooks/processor.js +17 -6
- package/dist/src/hooks/processor.js.map +1 -1
- package/package.json +1 -1
- package/plugins/specweave/commands/save.md +28 -0
- package/plugins/specweave/hooks/hooks.json +0 -32
- package/plugins/specweave/hooks/lib/common-setup.sh +0 -18
- package/plugins/specweave/hooks/v2/guards/completion-guard.sh +7 -69
- package/plugins/specweave/hooks/v2/guards/increment-duplicate-guard.sh +7 -142
- package/plugins/specweave/hooks/v2/guards/metadata-json-guard.sh +8 -92
- package/plugins/specweave/hooks/v2/guards/spec-validation-guard.sh +7 -168
- package/plugins/specweave/hooks/v2/handlers/github-sync-handler.sh +12 -1
- package/plugins/specweave/hooks/v2/queue/processor.sh +21 -7
- package/plugins/specweave/hooks/v2/guards/metadata-json-guard.test.sh +0 -302
|
@@ -1,175 +1,14 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# spec-validation-guard.sh -
|
|
2
|
+
# spec-validation-guard.sh - DISABLED (v1.0.38)
|
|
3
3
|
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
# - spec-project-validator.sh (deleted) - Placeholder detection
|
|
7
|
-
# - project-folder-guard.sh (deleted) - Living docs folder validation
|
|
4
|
+
# This guard was converted to WARNING-only in v1.0.37, and now completely
|
|
5
|
+
# disabled in v1.0.38 per user feedback: "you MUST NEVER block such operations"
|
|
8
6
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# - file_path matches: .specweave/increments/*/spec.md OR .specweave/docs/internal/specs/*/
|
|
7
|
+
# Spec validation should be handled by agents/scripts with proper business logic,
|
|
8
|
+
# not by hooks that can interfere with file operations.
|
|
12
9
|
#
|
|
13
|
-
#
|
|
14
|
-
# User feedback: "you MUST NEVER block such operations... do at least warning"
|
|
15
|
-
# Business logic and validation should be in scripts/agents, not hard blocks.
|
|
16
|
-
#
|
|
17
|
-
# Exit 0 = allow (with JSON warning message if validation fails)
|
|
18
|
-
#
|
|
19
|
-
# Bypasses:
|
|
20
|
-
# - SPECWEAVE_DISABLE_HOOKS=1 - Disable all hooks
|
|
21
|
-
# - SPECWEAVE_FORCE_PROJECT=1 - Skip project validation
|
|
22
|
-
# - SPECWEAVE_FORCE_METADATA=1 - Skip all spec validation
|
|
23
|
-
|
|
24
|
-
set +e # CRITICAL: Never use set -e in hooks
|
|
25
|
-
|
|
26
|
-
# Source shared library if available
|
|
27
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
28
|
-
LIB_DIR="${SCRIPT_DIR}/../../lib"
|
|
29
|
-
if [[ -f "$LIB_DIR/common-setup.sh" ]]; then
|
|
30
|
-
source "$LIB_DIR/common-setup.sh"
|
|
31
|
-
init_pretool_guard || exit 0
|
|
32
|
-
else
|
|
33
|
-
# Fallback inline implementation
|
|
34
|
-
[[ "${SPECWEAVE_DISABLE_HOOKS:-0}" == "1" ]] && echo '{"decision":"allow"}' && exit 0
|
|
35
|
-
HOOK_INPUT=$(cat 2>/dev/null || echo '{}')
|
|
36
|
-
if ! command -v jq >/dev/null 2>&1; then
|
|
37
|
-
echo '{"decision":"allow"}'
|
|
38
|
-
exit 0
|
|
39
|
-
fi
|
|
40
|
-
HOOK_TOOL_NAME=$(echo "$HOOK_INPUT" | jq -r '.tool_name // ""' 2>/dev/null || echo "")
|
|
41
|
-
HOOK_FILE_PATH=$(echo "$HOOK_INPUT" | jq -r '.tool_input.file_path // .file_path // ""' 2>/dev/null || echo "")
|
|
42
|
-
HOOK_CONTENT=$(echo "$HOOK_INPUT" | jq -r '.tool_input.content // .tool_input.new_string // ""' 2>/dev/null || echo "")
|
|
43
|
-
fi
|
|
44
|
-
|
|
45
|
-
# Check bypass flags
|
|
46
|
-
[[ "$SPECWEAVE_FORCE_PROJECT" == "1" ]] && echo '{"decision":"allow","message":"Project validation bypassed"}' && exit 0
|
|
47
|
-
[[ "$SPECWEAVE_FORCE_METADATA" == "1" ]] && echo '{"decision":"allow","message":"Spec validation bypassed"}' && exit 0
|
|
48
|
-
|
|
49
|
-
# Only validate Write tool
|
|
50
|
-
[[ "$HOOK_TOOL_NAME" != "Write" ]] && echo '{"decision":"allow"}' && exit 0
|
|
51
|
-
|
|
52
|
-
# No file path = allow
|
|
53
|
-
[[ -z "$HOOK_FILE_PATH" ]] && echo '{"decision":"allow"}' && exit 0
|
|
54
|
-
|
|
55
|
-
# ============================================================================
|
|
56
|
-
# VALIDATION 1: spec.md placeholder detection
|
|
57
|
-
# ============================================================================
|
|
58
|
-
if [[ "$HOOK_FILE_PATH" =~ \.specweave/increments/[0-9]{3,4}E?-[^/]+/spec\.md$ ]]; then
|
|
59
|
-
|
|
60
|
-
# Check for unresolved {{...}} placeholders - WARN only (v1.0.37)
|
|
61
|
-
if echo "$HOOK_CONTENT" | grep -qE '\{\{[A-Z_]+\}\}'; then
|
|
62
|
-
PLACEHOLDERS=$(echo "$HOOK_CONTENT" | grep -oE '\{\{[A-Z_]+\}\}' | sort -u | tr '\n' ', ' | sed 's/,$//')
|
|
63
|
-
printf '{"decision":"allow","message":"⚠️ UNRESOLVED PLACEHOLDERS DETECTED\\n\\nFound: %s\\n\\n🔧 FIX: Replace placeholders with actual values.\\nRun: specweave context projects\\nThen use values from the JSON output.\\n\\nOperation ALLOWED - proceeding anyway."}\n' "$PLACEHOLDERS"
|
|
64
|
-
exit 0
|
|
65
|
-
fi
|
|
66
|
-
|
|
67
|
-
# Check for **Project**: field in User Stories (soft validation - warn, don't block)
|
|
68
|
-
# Pattern: ### US-XXX or #### US-XXX followed by **Project**:
|
|
69
|
-
US_COUNT=$(echo "$HOOK_CONTENT" | grep -cE '^#{3,4} US-' 2>/dev/null || echo "0")
|
|
70
|
-
PROJECT_COUNT=$(echo "$HOOK_CONTENT" | grep -cE '^\*\*Project\*\*:' 2>/dev/null || echo "0")
|
|
71
|
-
|
|
72
|
-
# Trim to just the number
|
|
73
|
-
US_COUNT="${US_COUNT//[^0-9]/}"
|
|
74
|
-
PROJECT_COUNT="${PROJECT_COUNT//[^0-9]/}"
|
|
75
|
-
[[ -z "$US_COUNT" ]] && US_COUNT=0
|
|
76
|
-
[[ -z "$PROJECT_COUNT" ]] && PROJECT_COUNT=0
|
|
77
|
-
|
|
78
|
-
# Only warn if there are User Stories but no Project fields
|
|
79
|
-
# Don't block - just allow with warning
|
|
80
|
-
if [[ "$US_COUNT" -gt 0 ]] && [[ "$PROJECT_COUNT" -eq 0 ]]; then
|
|
81
|
-
echo '{"decision":"allow","message":"⚠️ WARNING: No **Project**: fields found. Add **Project**: after each US heading for proper sync."}'
|
|
82
|
-
exit 0
|
|
83
|
-
fi
|
|
84
|
-
|
|
85
|
-
# Check for comma-separated projects (1:1 mapping required) - WARN only (v1.0.37)
|
|
86
|
-
if echo "$HOOK_CONTENT" | grep -qE '^\*\*Project\*\*:.*,'; then
|
|
87
|
-
printf '{"decision":"allow","message":"⚠️ MULTIPLE PROJECTS IN ONE US DETECTED\\n\\nEach User Story should map to exactly ONE project.\\n\\n💡 RECOMMENDATION: Split cross-project features into separate User Stories:\\n\\nWRONG:\\n### US-001: OAuth Implementation\\n**Project**: frontend, backend\\n\\nCORRECT:\\n### US-001: OAuth Login Form\\n**Project**: frontend\\n\\n### US-002: OAuth API\\n**Project**: backend\\n\\nOperation ALLOWED - proceeding anyway."}\n'
|
|
88
|
-
exit 0
|
|
89
|
-
fi
|
|
90
|
-
|
|
91
|
-
# Check structure level to validate **Board**: fields
|
|
92
|
-
# For 1-level structures (GitHub), **Board**: should NOT be present
|
|
93
|
-
# For 2-level structures (ADO/JIRA with boards), **Board**: is required
|
|
94
|
-
# NOTE: This is a WARNING only - do NOT block spec writes!
|
|
95
|
-
PROJECT_ROOT="${HOOK_FILE_PATH%%/.specweave/*}"
|
|
96
|
-
BOARD_COUNT=$(echo "$HOOK_CONTENT" | grep -cE '^\*\*Board\*\*:' 2>/dev/null || echo "0")
|
|
97
|
-
BOARD_COUNT="${BOARD_COUNT//[^0-9]/}"
|
|
98
|
-
[[ -z "$BOARD_COUNT" ]] && BOARD_COUNT=0
|
|
99
|
-
|
|
100
|
-
if [[ "$BOARD_COUNT" -gt 0 ]]; then
|
|
101
|
-
# Has **Board**: fields - check if this is a 2-level structure
|
|
102
|
-
CONFIG_FILE="$PROJECT_ROOT/.specweave/config.json"
|
|
103
|
-
IS_2LEVEL="false"
|
|
104
|
-
|
|
105
|
-
if [[ -f "$CONFIG_FILE" ]]; then
|
|
106
|
-
# 2-level indicators: ADO areaPathMapping, JIRA boardMapping with multiple boards
|
|
107
|
-
HAS_AREA_MAPPING=$(jq -r '.sync.profiles | to_entries[] | select(.value.provider == "ado") | .value.config.areaPathMapping.mappings | length > 0' "$CONFIG_FILE" 2>/dev/null | grep -c "true" || echo "0")
|
|
108
|
-
HAS_BOARD_MAPPING=$(jq -r '.sync.profiles | to_entries[] | select(.value.provider == "jira") | .value.config.boardMapping.boards | length > 1' "$CONFIG_FILE" 2>/dev/null | grep -c "true" || echo "0")
|
|
109
|
-
HAS_MULTI_TEAMS=$(jq -r '.umbrella.childRepos | map(.team) | unique | length > 1' "$CONFIG_FILE" 2>/dev/null || echo "false")
|
|
110
|
-
|
|
111
|
-
if [[ "$HAS_AREA_MAPPING" -gt 0 ]] || [[ "$HAS_BOARD_MAPPING" -gt 0 ]] || [[ "$HAS_MULTI_TEAMS" == "true" ]]; then
|
|
112
|
-
IS_2LEVEL="true"
|
|
113
|
-
fi
|
|
114
|
-
fi
|
|
115
|
-
|
|
116
|
-
# WARN only - never block spec.md writes (too disruptive)
|
|
117
|
-
if [[ "$IS_2LEVEL" != "true" ]]; then
|
|
118
|
-
echo '{"decision":"allow","message":"⚠️ WARNING: **Board**: fields found but this is a 1-level structure (GitHub). Board fields are only needed for ADO/JIRA with multiple boards. Consider removing **Board**: lines."}'
|
|
119
|
-
exit 0
|
|
120
|
-
fi
|
|
121
|
-
fi
|
|
122
|
-
|
|
123
|
-
echo '{"decision":"allow"}'
|
|
124
|
-
exit 0
|
|
125
|
-
fi
|
|
126
|
-
|
|
127
|
-
# ============================================================================
|
|
128
|
-
# VALIDATION 2: Living docs folder validation
|
|
129
|
-
# ============================================================================
|
|
130
|
-
if [[ "$HOOK_FILE_PATH" =~ \.specweave/docs/internal/specs/([^/]+)/ ]]; then
|
|
131
|
-
PROJECT_NAME="${BASH_REMATCH[1]}"
|
|
132
|
-
|
|
133
|
-
# Skip README.md and _features/_archive special folders
|
|
134
|
-
[[ "$PROJECT_NAME" == "README.md" ]] && echo '{"decision":"allow"}' && exit 0
|
|
135
|
-
[[ "$PROJECT_NAME" == "_features" ]] && echo '{"decision":"allow"}' && exit 0
|
|
136
|
-
[[ "$PROJECT_NAME" == "_archive" ]] && echo '{"decision":"allow"}' && exit 0
|
|
137
|
-
|
|
138
|
-
# Check for template placeholders - WARN only (v1.0.37)
|
|
139
|
-
if [[ "$PROJECT_NAME" =~ \{\{.*\}\} ]]; then
|
|
140
|
-
printf '{"decision":"allow","message":"⚠️ UNRESOLVED PLACEHOLDER: %s\\n\\n🔧 FIX: Replace {{...}} with actual project name\\n\\nOperation ALLOWED - proceeding anyway."}\n' "$PROJECT_NAME"
|
|
141
|
-
exit 0
|
|
142
|
-
fi
|
|
143
|
-
|
|
144
|
-
# Check for comma-separated (invalid) - WARN only (v1.0.37)
|
|
145
|
-
if [[ "$PROJECT_NAME" =~ , ]]; then
|
|
146
|
-
printf '{"decision":"allow","message":"⚠️ COMMA-SEPARATED PROJECTS: %s\\n\\nEach User Story should have ONE project folder.\\n\\n💡 RECOMMENDATION: Split into separate specs\\n\\nOperation ALLOWED - proceeding anyway."}\n' "$PROJECT_NAME"
|
|
147
|
-
exit 0
|
|
148
|
-
fi
|
|
149
|
-
|
|
150
|
-
# Check for common example/placeholder names - WARN only (v1.0.37)
|
|
151
|
-
EXAMPLE_NAMES="frontend-app|backend-api|mobile-app|shared-lib|acme-corp|my-app|myapp|example-project|test-project"
|
|
152
|
-
if [[ "$PROJECT_NAME" =~ ^($EXAMPLE_NAMES)$ ]]; then
|
|
153
|
-
# Try to get valid projects from config
|
|
154
|
-
PROJECT_ROOT="${HOOK_FILE_PATH%%/.specweave/*}"
|
|
155
|
-
CONFIG_FILE="$PROJECT_ROOT/.specweave/config.json"
|
|
156
|
-
|
|
157
|
-
if [[ -f "$CONFIG_FILE" ]]; then
|
|
158
|
-
# Check if this example name is actually configured
|
|
159
|
-
IS_CONFIGURED=$(jq -r --arg name "$PROJECT_NAME" '.multiProject.projects[$name] // .project.name == $name' "$CONFIG_FILE" 2>/dev/null || echo "false")
|
|
160
|
-
|
|
161
|
-
if [[ "$IS_CONFIGURED" != "true" ]]; then
|
|
162
|
-
VALID_PROJECTS=$(jq -r '.multiProject.projects | keys | join(", ") // .project.name // "specweave"' "$CONFIG_FILE" 2>/dev/null || echo "specweave")
|
|
163
|
-
printf '{"decision":"allow","message":"⚠️ EXAMPLE PROJECT NAME DETECTED: %s\\n\\nThis looks like a placeholder/example name from documentation.\\n\\nConfigured projects: %s\\n\\n💡 RECOMMENDATION: Edit spec.md and use a real project name\\n\\nOperation ALLOWED - proceeding anyway."}\n' "$PROJECT_NAME" "$VALID_PROJECTS"
|
|
164
|
-
exit 0
|
|
165
|
-
fi
|
|
166
|
-
fi
|
|
167
|
-
fi
|
|
168
|
-
|
|
169
|
-
echo '{"decision":"allow"}'
|
|
170
|
-
exit 0
|
|
171
|
-
fi
|
|
10
|
+
# This guard now does NOTHING - just allows all operations.
|
|
172
11
|
|
|
173
|
-
|
|
12
|
+
set +e
|
|
174
13
|
echo '{"decision":"allow"}'
|
|
175
14
|
exit 0
|
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# github-sync-handler.sh - Sync increment
|
|
2
|
+
# github-sync-handler.sh - Sync increment to GitHub (create issues for User Stories)
|
|
3
3
|
# Called async by processor, non-blocking, error-tolerant
|
|
4
4
|
#
|
|
5
|
+
# Argument formats supported:
|
|
6
|
+
# 1. (event_type, increment_id) - from lifecycle/spec.updated events
|
|
7
|
+
# 2. (increment_id) - from metadata.changed events (legacy)
|
|
8
|
+
#
|
|
5
9
|
# IMPORTANT: Never crash Claude, always exit 0
|
|
6
10
|
set +e
|
|
7
11
|
|
|
8
12
|
[[ "${SPECWEAVE_DISABLE_HOOKS:-0}" == "1" ]] && exit 0
|
|
9
13
|
|
|
14
|
+
# Support both argument formats:
|
|
15
|
+
# - Called from increment.created/spec.updated: $1 = event_type, $2 = increment_id
|
|
16
|
+
# - Called from metadata.changed: $1 = increment_id
|
|
10
17
|
INC_ID="${1:-}"
|
|
18
|
+
if [[ "$INC_ID" == increment.* ]] || [[ "$INC_ID" == spec.* ]] || [[ "$INC_ID" == metadata.* ]]; then
|
|
19
|
+
# First arg is event type, second is increment ID
|
|
20
|
+
INC_ID="${2:-}"
|
|
21
|
+
fi
|
|
11
22
|
[[ -z "$INC_ID" ]] && exit 0
|
|
12
23
|
|
|
13
24
|
# Find project root
|
|
@@ -5,14 +5,17 @@
|
|
|
5
5
|
# Usage: processor.sh [--daemon]
|
|
6
6
|
#
|
|
7
7
|
# Event routing (EDA v2):
|
|
8
|
-
# - increment.created/done/archived/reopened -> living-specs-handler + status-line-handler + project-bridge-handler
|
|
8
|
+
# - increment.created/done/archived/reopened -> living-specs-handler + status-line-handler + project-bridge-handler + github-sync-handler
|
|
9
9
|
# - user-story.completed/reopened -> status-line-handler + project-bridge-handler
|
|
10
|
-
# -
|
|
10
|
+
# - spec.updated -> living-docs-handler + ac-validation-handler + github-sync-handler (creates GitHub issues for User Stories)
|
|
11
|
+
# - task.updated -> living-docs-handler + ac-validation-handler (legacy)
|
|
11
12
|
# - metadata.changed -> github-sync-handler
|
|
12
13
|
#
|
|
13
14
|
# The project-bridge-handler connects increment events to project-level EDA,
|
|
14
15
|
# enabling automatic sync to GitHub, ADO, and JIRA via ProjectService.
|
|
15
16
|
#
|
|
17
|
+
# The github-sync-handler creates GitHub issues for User Stories when spec.md is updated.
|
|
18
|
+
#
|
|
16
19
|
# Self-terminates after 60s of idle
|
|
17
20
|
#
|
|
18
21
|
# IMPORTANT: Uses cross-platform locking (mkdir is atomic on all POSIX systems)
|
|
@@ -152,13 +155,16 @@ process_event() {
|
|
|
152
155
|
# EDA Event Routing (new architecture)
|
|
153
156
|
# ========================================
|
|
154
157
|
|
|
155
|
-
# Lifecycle events -> living-specs
|
|
158
|
+
# Lifecycle events -> living-specs + status-line + project-bridge + github-sync
|
|
159
|
+
# Note: github-sync-handler added to ensure GitHub issues are created for User Stories
|
|
156
160
|
increment.created|increment.done|increment.archived|increment.reopened)
|
|
157
161
|
run_handler "$HANDLER_DIR/living-specs-handler.sh" "$EVENT_TYPE" "$EVENT_DATA"
|
|
158
162
|
# Also update status line on lifecycle changes
|
|
159
163
|
run_handler "$HANDLER_DIR/status-line-handler.sh" "$EVENT_TYPE" "$EVENT_DATA"
|
|
160
164
|
# Bridge to project-level EDA (triggers sync to GitHub/ADO/JIRA)
|
|
161
165
|
run_handler "$HANDLER_DIR/project-bridge-handler.sh" "$EVENT_TYPE" "$EVENT_DATA"
|
|
166
|
+
# Sync to GitHub (creates issues for User Stories)
|
|
167
|
+
run_handler "$HANDLER_DIR/github-sync-handler.sh" "$EVENT_TYPE" "$EVENT_DATA"
|
|
162
168
|
;;
|
|
163
169
|
|
|
164
170
|
# User story events -> status-line-handler + project-bridge-handler
|
|
@@ -168,10 +174,18 @@ process_event() {
|
|
|
168
174
|
run_handler "$HANDLER_DIR/project-bridge-handler.sh" "$EVENT_TYPE" "$EVENT_DATA"
|
|
169
175
|
;;
|
|
170
176
|
|
|
171
|
-
#
|
|
172
|
-
#
|
|
173
|
-
#
|
|
174
|
-
|
|
177
|
+
# Spec updated -> sync to living docs + validate ACs + sync to GitHub
|
|
178
|
+
# CRITICAL: spec.updated fires AFTER spec.md has User Stories defined
|
|
179
|
+
# This is the key event that triggers GitHub issue creation for USs
|
|
180
|
+
spec.updated)
|
|
181
|
+
run_handler "$HANDLER_DIR/living-docs-handler.sh" "" "$EVENT_DATA"
|
|
182
|
+
run_handler "$HANDLER_DIR/ac-validation-handler.sh" "" "$EVENT_DATA"
|
|
183
|
+
# Sync to GitHub (creates issues for User Stories when spec has USs)
|
|
184
|
+
run_handler "$HANDLER_DIR/github-sync-handler.sh" "$EVENT_TYPE" "$EVENT_DATA"
|
|
185
|
+
;;
|
|
186
|
+
|
|
187
|
+
# Legacy task.updated event (backward compat)
|
|
188
|
+
task.updated)
|
|
175
189
|
# Legacy: don't update status line on every task edit
|
|
176
190
|
# That causes race conditions and flickering
|
|
177
191
|
run_handler "$HANDLER_DIR/living-docs-handler.sh" "" "$EVENT_DATA"
|
|
@@ -1,302 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Comprehensive test suite for metadata-json-guard.sh
|
|
3
|
-
# Tests that spec.md creation is blocked when metadata.json is missing
|
|
4
|
-
#
|
|
5
|
-
# Usage: bash metadata-json-guard.test.sh
|
|
6
|
-
#
|
|
7
|
-
# v0.34.0 - Initial test suite
|
|
8
|
-
|
|
9
|
-
set -e
|
|
10
|
-
|
|
11
|
-
GUARD="$(dirname "$0")/metadata-json-guard.sh"
|
|
12
|
-
TEST_DIR=$(mktemp -d)
|
|
13
|
-
PASS=0
|
|
14
|
-
FAIL=0
|
|
15
|
-
TOTAL=0
|
|
16
|
-
|
|
17
|
-
# Colors for output
|
|
18
|
-
RED='\033[0;31m'
|
|
19
|
-
GREEN='\033[0;32m'
|
|
20
|
-
YELLOW='\033[1;33m'
|
|
21
|
-
NC='\033[0m' # No Color
|
|
22
|
-
|
|
23
|
-
cleanup() {
|
|
24
|
-
rm -rf "$TEST_DIR"
|
|
25
|
-
}
|
|
26
|
-
trap cleanup EXIT
|
|
27
|
-
|
|
28
|
-
# Create test increment structure
|
|
29
|
-
setup_test_increment() {
|
|
30
|
-
local with_metadata="$1"
|
|
31
|
-
local increment_name="${2:-0001-test-feature}"
|
|
32
|
-
|
|
33
|
-
mkdir -p "$TEST_DIR/.specweave/increments/$increment_name"
|
|
34
|
-
|
|
35
|
-
if [ "$with_metadata" = "true" ]; then
|
|
36
|
-
cat > "$TEST_DIR/.specweave/increments/$increment_name/metadata.json" << 'EOF'
|
|
37
|
-
{
|
|
38
|
-
"id": "0001-test-feature",
|
|
39
|
-
"status": "planned",
|
|
40
|
-
"type": "feature",
|
|
41
|
-
"priority": "P1",
|
|
42
|
-
"created": "2025-12-10T00:00:00Z",
|
|
43
|
-
"lastActivity": "2025-12-10T00:00:00Z",
|
|
44
|
-
"testMode": "TDD",
|
|
45
|
-
"coverageTarget": 95,
|
|
46
|
-
"feature_id": null,
|
|
47
|
-
"epic_id": null,
|
|
48
|
-
"externalLinks": {}
|
|
49
|
-
}
|
|
50
|
-
EOF
|
|
51
|
-
fi
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
# Test helper: should block
|
|
55
|
-
test_should_block() {
|
|
56
|
-
local name="$1"
|
|
57
|
-
local file_path="$2"
|
|
58
|
-
local content="$3"
|
|
59
|
-
TOTAL=$((TOTAL + 1))
|
|
60
|
-
|
|
61
|
-
# Build JSON input for the guard
|
|
62
|
-
local json_input
|
|
63
|
-
json_input=$(jq -n \
|
|
64
|
-
--arg tool_name "Write" \
|
|
65
|
-
--arg file_path "$file_path" \
|
|
66
|
-
--arg content "$content" \
|
|
67
|
-
'{tool_name: $tool_name, tool_input: {file_path: $file_path, content: $content}}')
|
|
68
|
-
|
|
69
|
-
result=$(echo "$json_input" | bash "$GUARD" 2>&1; echo "EXIT:$?")
|
|
70
|
-
exit_code=$(echo "$result" | grep -o 'EXIT:[0-9]*' | cut -d: -f2)
|
|
71
|
-
|
|
72
|
-
if [[ "$exit_code" == "2" ]]; then
|
|
73
|
-
echo -e "${GREEN}✓ BLOCKED${NC}: $name"
|
|
74
|
-
PASS=$((PASS + 1))
|
|
75
|
-
else
|
|
76
|
-
echo -e "${RED}✗ NOT BLOCKED${NC}: $name"
|
|
77
|
-
echo " File: $file_path"
|
|
78
|
-
echo " Exit code: $exit_code (expected 2)"
|
|
79
|
-
echo " Output: $(echo "$result" | head -5)"
|
|
80
|
-
FAIL=$((FAIL + 1))
|
|
81
|
-
fi
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
# Test helper: should allow
|
|
85
|
-
test_should_allow() {
|
|
86
|
-
local name="$1"
|
|
87
|
-
local file_path="$2"
|
|
88
|
-
local content="$3"
|
|
89
|
-
TOTAL=$((TOTAL + 1))
|
|
90
|
-
|
|
91
|
-
# Build JSON input for the guard
|
|
92
|
-
local json_input
|
|
93
|
-
json_input=$(jq -n \
|
|
94
|
-
--arg tool_name "Write" \
|
|
95
|
-
--arg file_path "$file_path" \
|
|
96
|
-
--arg content "$content" \
|
|
97
|
-
'{tool_name: $tool_name, tool_input: {file_path: $file_path, content: $content}}')
|
|
98
|
-
|
|
99
|
-
result=$(echo "$json_input" | bash "$GUARD" 2>&1; echo "EXIT:$?")
|
|
100
|
-
exit_code=$(echo "$result" | grep -o 'EXIT:[0-9]*' | cut -d: -f2)
|
|
101
|
-
|
|
102
|
-
if [[ "$exit_code" == "0" ]]; then
|
|
103
|
-
echo -e "${GREEN}✓ ALLOWED${NC}: $name"
|
|
104
|
-
PASS=$((PASS + 1))
|
|
105
|
-
else
|
|
106
|
-
echo -e "${RED}✗ WRONGLY BLOCKED${NC}: $name"
|
|
107
|
-
echo " File: $file_path"
|
|
108
|
-
echo " Exit code: $exit_code (expected 0)"
|
|
109
|
-
FAIL=$((FAIL + 1))
|
|
110
|
-
fi
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
# Test non-Write tools (should always allow)
|
|
114
|
-
test_non_write_tool() {
|
|
115
|
-
local name="$1"
|
|
116
|
-
local tool_name="$2"
|
|
117
|
-
TOTAL=$((TOTAL + 1))
|
|
118
|
-
|
|
119
|
-
local json_input
|
|
120
|
-
json_input=$(jq -n \
|
|
121
|
-
--arg tool_name "$tool_name" \
|
|
122
|
-
--arg file_path "$TEST_DIR/.specweave/increments/0001-test/spec.md" \
|
|
123
|
-
'{tool_name: $tool_name, tool_input: {file_path: $file_path}}')
|
|
124
|
-
|
|
125
|
-
result=$(echo "$json_input" | bash "$GUARD" 2>&1; echo "EXIT:$?")
|
|
126
|
-
exit_code=$(echo "$result" | grep -o 'EXIT:[0-9]*' | cut -d: -f2)
|
|
127
|
-
|
|
128
|
-
if [[ "$exit_code" == "0" ]]; then
|
|
129
|
-
echo -e "${GREEN}✓ ALLOWED (non-Write)${NC}: $name"
|
|
130
|
-
PASS=$((PASS + 1))
|
|
131
|
-
else
|
|
132
|
-
echo -e "${RED}✗ WRONGLY BLOCKED${NC}: $name"
|
|
133
|
-
FAIL=$((FAIL + 1))
|
|
134
|
-
fi
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
echo "========================================"
|
|
138
|
-
echo " METADATA JSON GUARD - COMPREHENSIVE TESTS"
|
|
139
|
-
echo "========================================"
|
|
140
|
-
echo ""
|
|
141
|
-
echo "Test directory: $TEST_DIR"
|
|
142
|
-
echo ""
|
|
143
|
-
|
|
144
|
-
echo -e "${YELLOW}=== CORE FUNCTIONALITY: BLOCK WHEN METADATA MISSING ===${NC}"
|
|
145
|
-
|
|
146
|
-
# Setup without metadata
|
|
147
|
-
setup_test_increment "false" "0001-no-metadata"
|
|
148
|
-
test_should_block "spec.md without metadata.json (3-digit)" \
|
|
149
|
-
"$TEST_DIR/.specweave/increments/0001-no-metadata/spec.md" \
|
|
150
|
-
"---\nincrement: 0001-no-metadata\n---"
|
|
151
|
-
|
|
152
|
-
setup_test_increment "false" "0012-no-metadata"
|
|
153
|
-
test_should_block "spec.md without metadata.json (4-digit)" \
|
|
154
|
-
"$TEST_DIR/.specweave/increments/0012-no-metadata/spec.md" \
|
|
155
|
-
"---\nincrement: 0012-no-metadata\n---"
|
|
156
|
-
|
|
157
|
-
setup_test_increment "false" "123-short"
|
|
158
|
-
test_should_block "spec.md without metadata.json (3-digit short)" \
|
|
159
|
-
"$TEST_DIR/.specweave/increments/123-short/spec.md" \
|
|
160
|
-
"---\nincrement: 123-short\n---"
|
|
161
|
-
|
|
162
|
-
setup_test_increment "false" "0001E-external"
|
|
163
|
-
test_should_block "spec.md without metadata.json (external E suffix)" \
|
|
164
|
-
"$TEST_DIR/.specweave/increments/0001E-external/spec.md" \
|
|
165
|
-
"---\nincrement: 0001E-external\n---"
|
|
166
|
-
|
|
167
|
-
echo ""
|
|
168
|
-
echo -e "${YELLOW}=== CORE FUNCTIONALITY: ALLOW WHEN METADATA EXISTS ===${NC}"
|
|
169
|
-
|
|
170
|
-
# Setup with metadata
|
|
171
|
-
setup_test_increment "true" "0002-has-metadata"
|
|
172
|
-
test_should_allow "spec.md WITH metadata.json present" \
|
|
173
|
-
"$TEST_DIR/.specweave/increments/0002-has-metadata/spec.md" \
|
|
174
|
-
"---\nincrement: 0002-has-metadata\n---"
|
|
175
|
-
|
|
176
|
-
setup_test_increment "true" "0123E-external-with-meta"
|
|
177
|
-
test_should_allow "spec.md WITH metadata.json (external E suffix)" \
|
|
178
|
-
"$TEST_DIR/.specweave/increments/0123E-external-with-meta/spec.md" \
|
|
179
|
-
"---\nincrement: 0123E-external-with-meta\n---"
|
|
180
|
-
|
|
181
|
-
echo ""
|
|
182
|
-
echo -e "${YELLOW}=== NON-SPEC FILES: SHOULD NOT VALIDATE ===${NC}"
|
|
183
|
-
|
|
184
|
-
# Create increment without metadata for these tests
|
|
185
|
-
setup_test_increment "false" "0003-other-files"
|
|
186
|
-
test_should_allow "plan.md (not spec.md)" \
|
|
187
|
-
"$TEST_DIR/.specweave/increments/0003-other-files/plan.md" \
|
|
188
|
-
"# Plan"
|
|
189
|
-
|
|
190
|
-
test_should_allow "tasks.md (not spec.md)" \
|
|
191
|
-
"$TEST_DIR/.specweave/increments/0003-other-files/tasks.md" \
|
|
192
|
-
"# Tasks"
|
|
193
|
-
|
|
194
|
-
test_should_allow "metadata.json itself" \
|
|
195
|
-
"$TEST_DIR/.specweave/increments/0003-other-files/metadata.json" \
|
|
196
|
-
'{"id": "test"}'
|
|
197
|
-
|
|
198
|
-
test_should_allow "report in subfolder" \
|
|
199
|
-
"$TEST_DIR/.specweave/increments/0003-other-files/reports/COMPLETION.md" \
|
|
200
|
-
"# Report"
|
|
201
|
-
|
|
202
|
-
echo ""
|
|
203
|
-
echo -e "${YELLOW}=== FILES OUTSIDE INCREMENTS: SHOULD NOT VALIDATE ===${NC}"
|
|
204
|
-
|
|
205
|
-
test_should_allow "spec.md outside increments (docs)" \
|
|
206
|
-
"$TEST_DIR/.specweave/docs/internal/spec.md" \
|
|
207
|
-
"# Documentation"
|
|
208
|
-
|
|
209
|
-
test_should_allow "spec.md in root" \
|
|
210
|
-
"$TEST_DIR/spec.md" \
|
|
211
|
-
"# Root spec"
|
|
212
|
-
|
|
213
|
-
test_should_allow "Random file" \
|
|
214
|
-
"$TEST_DIR/random.txt" \
|
|
215
|
-
"Random content"
|
|
216
|
-
|
|
217
|
-
echo ""
|
|
218
|
-
echo -e "${YELLOW}=== NON-WRITE TOOLS: SHOULD ALWAYS ALLOW ===${NC}"
|
|
219
|
-
|
|
220
|
-
test_non_write_tool "Read tool" "Read"
|
|
221
|
-
test_non_write_tool "Edit tool" "Edit"
|
|
222
|
-
test_non_write_tool "Glob tool" "Glob"
|
|
223
|
-
test_non_write_tool "Grep tool" "Grep"
|
|
224
|
-
test_non_write_tool "Bash tool" "Bash"
|
|
225
|
-
|
|
226
|
-
echo ""
|
|
227
|
-
echo -e "${YELLOW}=== BYPASS MODES ===${NC}"
|
|
228
|
-
|
|
229
|
-
# Test SPECWEAVE_FORCE_METADATA bypass
|
|
230
|
-
setup_test_increment "false" "0004-bypass"
|
|
231
|
-
TOTAL=$((TOTAL + 1))
|
|
232
|
-
json_input=$(jq -n \
|
|
233
|
-
--arg tool_name "Write" \
|
|
234
|
-
--arg file_path "$TEST_DIR/.specweave/increments/0004-bypass/spec.md" \
|
|
235
|
-
--arg content "---\nincrement: 0004-bypass\n---" \
|
|
236
|
-
'{tool_name: $tool_name, tool_input: {file_path: $file_path, content: $content}}')
|
|
237
|
-
|
|
238
|
-
result=$(SPECWEAVE_FORCE_METADATA=1 bash -c "echo '$json_input' | bash '$GUARD'" 2>&1; echo "EXIT:$?")
|
|
239
|
-
exit_code=$(echo "$result" | grep -o 'EXIT:[0-9]*' | cut -d: -f2)
|
|
240
|
-
if [[ "$exit_code" == "0" ]] && echo "$result" | grep -q "bypassed"; then
|
|
241
|
-
echo -e "${GREEN}✓ ALLOWED (bypass)${NC}: SPECWEAVE_FORCE_METADATA=1 works"
|
|
242
|
-
PASS=$((PASS + 1))
|
|
243
|
-
else
|
|
244
|
-
echo -e "${RED}✗ BYPASS FAILED${NC}: SPECWEAVE_FORCE_METADATA=1"
|
|
245
|
-
echo " Exit code: $exit_code"
|
|
246
|
-
FAIL=$((FAIL + 1))
|
|
247
|
-
fi
|
|
248
|
-
|
|
249
|
-
# Test SPECWEAVE_DISABLE_HOOKS bypass
|
|
250
|
-
TOTAL=$((TOTAL + 1))
|
|
251
|
-
result=$(SPECWEAVE_DISABLE_HOOKS=1 bash -c "echo '$json_input' | bash '$GUARD'" 2>&1; echo "EXIT:$?")
|
|
252
|
-
exit_code=$(echo "$result" | grep -o 'EXIT:[0-9]*' | cut -d: -f2)
|
|
253
|
-
if [[ "$exit_code" == "0" ]]; then
|
|
254
|
-
echo -e "${GREEN}✓ ALLOWED (bypass)${NC}: SPECWEAVE_DISABLE_HOOKS=1 works"
|
|
255
|
-
PASS=$((PASS + 1))
|
|
256
|
-
else
|
|
257
|
-
echo -e "${RED}✗ BYPASS FAILED${NC}: SPECWEAVE_DISABLE_HOOKS=1"
|
|
258
|
-
FAIL=$((FAIL + 1))
|
|
259
|
-
fi
|
|
260
|
-
|
|
261
|
-
echo ""
|
|
262
|
-
echo -e "${YELLOW}=== EDGE CASES ===${NC}"
|
|
263
|
-
|
|
264
|
-
# Test with empty file path
|
|
265
|
-
TOTAL=$((TOTAL + 1))
|
|
266
|
-
json_input='{"tool_name": "Write", "tool_input": {"file_path": "", "content": "test"}}'
|
|
267
|
-
result=$(echo "$json_input" | bash "$GUARD" 2>&1; echo "EXIT:$?")
|
|
268
|
-
exit_code=$(echo "$result" | grep -o 'EXIT:[0-9]*' | cut -d: -f2)
|
|
269
|
-
if [[ "$exit_code" == "0" ]]; then
|
|
270
|
-
echo -e "${GREEN}✓ ALLOWED${NC}: Empty file path"
|
|
271
|
-
PASS=$((PASS + 1))
|
|
272
|
-
else
|
|
273
|
-
echo -e "${RED}✗ FAILED${NC}: Empty file path should be allowed"
|
|
274
|
-
FAIL=$((FAIL + 1))
|
|
275
|
-
fi
|
|
276
|
-
|
|
277
|
-
# Test with malformed JSON (should fail gracefully)
|
|
278
|
-
TOTAL=$((TOTAL + 1))
|
|
279
|
-
result=$(echo "not json" | bash "$GUARD" 2>&1; echo "EXIT:$?")
|
|
280
|
-
exit_code=$(echo "$result" | grep -o 'EXIT:[0-9]*' | cut -d: -f2)
|
|
281
|
-
if [[ "$exit_code" == "0" ]]; then
|
|
282
|
-
echo -e "${GREEN}✓ ALLOWED${NC}: Malformed JSON handled gracefully"
|
|
283
|
-
PASS=$((PASS + 1))
|
|
284
|
-
else
|
|
285
|
-
echo -e "${RED}✗ FAILED${NC}: Malformed JSON should be allowed (fail-safe)"
|
|
286
|
-
FAIL=$((FAIL + 1))
|
|
287
|
-
fi
|
|
288
|
-
|
|
289
|
-
echo ""
|
|
290
|
-
echo "========================================"
|
|
291
|
-
echo " RESULTS"
|
|
292
|
-
echo "========================================"
|
|
293
|
-
echo -e "Total: $TOTAL"
|
|
294
|
-
echo -e "${GREEN}Passed: $PASS${NC}"
|
|
295
|
-
if [[ $FAIL -gt 0 ]]; then
|
|
296
|
-
echo -e "${RED}Failed: $FAIL${NC}"
|
|
297
|
-
exit 1
|
|
298
|
-
else
|
|
299
|
-
echo -e "Failed: 0"
|
|
300
|
-
echo ""
|
|
301
|
-
echo -e "${GREEN}ALL TESTS PASSED!${NC}"
|
|
302
|
-
fi
|