name: Unit Tests on: pull_request: types: [opened, ready_for_review, synchronize] paths: - 'src/langbot/**' - 'tests/**' - '.github/workflows/run-tests.yml' - 'pyproject.toml' - 'uv.lock' - 'run_tests.sh' - 'scripts/test-*.sh' - 'scripts/assert-required-tests.py' push: branches: - master - develop - 'feat/**' # No path filter on push: every push to the branches above runs the # full unit-test suite. feat/** branches in particular must be tested # on every push (they accumulate large changes before a PR exists). jobs: test: name: Unit Tests runs-on: ubuntu-latest strategy: matrix: python-version: ['3.11', '3.12', '3.13'] fail-fast: false steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install uv uses: astral-sh/setup-uv@v4 - name: Install dependencies run: uv sync --dev - name: Run unit + smoke tests run: uv run pytest tests/unit_tests/ tests/smoke/ -q --tb=short - name: Run required security regressions run: | uv run pytest tests/test_cwe94_debug_exec.py \ -q --tb=short -rs --junitxml=cwe94-results.xml python scripts/assert-required-tests.py cwe94-results.xml \ --module tests.test_cwe94_debug_exec \ --name-prefix test_no_ --expected-tests 2 - name: Upload security regression results if: always() uses: actions/upload-artifact@v4 with: name: cwe94-results-${{ matrix.python-version }} path: cwe94-results.xml if-no-files-found: ignore - name: Test Summary if: always() run: | echo "## Unit Tests Results" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "Python Version: ${{ matrix.python-version }}" >> $GITHUB_STEP_SUMMARY echo "Test Status: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY integration: name: Fast Integration Tests runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.12' - name: Install uv uses: astral-sh/setup-uv@v4 - name: Install dependencies run: uv sync --dev - name: Run fast integration tests run: uv run pytest tests/integration/ -m "not slow" -q --tb=short - name: Integration Test Summary if: always() run: | echo "## Integration Tests Results" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "Test Status: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY postgres-contracts: name: PostgreSQL Contract Tests runs-on: ubuntu-latest timeout-minutes: 15 # Only this disposable, loopback-bound CI service uses trust authentication. # Admission fixtures create a separate unprivileged role with no password; # the literal postgres@ URL lets that fixture switch to the runtime role. env: TEST_POSTGRES_URL: postgresql+asyncpg://postgres@localhost:5432/langbot_test LANGBOT_ADMISSION_TEST_URL: postgresql+asyncpg://postgres@localhost:5432/langbot_admission_test services: postgres: image: pgvector/pgvector:pg16 env: POSTGRES_USER: postgres POSTGRES_DB: langbot_test POSTGRES_HOST_AUTH_METHOD: trust ports: - '127.0.0.1:5432:5432' options: >- --health-cmd="pg_isready -U postgres -d langbot_test" --health-interval=5s --health-timeout=5s --health-retries=5 steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.12' - name: Install uv uses: astral-sh/setup-uv@v4 - name: Install dependencies run: uv sync --dev --frozen - name: Create isolated admission test database run: >- docker exec "${{ job.services.postgres.id }}" psql -U postgres -d postgres -v ON_ERROR_STOP=1 -c 'CREATE DATABASE langbot_admission_test;' - name: Run required PostgreSQL contracts run: | uv run pytest \ tests/integration/persistence/test_pipeline_admission_postgres.py \ tests/integration/persistence/test_rag_document_identity.py \ tests/integration/persistence/test_rag_interrupted_ingestion.py \ tests/integration/persistence/test_migration_branch_convergence.py \ tests/integration/persistence/test_runner_timestamps_postgres.py \ -k postgres -q --tb=short -rs --durations=10 \ --junitxml=postgres-contract-results.xml python - <<'PY' from collections import Counter import xml.etree.ElementTree as ET cases = list(ET.parse('postgres-contract-results.xml').getroot().iter('testcase')) expected = { 'test_pipeline_admission_postgres': 27, 'test_rag_document_identity': 34, 'test_rag_interrupted_ingestion': 22, 'test_migration_branch_convergence': 7, 'test_runner_timestamps_postgres': 32, } counts = Counter(case.get('classname', '').rsplit('.', 1)[-1] for case in cases) for module, minimum in expected.items(): assert counts[module] >= minimum, f'{module}: expected at least {minimum} cases, got {counts[module]}' unsuccessful = [ case.get('name') for case in cases if any(case.find(tag) is not None for tag in ('skipped', 'failure', 'error')) ] assert not unsuccessful, f'PostgreSQL contracts must pass without skips: {unsuccessful}' print(f'PostgreSQL contracts: {len(cases)} passed, 0 skipped; {dict(counts)}') PY - name: Upload PostgreSQL contract results if: always() uses: actions/upload-artifact@v4 with: name: postgres-contract-results path: postgres-contract-results.xml if-no-files-found: ignore e2e: name: E2E Startup Tests runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.12' - name: Install uv uses: astral-sh/setup-uv@v4 - name: Install dependencies run: uv sync --dev - name: Run E2E startup tests run: uv run pytest tests/e2e -q --tb=short - name: E2E Test Summary if: always() run: | echo "## E2E Startup Test Results" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "Test Status: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY box-integration: name: Box Integration Tests runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.12' - name: Install uv uses: astral-sh/setup-uv@v4 - name: Install dependencies run: uv sync --dev - name: Check Docker runtime run: docker info - name: Run Box integration tests run: uv run pytest tests/integration_tests -q --tb=short - name: Box Integration Test Summary if: always() run: | echo "## Box Integration Test Results" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "Test Status: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY coverage: name: Coverage Gate runs-on: ubuntu-latest needs: [test, integration, postgres-contracts] if: always() steps: - name: Verify required test jobs run: | test "${{ needs.test.result }}" = success test "${{ needs.integration.result }}" = success test "${{ needs.postgres-contracts.result }}" = success - name: Checkout code uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.12' - name: Install uv uses: astral-sh/setup-uv@v4 - name: Install dependencies run: uv sync --dev - name: Run coverage (unit + smoke) run: | uv run pytest tests/unit_tests/ tests/smoke/ \ --cov=langbot \ --cov-report=xml \ --cov-report=term-missing \ --cov-fail-under=18 \ -q --tb=short - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 with: files: ./coverage.xml flags: unit-tests name: coverage-report fail_ci_if_error: false env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - name: Coverage Summary if: always() run: | echo "## Coverage Results" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "Threshold: 18%" >> $GITHUB_STEP_SUMMARY echo "Status: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY