From 13bad4f4e9f32c1be1a1118bc2d50b281ccf33eb Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 26 Mar 2026 17:55:53 -0500 Subject: [PATCH] ci: enforce test requirements and coverage thresholds on PRs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add two CI gates to enforce CONTRIBUTING.md test requirements: 1. File-matching check (lint job): fails PRs that change source files without including test file changes. Exempts docs/chore/ci branches. 2. Coverage gate (build job): wires existing `npm run test:coverage` into CI with c8 thresholds (40% statements/lines, 20% branches/functions). Previously defined in package.json but never ran in CI. Lowers coverage thresholds from 50% to 40% for statements/lines to match current codebase reality (~44%) — prevents the gate from blocking every PR on day one while still catching coverage regressions. --- .github/workflows/ci.yml | 9 ++++++ package.json | 2 +- scripts/require-tests.sh | 67 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 1 deletion(-) create mode 100755 scripts/require-tests.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4e0e5f64a..17351ebb2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -102,6 +102,12 @@ jobs: - name: Validate skill references run: node scripts/check-skill-references.mjs + - name: Require tests with source changes + if: github.event_name == 'pull_request' + env: + PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} + run: bash scripts/require-tests.sh + build: timeout-minutes: 15 needs: detect-changes @@ -145,6 +151,9 @@ jobs: - name: Run integration tests run: npm run test:integration + - name: Check test coverage thresholds + run: npm run test:coverage + windows-portability: timeout-minutes: 15 needs: detect-changes diff --git a/package.json b/package.json index c24afcbf2..b9470dea4 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "test:unit": "node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --experimental-test-isolation=process --test src/resources/extensions/gsd/tests/*.test.ts src/resources/extensions/gsd/tests/*.test.mjs src/tests/*.test.ts", "test:packages": "node --test packages/pi-coding-agent/dist/core/*.test.js", "test:marketplace": "GSD_TEST_CLONE_MARKETPLACES=1 node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test src/resources/extensions/gsd/tests/claude-import-tui.test.ts src/resources/extensions/gsd/tests/plugin-importer-live.test.ts src/tests/marketplace-discovery.test.ts", - "test:coverage": "c8 --reporter=text --reporter=lcov --exclude='src/resources/extensions/gsd/tests/**' --exclude='src/tests/**' --exclude='scripts/**' --exclude='native/**' --exclude='node_modules/**' --check-coverage --statements=50 --lines=50 --branches=20 --functions=20 node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --experimental-test-isolation=process --test src/resources/extensions/gsd/tests/*.test.ts src/resources/extensions/gsd/tests/*.test.mjs src/tests/*.test.ts", + "test:coverage": "c8 --reporter=text --reporter=lcov --exclude='src/resources/extensions/gsd/tests/**' --exclude='src/tests/**' --exclude='scripts/**' --exclude='native/**' --exclude='node_modules/**' --check-coverage --statements=40 --lines=40 --branches=20 --functions=20 node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --experimental-test-isolation=process --test src/resources/extensions/gsd/tests/*.test.ts src/resources/extensions/gsd/tests/*.test.mjs src/tests/*.test.ts", "test:integration": "node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --experimental-test-isolation=process --test src/resources/extensions/gsd/tests/*integration*.test.ts src/tests/integration/*.test.ts", "pretest": "npm run typecheck:extensions", "test": "npm run test:unit && npm run test:integration", diff --git a/scripts/require-tests.sh b/scripts/require-tests.sh new file mode 100755 index 000000000..900be6226 --- /dev/null +++ b/scripts/require-tests.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# GSD-2 — Require tests with source changes +# Copyright (c) 2026 Jeremy McSpadden +# +# Fails CI if a PR changes source files but includes no test file changes. +# Exemptions: docs-only, CI/config, test-only, and chore branches. + +set -euo pipefail + +# --- resolve base ref --- +if [ -n "${PR_BASE_SHA:-}" ]; then + BASE="$PR_BASE_SHA" +elif [ -n "${PUSH_BEFORE_SHA:-}" ]; then + BASE="$PUSH_BEFORE_SHA" +else + BASE="origin/main" +fi + +FILES=$(git diff --name-only "$BASE" HEAD 2>/dev/null || git diff --name-only HEAD~1) + +# --- exempt branch types that don't need tests --- +BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "") +if [[ "$BRANCH" =~ ^(docs|chore|ci)/ ]]; then + echo "✓ Branch type '${BRANCH%%/*}/' is exempt from test requirements" + exit 0 +fi + +# --- classify changed files --- +# Source files: .ts/.mts/.mjs/.js in src/ or packages/, excluding tests and type declarations +SRC_FILES=$(echo "$FILES" | grep -E '^(src|packages)/.*\.(ts|mts|mjs|js)$' \ + | grep -vE '\.(test|spec)\.' \ + | grep -vE '\.d\.ts$' \ + | grep -vE '__tests__/' \ + | grep -vE '/tests/' \ + || true) + +# Test files: anything with .test. or .spec. or inside __tests__/ or tests/ +TEST_FILES=$(echo "$FILES" | grep -E '\.(test|spec)\.(ts|mts|mjs|js|cjs)$' || true) + +# --- no source changes? nothing to enforce --- +if [ -z "$SRC_FILES" ]; then + echo "✓ No source file changes detected — test requirement does not apply" + exit 0 +fi + +# --- source changes exist — require test changes --- +SRC_COUNT=$(echo "$SRC_FILES" | wc -l | tr -d ' ') + +if [ -z "$TEST_FILES" ]; then + echo "──────────────────────────────────────────────────────" + echo "✗ FAILED: Source files changed but no tests included" + echo "──────────────────────────────────────────────────────" + echo "" + echo "Changed source files ($SRC_COUNT):" + echo "$SRC_FILES" | sed 's/^/ /' + echo "" + echo "Per CONTRIBUTING.md:" + echo " • Bug fixes must include a regression test" + echo " • Features must include tests covering primary success + one failure path" + echo " • Behavior changes must update existing tests" + echo "" + echo "Add or update test files (*.test.ts) to proceed." + exit 1 +fi + +TEST_COUNT=$(echo "$TEST_FILES" | wc -l | tr -d ' ') +echo "✓ Test requirement satisfied: $SRC_COUNT source file(s), $TEST_COUNT test file(s) changed"