name: ext-vscode-test on: push: branches: - main workflow_dispatch: pull_request: branches: - main workflow_call: # Set default permissions for all jobs permissions: contents: read # Needed to check out code pull-requests: read # Needed for changed-file detection on pull requests jobs: detect-changes: runs-on: ubuntu-latest name: Detect Changes outputs: vscode: ${{ steps.force.outputs.run_all == 'true' || steps.filter.outputs.vscode == 'true' }} testing_platform: ${{ steps.force.outputs.run_all == 'true' || steps.filter.outputs.testing_platform == 'true' }} steps: - id: force if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call' run: echo "run_all=true" >> "$GITHUB_OUTPUT" - uses: actions/checkout@v4 if: steps.force.outputs.run_all != 'true' - uses: dorny/paths-filter@v3 if: steps.force.outputs.run_all != 'true' id: filter with: filters: | vscode: - 'apps/vscode/src/**' - 'apps/vscode/webview-ui/**' - 'apps/vscode/proto/**' - 'apps/vscode/tests/**' - 'apps/vscode/scripts/**' - 'apps/vscode/standalone/**' - 'apps/vscode/assets/**' - 'apps/vscode/walkthrough/**' - 'apps/vscode/package.json' - 'apps/vscode/webview-ui/package.json' # Root bun lockfile is authoritative for the whole workspace (incl. apps/vscode). - 'bun.lock' # SDK source packages are local workspace symlinks (@cline/*), so SDK changes affect the build. - 'sdk/packages/**' - 'apps/vscode/buf.yaml' - 'apps/vscode/tsconfig*.json' - 'apps/vscode/biome.jsonc' - 'apps/vscode/esbuild.mjs' - 'apps/vscode/bunfig.toml' - 'apps/vscode/.vscode-test.mjs' - 'apps/vscode/test-setup.js' - '.github/workflows/ext-vscode-test.yml' testing_platform: - 'apps/vscode/src/**' - 'apps/vscode/proto/**' - 'apps/vscode/standalone/**' - 'apps/vscode/testing-platform/**' - 'apps/vscode/testing-platform/package.json' - 'apps/vscode/tests/specs/**' - 'apps/vscode/package.json' # Root bun lockfile is authoritative for the whole workspace (incl. apps/vscode). - 'bun.lock' # SDK source packages are local workspace symlinks (@cline/*), so SDK changes affect the build. - 'sdk/packages/**' - 'apps/vscode/buf.yaml' - 'apps/vscode/tsconfig*.json' - 'apps/vscode/esbuild.mjs' - 'apps/vscode/.vscodeignore' - 'apps/vscode/scripts/**' - '.github/workflows/ext-vscode-test.yml' quality-checks: needs: detect-changes if: needs.detect-changes.outputs.vscode == 'true' || needs.detect-changes.outputs.testing_platform == 'true' runs-on: ubuntu-latest name: Quality Checks defaults: run: working-directory: apps/vscode steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Bun uses: oven-sh/setup-bun@v2 with: bun-version: 1.3.14 # Single root install resolves the entire bun workspace (apps/vscode, # webview-ui, testing-platform and the @cline/* SDK symlinks) at once, # so the previous per-package `npm ci` steps collapse into one. - name: Install workspace dependencies working-directory: ${{ github.workspace }} run: bun install --frozen-lockfile env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # @cline/* are local workspace symlinks to source packages; their dist/ # output must be built before the extension can type-check/compile. - name: Build SDK packages working-directory: ${{ github.workspace }} run: bun run build:sdk - name: Assert better-sqlite3 native binary present run: | NODE_FILE="node_modules/better-sqlite3/build/Release/better_sqlite3.node" if [ ! -f "$NODE_FILE" ]; then echo "ERROR: better-sqlite3 native binary missing at apps/vscode/$NODE_FILE" echo "(bun trustedDependencies postinstall likely did not run)" exit 1 fi echo "Found better-sqlite3 native binary: $NODE_FILE" - name: Run Quality Checks (Parallel) run: bun run ci:check-all vscode-test: needs: [detect-changes, quality-checks] if: needs.detect-changes.outputs.vscode == 'true' env: VSCODE_TEST_VERSION: 1.103.0 strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest] runs-on: ${{ matrix.os }} name: ${{ matrix.os == 'ubuntu-latest' && 'vscode test' || format('vscode test ({0})', matrix.os) }} defaults: run: shell: bash working-directory: apps/vscode steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Bun uses: oven-sh/setup-bun@v2 with: bun-version: 1.3.14 # Single root install resolves the entire bun workspace at once (replaces # the per-package `npm ci` steps for apps/vscode + webview-ui). - name: Install workspace dependencies working-directory: ${{ github.workspace }} run: bun install --frozen-lockfile env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # @cline/* are local workspace symlinks to source packages; build dist/ # before compiling/testing the extension. - name: Build SDK packages working-directory: ${{ github.workspace }} run: bun run build:sdk - name: Assert better-sqlite3 native binary present run: | NODE_FILE="node_modules/better-sqlite3/build/Release/better_sqlite3.node" if [ ! -f "$NODE_FILE" ]; then echo "ERROR: better-sqlite3 native binary missing at apps/vscode/$NODE_FILE" echo "(bun trustedDependencies postinstall likely did not run)" exit 1 fi echo "Found better-sqlite3 native binary: $NODE_FILE" # NOTE: The old `npm config set script-shell bash` step is intentionally # removed. Scripts are now launched with `bun run`, which uses Bun's own # built-in cross-platform shell rather than npm's configured script-shell, # so that npm-specific Windows workaround no longer applies. Bash-dependent # scripts (e.g. scripts/proto-lint.sh, standalone/runclinecore.sh) are # invoked explicitly via `bash ...` from within the package scripts, and # this job's `defaults.run.shell: bash` (Git Bash on Windows) still covers # the workflow `run:` blocks below. - name: Cache VS Code test runtime if: runner.os == 'Windows' uses: actions/cache@v4 with: path: .vscode-test key: vscode-test-runtime-${{ runner.os }}-${{ env.VSCODE_TEST_VERSION }} # Build the extension and tests (without redundant checks) - name: Build Tests and Extension id: build_step run: bun run ci:build - name: Vitest Suites (SDK adapter + model catalog) id: vitest_tests if: ${{ !cancelled() && steps.build_step.outcome == 'success' }} # The vitest config sets passWithNoTests: true, so a broken glob/alias # would "pass" with zero tests. Capture output and assert a non-zero # test count to guard against silent skips. run: | set -o pipefail bun run test:vitest 2>&1 | tee vitest-output.log # Strip ANSI color codes before matching — vitest colorizes the # "Tests N passed" summary, so the count is not adjacent to the # "Tests" label in the raw bytes. if ! sed -r 's/\x1b\[[0-9;]*m//g' vitest-output.log | grep -Eq 'Tests[[:space:]]+[0-9]*[1-9][0-9]* (passed|failed)'; then echo "ERROR: vitest reported zero tests (possible silent skip)." exit 1 fi - name: Unit Tests (bun) - Linux id: unit_tests_linux if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os == 'Linux' }} # The node-side unit suite (.mocharc spec set) now runs under `bun test` # via scripts/run-bun-unit-tests.ts (one isolated bun process per file). # The runner exits non-zero on any failure and prints a final # "Files: N Pass: P Fail: F" summary; assert a non-zero pass count to # guard against an empty glob silently "passing". run: | set -o pipefail bun run test:unit 2>&1 | tee unit-output.log if ! grep -Eq 'Pass:[[:space:]]+[0-9]*[1-9][0-9]*' unit-output.log; then echo "ERROR: bun unit runner reported zero passing tests (possible silent skip)." exit 1 fi - name: Unit Tests (bun) - Non-Linux id: unit_tests_non_linux if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os != 'Linux' }} run: | bun run test:unit - name: Extension Integration Tests - Linux id: integration_tests_linux if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os == 'Linux' }} run: xvfb-run -a bun run test:coverage - name: Extension Integration Tests - Non-Linux id: integration_tests_non_linux if: ${{ !cancelled() && steps.build_step.outcome == 'success' && runner.os != 'Linux' }} run: | for attempt in 1 2 3; do echo "Running extension integration tests (attempt ${attempt}/3)" if bun run test:integration; then exit 0 fi if [ "$attempt" -eq 3 ]; then echo "Extension integration tests failed after 3 attempts" exit 1 fi echo "Extension integration tests failed; retrying after short delay" sleep 5 done - name: Webview Tests with Coverage id: webview_tests if: ${{ !cancelled() && steps.build_step.outcome == 'success' }} run: | cd webview-ui bun run test:coverage - name: Save Coverage Reports uses: actions/upload-artifact@v4 # Only upload artifacts on Linux - We only need coverage from one OS if: runner.os == 'Linux' with: name: pr-coverage-reports path: | apps/vscode/webview-ui/coverage/lcov.info test-platform-integration: needs: [detect-changes, quality-checks] if: needs.detect-changes.outputs.testing_platform == 'true' runs-on: ubuntu-latest defaults: run: working-directory: apps/vscode steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Bun uses: oven-sh/setup-bun@v2 with: bun-version: 1.3.14 # Single root install resolves the whole bun workspace, including the # testing-platform package, so the separate per-package `npm ci` steps # (extension + webview-ui + testing-platform) collapse into one. - name: Install workspace dependencies working-directory: ${{ github.workspace }} run: bun install --frozen-lockfile env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # @cline/* are local workspace symlinks to source packages; build dist/ # before compiling the standalone core. - name: Build SDK packages working-directory: ${{ github.workspace }} run: bun run build:sdk - name: Assert better-sqlite3 native binary present run: | NODE_FILE="node_modules/better-sqlite3/build/Release/better_sqlite3.node" if [ ! -f "$NODE_FILE" ]; then echo "ERROR: better-sqlite3 native binary missing at apps/vscode/$NODE_FILE" echo "(bun trustedDependencies postinstall likely did not run)" exit 1 fi echo "Found better-sqlite3 native binary: $NODE_FILE" - name: Download ripgrep binaries run: bun run download-ripgrep - name: Compile Standalone run: bun run compile-standalone - name: Running testing platform integration spec tests timeout-minutes: 7 run: bun run test:tp-orchestrator -- tests/specs/ --count=1 --coverage - name: Save Coverage Reports uses: actions/upload-artifact@v4 with: name: test-platform-integration-core-coverage path: apps/vscode/coverage/**/lcov.info # Keep the required "test" check as a tiny aggregate gate instead of the conditional # VS Code matrix. GitHub treats conditionally skipped jobs as successful required # checks, so the gate below preserves the old required check name while making sure # whichever filtered test jobs were selected actually passed. test: needs: [detect-changes, quality-checks, vscode-test, test-platform-integration] if: ${{ !cancelled() }} runs-on: ubuntu-latest name: test steps: - name: Verify selected test jobs env: DETECT_CHANGES_RESULT: ${{ needs.detect-changes.result }} QUALITY_CHECKS_RESULT: ${{ needs.quality-checks.result }} VSCODE_CHANGED: ${{ needs.detect-changes.outputs.vscode }} TESTING_PLATFORM_CHANGED: ${{ needs.detect-changes.outputs.testing_platform }} VSCODE_TEST_RESULT: ${{ needs.vscode-test.result }} TEST_PLATFORM_RESULT: ${{ needs.test-platform-integration.result }} run: | if [ "$DETECT_CHANGES_RESULT" != "success" ]; then echo "detect-changes did not succeed: $DETECT_CHANGES_RESULT" exit 1 fi if [ "$VSCODE_CHANGED" != "true" ] && [ "$TESTING_PLATFORM_CHANGED" != "true" ]; then echo "No root test paths changed; skipping root test requirements." exit 0 fi if [ "$QUALITY_CHECKS_RESULT" != "success" ]; then echo "quality-checks did not succeed: $QUALITY_CHECKS_RESULT" exit 1 fi if [ "$VSCODE_CHANGED" = "true" ] && [ "$VSCODE_TEST_RESULT" != "success" ]; then echo "vscode-test did not succeed: $VSCODE_TEST_RESULT" exit 1 fi if [ "$TESTING_PLATFORM_CHANGED" = "true" ] && [ "$TEST_PLATFORM_RESULT" != "success" ]; then echo "test-platform-integration did not succeed: $TEST_PLATFORM_RESULT" exit 1 fi echo "Selected root test jobs passed." qlty: needs: [detect-changes, quality-checks, vscode-test, test-platform-integration] if: ${{ !cancelled() && needs.quality-checks.result == 'success' && (needs.vscode-test.result == 'success' || needs.vscode-test.result == 'skipped') && (needs.test-platform-integration.result == 'success' || needs.test-platform-integration.result == 'skipped') && (needs.detect-changes.outputs.vscode == 'true' || needs.detect-changes.outputs.testing_platform == 'true') }} runs-on: ubuntu-latest # Run on PRs to main, pushes to main, and manual dispatches steps: - name: Checkout code uses: actions/checkout@v4 - name: Download unit tests coverage reports if: needs.detect-changes.outputs.vscode == 'true' uses: actions/download-artifact@v4 with: name: pr-coverage-reports path: apps/vscode - name: Upload core unit tests coverage to Qlty if: needs.detect-changes.outputs.vscode == 'true' uses: qltysh/qlty-action/coverage@v2 with: token: ${{ secrets.QLTY_COVERAGE_TOKEN }} # we can merge multiple files if necessary files: | apps/vscode/coverage-unit/lcov.info tag: unit:core - name: Upload webview-ui unit tests coverage to Qlty if: needs.detect-changes.outputs.vscode == 'true' uses: qltysh/qlty-action/coverage@v2 with: token: ${{ secrets.QLTY_COVERAGE_TOKEN }} # we can merge multiple files if necessary files: | apps/vscode/webview-ui/coverage/lcov.info tag: unit:webview-ui add-prefix: webview-ui/ - name: Download test platform integration core coverage artifact if: needs.detect-changes.outputs.testing_platform == 'true' uses: actions/download-artifact@v4 continue-on-error: true id: download-integration-coverage with: name: test-platform-integration-core-coverage path: apps/vscode/integration-core-coverage-reports - name: Upload core integration tests coverage to Qlty if: needs.detect-changes.outputs.testing_platform == 'true' && steps.download-integration-coverage.outcome == 'success' uses: qltysh/qlty-action/coverage@v2 with: token: ${{ secrets.QLTY_COVERAGE_TOKEN }} files: apps/vscode/integration-core-coverage-reports/**/lcov.info tag: integration:core