test(coverage): close 20 audited test gaps + fix 4 bugs surfaced #172
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: PR | |
| on: | |
| pull_request: | |
| branches: [main] | |
| # Cancel in-progress runs when new commits are pushed to the same PR | |
| # so we never waste minutes on outdated code. On main (push), github.ref is | |
| # refs/heads/main — separate concurrency key from PR runs. | |
| concurrency: | |
| group: pr-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| env: | |
| NODE_VERSION: "24" | |
| # Hash all workspace pyproject.toml files so cache invalidates when any | |
| # member's deps change. uv.lock is gitignored, so this is the best key | |
| # available; commit uv.lock to get true lockfile-based caching. | |
| UV_CACHE_GLOB: | | |
| pyproject.toml | |
| framework/*/pyproject.toml | |
| modules/*/pyproject.toml | |
| host/pyproject.toml | |
| jobs: | |
| python-lint: | |
| name: Python lint & format | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: astral-sh/setup-uv@v8.0.0 | |
| with: | |
| enable-cache: true | |
| cache-dependency-glob: ${{ env.UV_CACHE_GLOB }} | |
| - run: make install-py | |
| - run: make ci-python-lint | |
| python-typecheck: | |
| name: Python typecheck | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: astral-sh/setup-uv@v8.0.0 | |
| with: | |
| enable-cache: true | |
| cache-dependency-glob: ${{ env.UV_CACHE_GLOB }} | |
| - run: make install-py | |
| - run: make ci-python-typecheck | |
| python-tests: | |
| name: Python tests | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: astral-sh/setup-uv@v8.0.0 | |
| with: | |
| enable-cache: true | |
| cache-dependency-glob: ${{ env.UV_CACHE_GLOB }} | |
| - run: make install-py | |
| - run: make test-py | |
| js-tests: | |
| name: JS tests (Vitest) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: "npm" | |
| - run: make install-js | |
| - run: make test-js | |
| js-lint: | |
| name: JS lint & format | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: "npm" | |
| - run: make install-js | |
| - run: make ci-js-lint | |
| js-typecheck: | |
| name: JS typecheck | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: "npm" | |
| # Need uv too — pages.ts imports ./modules.generated, which is | |
| # produced by `smpy gen-pages` from the installed Python modules. | |
| - uses: astral-sh/setup-uv@v8.0.0 | |
| with: | |
| enable-cache: true | |
| cache-dependency-glob: ${{ env.UV_CACHE_GLOB }} | |
| - run: make install-py | |
| - run: make install-js | |
| - run: make gen-pages | |
| - run: make ci-js-typecheck | |
| js-build: | |
| name: JS build (Vite) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: "npm" | |
| # gen-pages needs the Python modules installed so Vite's import.meta.glob | |
| # sees every module's pages/ dir — which is how dep-scan failures surface. | |
| - uses: astral-sh/setup-uv@v8.0.0 | |
| with: | |
| enable-cache: true | |
| cache-dependency-glob: ${{ env.UV_CACHE_GLOB }} | |
| - run: make install-py | |
| - run: make install-js | |
| - run: make gen-pages | |
| - run: make build | |
| e2e-smoke: | |
| name: E2E smoke (Playwright) | |
| runs-on: ubuntu-latest | |
| services: | |
| redis: | |
| image: redis:7-alpine | |
| ports: ["6379:6379"] | |
| options: >- | |
| --health-cmd "redis-cli ping" | |
| --health-interval 5s | |
| --health-timeout 3s | |
| --health-retries 10 | |
| env: | |
| # SQLite keeps the job self-contained — no postgres service needed. | |
| # The bootstrap vars create the admin that tests/e2e/test_smoke.py | |
| # logs in as (admin@example.com / admin). | |
| SM_DATABASE_URL: sqlite+aiosqlite:///./app.db | |
| SM_ENVIRONMENT: development | |
| SM_SECRET_KEY: ci-test-key | |
| SM_USERS_BOOTSTRAP_EMAIL: admin@example.com | |
| SM_USERS_BOOTSTRAP_PASSWORD: admin | |
| E2E_BASE_URL: http://localhost:8000 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: astral-sh/setup-uv@v8.0.0 | |
| with: | |
| enable-cache: true | |
| cache-dependency-glob: ${{ env.UV_CACHE_GLOB }} | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: "npm" | |
| - run: make install | |
| # Playwright's chromium download is ~150MB — cache it keyed on uv.lock so | |
| # we only re-download when the pinned playwright version moves. OS deps | |
| # (apt packages) aren't cacheable across runs but install-deps is fast on | |
| # the GH runner image since most libs are preinstalled. | |
| - name: Cache Playwright browsers | |
| id: playwright-cache | |
| uses: actions/cache@v5 | |
| with: | |
| path: ~/.cache/ms-playwright | |
| key: playwright-${{ runner.os }}-${{ hashFiles('uv.lock') }} | |
| - name: Install Playwright chromium | |
| run: | | |
| if [ "${{ steps.playwright-cache.outputs.cache-hit }}" = "true" ]; then | |
| uv run --project host playwright install-deps chromium | |
| else | |
| uv run --project host playwright install --with-deps chromium | |
| fi | |
| - run: make gen-pages | |
| - run: uv run --project host alembic -c host/alembic.ini upgrade heads | |
| - name: Start API + Vite | |
| run: | | |
| uv run --project host uvicorn host.main:app --port 8000 > api.log 2>&1 & | |
| echo $! > api.pid | |
| npm run --workspace host/client_app dev > vite.log 2>&1 & | |
| echo $! > vite.pid | |
| - name: Wait for API + Vite | |
| run: | | |
| for i in $(seq 1 60); do | |
| curl -sf http://localhost:8000/health > /dev/null && \ | |
| curl -sf http://localhost:5050/@vite/client > /dev/null && \ | |
| echo "services ready" && exit 0 | |
| sleep 1 | |
| done | |
| echo "services did not come up in time" | |
| echo "--- api.log ---"; cat api.log || true | |
| echo "--- vite.log ---"; cat vite.log || true | |
| exit 1 | |
| - run: make test-e2e | |
| - name: Upload server logs on failure | |
| if: failure() | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: e2e-server-logs | |
| path: | | |
| api.log | |
| vite.log | |
| file-size-check: | |
| name: File size (300-line cap) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: astral-sh/setup-uv@v8.0.0 | |
| with: | |
| enable-cache: true | |
| cache-dependency-glob: ${{ env.UV_CACHE_GLOB }} | |
| - run: make install-py | |
| - run: make ci-check-file-size | |
| # Dry-run the release build on every PR — same `uv build --all-packages` | |
| # invocation the release workflow runs, so any packaging regression | |
| # (force-include going outside the package, missing template, broken sdist→ | |
| # wheel rebuild, etc.) fails here instead of only when someone clicks | |
| # "Run workflow" on Releases. No deps install needed: build isolation | |
| # provisions each package's build backend on its own. | |
| package-build: | |
| name: Package build (release dry-run) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: astral-sh/setup-uv@v8.0.0 | |
| with: | |
| enable-cache: true | |
| cache-dependency-glob: ${{ env.UV_CACHE_GLOB }} | |
| - run: make ci-build-packages | |
| # Single required status check for branch protection. | |
| # Protect `main` with this one check and every leaf job is required transitively. | |
| pr-checks: | |
| name: PR checks | |
| runs-on: ubuntu-latest | |
| needs: | |
| - python-lint | |
| - python-typecheck | |
| - python-tests | |
| - js-lint | |
| - js-typecheck | |
| - js-tests | |
| - js-build | |
| - e2e-smoke | |
| - file-size-check | |
| - package-build | |
| if: always() | |
| steps: | |
| - if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') | |
| run: exit 1 |