@boyingliu01/xp-gate 0.12.10 → 0.12.13

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.
Files changed (44) hide show
  1. package/adapters/iac.sh +51 -24
  2. package/adapters/typescript.sh +87 -10
  3. package/bin/xp-gate.js +2 -2
  4. package/gate-3.sh +1 -1
  5. package/hooks/adapter-common.sh +13 -18
  6. package/hooks/pre-commit +166 -27
  7. package/hooks/pre-push +231 -18
  8. package/lib/__tests__/sprint-discovery.test.ts +5 -4
  9. package/lib/__tests__/sprint-status.test.ts +5 -4
  10. package/lib/__tests__/tsconfig.json +8 -0
  11. package/lib/init.js +97 -3
  12. package/mock-policy/AGENTS.md +3 -3
  13. package/mutation/AGENTS.md +3 -3
  14. package/package.json +1 -1
  15. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  16. package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
  17. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
  18. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
  19. package/plugins/opencode/package.json +1 -1
  20. package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
  21. package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
  22. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
  23. package/plugins/qoder/plugin.json +1 -1
  24. package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
  25. package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
  26. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
  27. package/principles/AGENTS.md +3 -3
  28. package/principles/__tests__/baseline-storage.test.ts +3 -2
  29. package/principles/__tests__/baseline.test.ts +16 -14
  30. package/principles/__tests__/types.test.ts +1 -0
  31. package/principles/adapters/__tests__/base.test.ts +8 -8
  32. package/principles/adapters/__tests__/cpp.test.ts +26 -26
  33. package/principles/adapters/__tests__/dart.test.ts +11 -11
  34. package/principles/adapters/__tests__/go.test.ts +9 -9
  35. package/principles/adapters/__tests__/java.test.ts +9 -9
  36. package/principles/adapters/__tests__/kotlin.test.ts +10 -10
  37. package/principles/adapters/__tests__/objectivec.test.ts +27 -27
  38. package/principles/adapters/__tests__/python.test.ts +11 -11
  39. package/principles/adapters/__tests__/swift.test.ts +9 -9
  40. package/principles/adapters/__tests__/typescript.test.ts +13 -13
  41. package/principles/rules/__tests__/clean-code/large-file.test.ts +4 -3
  42. package/skills/delphi-review/AGENTS.md +3 -3
  43. package/skills/sprint-flow/AGENTS.md +3 -3
  44. package/skills/test-specification-alignment/AGENTS.md +3 -3
package/adapters/iac.sh CHANGED
@@ -20,8 +20,12 @@ detect_iac_files() {
20
20
  tf_files="$tf_files $file"
21
21
  ;;
22
22
  *.yaml|*.yml)
23
+ # Check if it's inside .github/workflows/ (GitHub Actions)
24
+ if echo "$file" | grep -q "\.github/workflows/"; then
25
+ # Pass directly to checkov as a GitHub Actions file
26
+ yaml_files="$yaml_files $file"
23
27
  # Check if it's a K8s manifest (has apiVersion and kind)
24
- if grep -qE "^(apiVersion|kind):" "$file" 2>/dev/null; then
28
+ elif grep -qE "^(apiVersion|kind):" "$file" 2>/dev/null; then
25
29
  yaml_files="$yaml_files $file"
26
30
  fi
27
31
  ;;
@@ -59,52 +63,75 @@ run_static_analysis() {
59
63
  # Try checkov first (recommended - supports multiple platforms)
60
64
  if command -v checkov >/dev/null 2>&1; then
61
65
  echo "Running checkov IaC security scan..."
62
-
66
+
67
+ # checkov 3.x always exits 0 even on failures. We capture output to a
68
+ # temp file and parse it for "FAILED for resource" lines instead.
69
+ local checkov_log=$(mktemp)
70
+ local checkov_args="--compact"
71
+
63
72
  # Run checkov on all detected IaC files
64
73
  local tf_files=$(echo "$iac_files" | grep "TERRAFORM:" | sed 's/TERRAFORM://')
65
74
  local k8s_files=$(echo "$iac_files" | grep "KUBERNETES:" | sed 's/KUBERNETES://')
66
75
  local docker_files=$(echo "$iac_files" | grep "DOCKER:" | sed 's/DOCKER://')
67
-
76
+
68
77
  if [ -n "$tf_files" ]; then
69
78
  echo "Scanning Terraform files..."
70
79
  for file in $tf_files; do
71
80
  if [ -f "$file" ]; then
72
- checkov --file "$file" --compact 2>&1 | tail -20
73
- local tf_exit=${PIPESTATUS[0]}
74
- if [ $tf_exit -ne 0 ]; then
75
- exit_code=$tf_exit
76
- fi
81
+ checkov --file "$file" $checkov_args >> "$checkov_log" 2>&1
77
82
  fi
78
83
  done
79
84
  fi
80
-
85
+
81
86
  if [ -n "$k8s_files" ]; then
82
- echo "Scanning Kubernetes manifests..."
87
+ local gh_files=""
88
+ local k8s_only=""
83
89
  for file in $k8s_files; do
84
- if [ -f "$file" ]; then
85
- checkov --file "$file" --compact 2>&1 | tail -20
86
- local k8s_exit=${PIPESTATUS[0]}
87
- if [ $k8s_exit -ne 0 ]; then
88
- exit_code=$k8s_exit
89
- fi
90
+ if echo "$file" | grep -q "\.github/workflows/"; then
91
+ gh_files="$gh_files $file"
92
+ else
93
+ k8s_only="$k8s_only $file"
90
94
  fi
91
95
  done
96
+
97
+ if [ -n "$k8s_only" ]; then
98
+ echo "Scanning Kubernetes manifests..."
99
+ for file in $k8s_only; do
100
+ if [ -f "$file" ]; then
101
+ checkov --file "$file" $checkov_args --framework kubernetes >> "$checkov_log" 2>&1
102
+ fi
103
+ done
104
+ fi
105
+
106
+ if [ -n "$gh_files" ]; then
107
+ echo "Scanning GitHub Actions workflows..."
108
+ for file in $gh_files; do
109
+ if [ -f "$file" ]; then
110
+ checkov --file "$file" $checkov_args --framework github_actions >> "$checkov_log" 2>&1
111
+ fi
112
+ done
113
+ fi
92
114
  fi
93
-
115
+
94
116
  if [ -n "$docker_files" ]; then
95
117
  echo "Scanning Dockerfiles..."
96
118
  for file in $docker_files; do
97
119
  if [ -f "$file" ]; then
98
- checkov --file "$file" --compact 2>&1 | tail -20
99
- local docker_exit=${PIPESTATUS[0]}
100
- if [ $docker_exit -ne 0 ]; then
101
- exit_code=$docker_exit
102
- fi
120
+ checkov --file "$file" $checkov_args >> "$checkov_log" 2>&1
103
121
  fi
104
122
  done
105
123
  fi
106
-
107
- echo "checkov scan completed with exit code: $exit_code"
124
+
125
+ # Print captured output then check for failures
126
+ cat "$checkov_log"
127
+ local failed_count=$(grep -c "FAILED for resource" "$checkov_log" 2>/dev/null || echo 0)
128
+ rm -f "$checkov_log"
129
+
130
+ if [ "$failed_count" -gt 0 ]; then
131
+ echo "❌ $failed_count security issue(s) found by checkov. Blocking commit."
132
+ exit_code=1
133
+ fi
134
+ echo "checkov scan completed."
108
135
  return $exit_code
109
136
  fi
110
137
 
@@ -3,25 +3,102 @@
3
3
  # TypeScript adapter for quality gates
4
4
 
5
5
  run_static_analysis() {
6
- if command -v npx >/dev/null 2>&1; then
7
- echo "Running TypeScript static analysis..."
8
- npx tsc --noEmit
9
- return $?
10
- else
6
+ if ! command -v npx >/dev/null 2>&1; then
11
7
  echo "npx not available, skipping TypeScript static analysis"
12
8
  return 0
13
9
  fi
10
+
11
+ local HAS_ERRORS=0
12
+ local TSC_VERSION
13
+
14
+ echo "Running TypeScript static analysis..."
15
+ npx tsc --noEmit
16
+ TSC_EXIT=$?
17
+ if [ $TSC_EXIT -ne 0 ]; then
18
+ HAS_ERRORS=1
19
+ fi
20
+
21
+ # Also check test files if tsconfig.json exists (see Issue #293).
22
+ # Many projects exclude __tests__/ from tsconfig.json, so test file type
23
+ # errors silently accumulate. We detect this and run a second tsc pass.
24
+ local HAS_TESTS=false
25
+ if [ -d "src/__tests__" ] || [ -d "src/tests" ] || [ -d "tests" ] || [ -d "__tests__" ]; then
26
+ HAS_TESTS=true
27
+ fi
28
+
29
+ if [ "$HAS_TESTS" = true ] && [ -f "tsconfig.json" ]; then
30
+ # Check if tsconfig.json already includes test files
31
+ if npx tsc --noEmit --listFiles 2>/dev/null | grep -qE '__tests__/|\.test\.ts|\.spec\.ts'; then
32
+ : # Test files already checked — no additional pass needed
33
+ elif [ -f "tsconfig.tests.json" ]; then
34
+ echo "Checking test files with tsconfig.tests.json..."
35
+ npx tsc --noEmit --project tsconfig.tests.json
36
+ TSC_TEST_EXIT=$?
37
+ if [ $TSC_TEST_EXIT -ne 0 ]; then
38
+ HAS_ERRORS=1
39
+ fi
40
+ else
41
+ # Generate a temp tsconfig that includes test files for the additional pass
42
+ local TSC_TEST_EXIT=0
43
+ echo "Checking test files for type errors (generating temp tsconfig)..."
44
+ npx tsc --noEmit 2>/dev/null
45
+ # Create temp tsconfig extending the original but with test files included
46
+ local TEMP_TSCONFIG=".tsconfig.withtests.json"
47
+ if [ ! -f "$TEMP_TSCONFIG" ]; then
48
+ node -e "
49
+ const cfg = JSON.parse(require('fs').readFileSync('tsconfig.json','utf8'));
50
+ delete cfg.exclude;
51
+ cfg.include = cfg.include || ['src/**/*'];
52
+ cfg.include.push('src/**/__tests__/**','src/**/*.test.ts','src/**/*.spec.ts');
53
+ require('fs').writeFileSync('$TEMP_TSCONFIG', JSON.stringify(cfg, null, 2));
54
+ " 2>/dev/null
55
+ npx tsc --noEmit --project "$TEMP_TSCONFIG"
56
+ TSC_TEST_EXIT=$?
57
+ rm -f "$TEMP_TSCONFIG"
58
+ fi
59
+ if [ $TSC_TEST_EXIT -ne 0 ]; then
60
+ echo ""
61
+ echo "❌ BLOCKED - TYPE ERRORS in test files"
62
+ echo "Your project's tsconfig.json excludes test files from type checking."
63
+ echo "Fix the type errors above or add a tsconfig.tests.json to customise test file checking."
64
+ HAS_ERRORS=1
65
+ fi
66
+ fi
67
+ fi
68
+
69
+ return $HAS_ERRORS
14
70
  }
15
71
 
16
72
  run_lint() {
17
- if command -v npx >/dev/null 2>&1; then
18
- echo "Running TypeScript linting..."
19
- npx eslint . --ext .ts,.tsx
20
- return $?
21
- else
73
+ if ! command -v npx >/dev/null 2>&1; then
22
74
  echo "npx not available, skipping TypeScript linting"
23
75
  return 0
24
76
  fi
77
+
78
+ # Prefer Biome if biome.json/biome.jsonc exists (covers lint + format in one pass)
79
+ if [ -f "biome.json" ] || [ -f "biome.jsonc" ]; then
80
+ echo "Running Biome linting..."
81
+ npx biome check . --no-errors-on-unmatched 2>&1 | head -50
82
+ local EXIT_CODE=$?
83
+ if [ $EXIT_CODE -ne 0 ]; then
84
+ echo ""
85
+ echo "❌ Biome check failed"
86
+ echo "Run 'npx biome check --write .' to auto-fix."
87
+ return $EXIT_CODE
88
+ fi
89
+ echo "✅ PASSED - Biome check."
90
+ return 0
91
+ fi
92
+
93
+ # Fall back to ESLint if no Biome config exists
94
+ if [ -f ".eslintrc.json" ] || [ -f ".eslintrc.js" ] || [ -f ".eslintrc.cjs" ] || [ -f "eslint.config.js" ]; then
95
+ echo "Running ESLint linting..."
96
+ npx eslint . --ext .ts,.tsx
97
+ return $?
98
+ fi
99
+
100
+ echo "ℹ️ No linter config found (biome.json or eslint config). Skipping lint."
101
+ return 0
25
102
  }
26
103
 
27
104
  run_tests() {
package/bin/xp-gate.js CHANGED
@@ -30,7 +30,7 @@ const COMMANDS = {
30
30
  'init': {
31
31
  description: 'Initialize xp-gate (use --global for all projects)',
32
32
  run: subargs => init(subargs).then(code => process.exit(code)),
33
- usage: 'xp-gate init [--global]'
33
+ usage: 'xp-gate init [--global] [--yes]'
34
34
  },
35
35
  'setup-global': {
36
36
  description: 'Set up xp-gate globally for all git projects',
@@ -125,7 +125,7 @@ const COMMANDS = {
125
125
  usage: 'xp-gate upgrade [--preview] [--apply]'
126
126
  },
127
127
  'bootstrap': {
128
- description: 'Install CLI tools required by quality gates (jscpd, lizard, checkov, gitleaks, semgrep)',
128
+ description: 'Install CLI tools required by quality gates. Also invoked by "xp-gate init --yes"',
129
129
  run: subargs => process.exit(bootstrap(subargs)),
130
130
  usage: 'xp-gate bootstrap [--dry-run] [--verbose]'
131
131
  },
package/gate-3.sh CHANGED
@@ -30,7 +30,7 @@ else
30
30
  if [ -n "$LIZARD_CMD" ]; then
31
31
  LIZARD_PATH=$(eval echo "$LIZARD_CMD")
32
32
 
33
- # Get changed source files for complexity check
33
+ # Get changed source files for complexity check (includes test files)
34
34
  CC_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx|py|go|java|swift|cpp|c|hpp|h|m|mm|kt)$' || true)
35
35
 
36
36
  if [ -n "$CC_FILES" ]; then
@@ -192,26 +192,21 @@ detect_iac_project() {
192
192
 
193
193
  # Stryker 9.x config files (new format, takes priority)
194
194
  detect_mutation_testable() {
195
- if [ -f "stryker.config.mjs" ] || [ -f "stryker.config.js" ] || \
196
- [ -f "stryker.config.cjs" ] || [ -f "stryker.config.json" ]; then
197
- if [ -f "package.json" ] && grep -qE '"@stryker-mutator[^"]*"' package.json 2>/dev/null; then
198
- return 0
199
- fi
200
- if command -v npx >/dev/null 2>&1 && npx --no-install stryker --version >/dev/null 2>&1; then
201
- return 0
202
- fi
195
+ # Check if @stryker-mutator/core is installed no config file required.
196
+ # The StrykerRunner passes --mutate via CLI flags, so stryker can run
197
+ # without any config file present.
198
+ if [ -f "package.json" ] && grep -qE '"@stryker-mutator[^"]*"' package.json 2>/dev/null; then
199
+ return 0
203
200
  fi
204
-
205
- # Legacy Stryker config files (backwards compatibility)
206
- if [ -f "stryker.conf.json" ] || [ -f "stryker.prepush.conf.json" ]; then
207
- if [ -f "package.json" ] && grep -qE '"@stryker-mutator[^"]*"' package.json 2>/dev/null; then
208
- return 0
209
- fi
210
- if command -v npx >/dev/null 2>&1 && npx --no-install stryker --version >/dev/null 2>&1; then
211
- return 0
212
- fi
201
+ if command -v npx >/dev/null 2>&1 && npx --no-install stryker --version >/dev/null 2>&1; then
202
+ return 0
203
+ fi
204
+ # Legacy config files (backwards compatibility deprecated)
205
+ if [ -f "stryker.conf.json" ] || [ -f "stryker.prepush.conf.json" ] || \
206
+ [ -f "stryker.config.mjs" ] || [ -f "stryker.config.js" ] || \
207
+ [ -f "stryker.config.cjs" ] || [ -f "stryker.config.json" ]; then
208
+ return 0
213
209
  fi
214
-
215
210
  return 1
216
211
  }
217
212
  # Detect if a Python project has mutmut installed and configured
package/hooks/pre-commit CHANGED
@@ -608,6 +608,44 @@ if [ "$PROJECT_LANG" = "documentation-only" ]; then
608
608
  exit 1
609
609
  fi
610
610
  echo "✅ PASSED - TypeScript static analysis."
611
+
612
+ # Also check test files if they exist (Issue #293)
613
+ # Many projects exclude __tests__/ from tsconfig.json, so test file type
614
+ # errors silently accumulate.
615
+ if [ -d "src/__tests__" ] || [ -d "src/tests" ] || [ -d "tests" ] || [ -d "__tests__" ]; then
616
+ HAS_TESTS_IN_TSC=false
617
+ if npx tsc --noEmit --skipLibCheck --listFiles 2>/dev/null | grep -qE '__tests__/|\.test\.ts|\.spec\.ts'; then
618
+ HAS_TESTS_IN_TSC=true
619
+ fi
620
+ if [ "$HAS_TESTS_IN_TSC" = false ]; then
621
+ TSC_TEST_EXIT=0
622
+ if [ -f "tsconfig.tests.json" ]; then
623
+ echo "Checking test files with tsconfig.tests.json..."
624
+ npx tsc --noEmit --project tsconfig.tests.json 2>&1 | head -30
625
+ TSC_TEST_EXIT=$?
626
+ else
627
+ echo "Checking test files for type errors..."
628
+ TEMP_TSCONFIG=".tsconfig.withtests.json"
629
+ node -e "
630
+ const cfg = JSON.parse(require('fs').readFileSync('tsconfig.json','utf8'));
631
+ delete cfg.exclude;
632
+ cfg.include = cfg.include || ['src/**/*'];
633
+ cfg.include.push('src/**/__tests__/**','src/**/*.test.ts','src/**/*.spec.ts');
634
+ require('fs').writeFileSync('$TEMP_TSCONFIG', JSON.stringify(cfg, null, 2));
635
+ " 2>/dev/null
636
+ npx tsc --noEmit --project "$TEMP_TSCONFIG" --skipLibCheck 2>&1 | head -30
637
+ TSC_TEST_EXIT=$?
638
+ rm -f "$TEMP_TSCONFIG"
639
+ fi
640
+ if [ $TSC_TEST_EXIT -ne 0 ]; then
641
+ echo ""
642
+ echo "❌ BLOCKED - TYPE ERRORS in test files"
643
+ echo "Your project's tsconfig.json excludes test files from type checking."
644
+ echo "Add a tsconfig.tests.json to customise test file checking."
645
+ exit 1
646
+ fi
647
+ fi
648
+ fi
611
649
  else
612
650
  echo "ℹ️ SKIP - tsconfig.json not found (no TypeScript project config)"
613
651
  fi
@@ -679,8 +717,20 @@ if [ "$PROJECT_LANG" = "documentation-only" ]; then
679
717
  fi
680
718
  fi
681
719
 
682
- # ── Biome config detection ──
683
- if ! [ -f "biome.json" ] && ! [ -f "biome.jsonc" ]; then
720
+ # ── Biome lint/format check ──
721
+ if [ -f "biome.json" ] || [ -f "biome.jsonc" ]; then
722
+ echo "Running Biome lint/format check..."
723
+ if ! npx biome check --staged . 2>/dev/null; then
724
+ npx biome check . 2>&1 | head -50
725
+ echo ""
726
+ echo "❌ BLOCKED - Biome check failed"
727
+ echo "Fix the errors above before committing."
728
+ echo "Tip: Run 'npx biome check --write .' to auto-fix formatting and safe lint issues."
729
+ echo " Run 'npx biome check --write --unsafe .' to also fix unsafe lint issues."
730
+ exit 1
731
+ fi
732
+ echo "✅ PASSED - Biome check."
733
+ else
684
734
  if [ -f "package.json" ] && command -v node &> /dev/null; then
685
735
  HAS_BIOME=$(node -e "try{const p=JSON.parse(require('fs').readFileSync('package.json','utf8'));const d=p.devDependencies||{};const deps=p.dependencies||{};console.log((d['@biomejs/biome']||deps['@biomejs/biome'])?'yes':'no')}catch{console.log('no')}") && true
686
736
  if [ "$HAS_BIOME" = "yes" ]; then
@@ -1221,13 +1271,6 @@ else
1221
1271
  fi
1222
1272
  done
1223
1273
 
1224
- if [ "$TEST_FOUND" = false ] && [ "$ext" = "py" ]; then
1225
- module_name="${filename//-/_}"
1226
- if grep -rq "import $module_name\|from $module_name import" tests/ 2>/dev/null; then
1227
- TEST_FOUND=true
1228
- fi
1229
- fi
1230
-
1231
1274
  if [ "$TEST_FOUND" = false ]; then
1232
1275
  echo "⚠️ TEST PAIRING WARNING: Source file without corresponding test: $src_file"
1233
1276
  echo " Expected patterns for .${ext}: ${patterns[0]}, ${patterns[1]:-...}"
@@ -1299,28 +1342,117 @@ else
1299
1342
  done
1300
1343
  fi
1301
1344
 
1302
- # Run tests + coverage in a single pass (optimization: avoids running vitest twice)
1303
- # For TypeScript with test:coverage script, use coverage-enabled run directly.
1304
- # For other languages, fall back to the adapter's run_tests + run_coverage pair.
1345
+ # ============================================================================
1346
+ # Smart test selection (Issue #286): only run tests affected by changed files.
1347
+ # Reuses CHANGED_TEST_FILES already computed in Gate 5b (line 1244).
1348
+ # Full run fallback: >20 changed test files, or vitest not found.
1349
+ # ============================================================================
1305
1350
  echo "Running tests..."
1306
1351
 
1307
1352
  # Route to appropriate test runner via adapter
1308
1353
  ADAPTER_FILE=$(resolve_adapter_path "$PROJECT_LANG")
1309
1354
  if [ "${PROJECT_LANG}" != "unknown" ] && [ -n "$ADAPTER_FILE" ]; then
1310
1355
  if source "$ADAPTER_FILE" 2>/dev/null; then
1311
- # TypeScript: single vitest run --coverage (combines tests + coverage)
1356
+ # TypeScript: vitest with smart test selection
1312
1357
  if [ "$PROJECT_LANG" = "typescript" ] && [ -f "package.json" ]; then
1313
1358
  if npx vitest --version >/dev/null 2>&1; then
1314
- echo "Running TypeScript tests with coverage..."
1315
- TESTS_OUTPUT=$(npx vitest run --coverage 2>&1)
1316
- TESTS_EXIT_CODE=$?
1317
- echo "$TESTS_OUTPUT" | head -60
1318
- if [ "$TESTS_EXIT_CODE" -ne 0 ]; then
1319
- echo ""
1320
- echo "❌ BLOCKED - Tests FAILED"
1321
- exit 1
1359
+ # Count changed test files to decide strategy
1360
+ CHANGED_TEST_FILE_COUNT=$(echo "$CHANGED_TEST_FILES" | grep -c '.' || echo "0")
1361
+
1362
+ # Also get changed source files (non-test) for related-test lookup
1363
+ CHANGED_SRC_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(ts|tsx|js|jsx)$' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '__tests__' | grep -v '\.d\.ts$' | grep -v '__snapshots__' || true)
1364
+ CHANGED_SRC_COUNT=$(echo "$CHANGED_SRC_FILES" | grep -c '.' || echo "0")
1365
+
1366
+ # Decide strategy
1367
+ if [ "$CHANGED_TEST_FILE_COUNT" -gt 20 ] 2>/dev/null; then
1368
+ # Too many test files changed → full run
1369
+ echo "Too many test files changed ($CHANGED_TEST_FILE_COUNT), running full test suite..."
1370
+ TESTS_OUTPUT=$(npx vitest run --coverage 2>&1)
1371
+ TESTS_EXIT_CODE=$?
1372
+ echo "$TESTS_OUTPUT" | head -60
1373
+ if [ "$TESTS_EXIT_CODE" -ne 0 ]; then
1374
+ echo ""
1375
+ echo "❌ BLOCKED - Tests FAILED"
1376
+ exit 1
1377
+ else
1378
+ echo "✅ PASSED - Full test suite passed (with coverage)."
1379
+ fi
1380
+ elif [ "$CHANGED_TEST_FILE_COUNT" -gt 0 ] 2>/dev/null; then
1381
+ # Run only changed test files with coverage
1382
+ SMART_TARGETS=$(echo "$CHANGED_TEST_FILES" | tr '\n' ' ')
1383
+ echo "Running ${CHANGED_TEST_FILE_COUNT} changed test file(s) with coverage..."
1384
+ echo " Target(s): $(echo "$CHANGED_TEST_FILES" | tr '\n' ' ')"
1385
+ TESTS_OUTPUT=$(npx vitest run --coverage $SMART_TARGETS 2>&1)
1386
+ TESTS_EXIT_CODE=$?
1387
+ echo "$TESTS_OUTPUT" | head -60
1388
+ if [ "$TESTS_EXIT_CODE" -ne 0 ]; then
1389
+ echo ""
1390
+ echo "❌ BLOCKED - Tests FAILED in changed test files"
1391
+ exit 1
1392
+ else
1393
+ echo "✅ PASSED - Changed test files passed (with coverage)."
1394
+ fi
1395
+ elif [ "$CHANGED_SRC_COUNT" -gt 0 ] 2>/dev/null; then
1396
+ # Source files changed but no test files → find related tests or run full
1397
+ echo "Source file(s) changed, finding related tests..."
1398
+ RELATED_TESTS=""
1399
+ for src_file in $CHANGED_SRC_FILES; do
1400
+ base="${src_file%.*}"
1401
+ filename="${base##*/}"
1402
+ dir="$(dirname "$src_file")"
1403
+ ext="${src_file##*.}"
1404
+ # Try common test file patterns
1405
+ for pattern in \
1406
+ "${base}.test.${ext}" \
1407
+ "${base}.spec.${ext}" \
1408
+ "${dir}/__tests__/${filename}.test.${ext}" \
1409
+ "${dir}/__tests__/${filename}.spec.${ext}" \
1410
+ "tests/${filename}.test.${ext}" \
1411
+ "tests/${filename}.spec.${ext}"; do
1412
+ if [ -f "$pattern" ]; then
1413
+ RELATED_TESTS="${RELATED_TESTS}${pattern} "
1414
+ fi
1415
+ done
1416
+ # Glob fallback for __tests__ dir patterns
1417
+ if [ -d "${dir}/__tests__" ]; then
1418
+ found_glob=$(find "${dir}/__tests__" -maxdepth 1 -name "${filename}.test.*" -o -name "${filename}.spec.*" 2>/dev/null | tr '\n' ' ')
1419
+ if [ -n "$found_glob" ]; then
1420
+ for f in $found_glob; do
1421
+ case " $RELATED_TESTS " in *" $f "*) ;; *) RELATED_TESTS="${RELATED_TESTS}${f} " ;; esac
1422
+ done
1423
+ fi
1424
+ fi
1425
+ done
1426
+ if [ -n "$RELATED_TESTS" ]; then
1427
+ echo " Found related test(s): $(echo "$RELATED_TESTS" | tr '\n' ' ')"
1428
+ TESTS_OUTPUT=$(npx vitest run --coverage $RELATED_TESTS 2>&1)
1429
+ TESTS_EXIT_CODE=$?
1430
+ echo "$TESTS_OUTPUT" | head -60
1431
+ if [ "$TESTS_EXIT_CODE" -ne 0 ]; then
1432
+ echo ""
1433
+ echo "❌ BLOCKED - Tests FAILED in related test files"
1434
+ exit 1
1435
+ else
1436
+ echo "✅ PASSED - Related test files passed (with coverage)."
1437
+ fi
1438
+ else
1439
+ # No related tests found → full run is safest
1440
+ echo " No related test files found, running full test suite..."
1441
+ TESTS_OUTPUT=$(npx vitest run --coverage 2>&1)
1442
+ TESTS_EXIT_CODE=$?
1443
+ echo "$TESTS_OUTPUT" | head -60
1444
+ if [ "$TESTS_EXIT_CODE" -ne 0 ]; then
1445
+ echo ""
1446
+ echo "❌ BLOCKED - Tests FAILED"
1447
+ exit 1
1448
+ else
1449
+ echo "✅ PASSED - Full test suite passed (with coverage)."
1450
+ fi
1451
+ fi
1322
1452
  else
1323
- echo "✅ PASSED - Unit tests passed (with coverage)."
1453
+ # No test files AND no source files changed → skip
1454
+ echo "✅ PASSED - No test or source files changed, skipping test run."
1455
+ TESTS_EXIT_CODE=0
1324
1456
  fi
1325
1457
  else
1326
1458
  # Fallback: run_tests without coverage
@@ -1360,11 +1492,18 @@ else
1360
1492
  if source "$ADAPTER_FILE" 2>/dev/null; then
1361
1493
  case "$PROJECT_LANG" in
1362
1494
  "typescript")
1363
- # Coverage already collected in the test section above (optimized single vitest run --coverage).
1364
- # The --coverage flag produces coverage/coverage-summary.json which the unconditional
1365
- # stage-2 enforcement reads. No need to run vitest again here.
1366
- echo "TypeScript coverage already collected during test run."
1367
- COV_EXIT=0
1495
+ # Coverage collected during the test section above.
1496
+ # Smart selection (Issue #286): only runs changed/related tests with --coverage,
1497
+ # producing coverage/coverage-summary.json for the tested subset.
1498
+ # Stage 2 enforcement reads global coverage from the subset run.
1499
+ # If no tests were run (TESTS_EXIT_CODE=0 skip path), warn instead of blocking.
1500
+ if [ -f "coverage/coverage-summary.json" ]; then
1501
+ echo "TypeScript coverage collected during test run."
1502
+ COV_EXIT=0
1503
+ else
1504
+ echo "ℹ️ No coverage data produced — test run was skipped (no TS changes)."
1505
+ COV_EXIT=0
1506
+ fi
1368
1507
  ;;
1369
1508
  "python")
1370
1509
  if command -v pytest &> /dev/null && command -v coverage &> /dev/null; then