Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.odds-api.io/llms.txt

Use this file to discover all available pages before exploring further.

PR Test Workflow Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.
Goal: Add a GitHub Actions workflow that runs all unit tests, typecheck, and lint for v3/, account-service/, and web/ on every PR, and gate merges on it passing. Architecture: One workflow file with three parallel per-subproject jobs plus an aggregator job. Path filters skip irrelevant jobs; the aggregator (all-checks) is the single required status check so branch protection works correctly with skipped jobs. Tech Stack: GitHub Actions, actions/setup-go@v5, actions/setup-node@v4, pnpm/action-setup@v4, Go 1.24, Node 22, pnpm. Spec: docs/superpowers/specs/2026-05-06-ci-pr-tests-design.md

File Structure

Create:
  • .github/workflows/pr-tests.yml — the workflow
  • .github/workflows/README.md — branch-protection setup instructions
Modify:
  • web/package.json — add typecheck script
  • v3/websocket/load_test.go — guard heavy test with testing.Short()
  • v3/websocket/load_stress_test.go — guard heavy tests
  • v3/websocket/hub_performance_test.go — guard the performance/throughput tests (not the simple unit ones)
  • v3/websocket/replay_benchmark_test.go — guard the latency test
  • v3/websocket/throughput_benchmark_test.go — guard the throughput tests
  • v3/websocket/peak_hour_fixes_test.go — guard only the integration test
No new Go test files; no new TS test files.

Task 1: Add typecheck script to web/package.json

Files:
  • Modify: web/package.json
  • Step 1: Read current scripts block
Run: grep -n typecheck web/package.json Expected: no matches (script doesn’t exist yet)
  • Step 2: Add the script
In web/package.json, in the "scripts" object, add this line after "test:watch": "vitest":
    "typecheck": "tsc --noEmit"
The "scripts" block should end up looking like:
"scripts": {
  "dev": "next dev --turbopack",
  "build": "next build",
  "start": "next start",
  "lint": "biome check .",
  "lint:fix": "biome check . --write",
  "format": "biome format .",
  "format:fix": "biome format . --write",
  "clean": "rm -rf node_modules .next",
  "postbuild": "next-sitemap",
  "test": "vitest run",
  "test:watch": "vitest",
  "typecheck": "tsc --noEmit"
}
  • Step 3: Run it locally to verify it works
Run: cd web && pnpm typecheck Expected: exits 0 with no output (or known pre-existing errors — note them, but don’t fix as part of this task; if there are errors, jump to Task 2 and come back)
  • Step 4: Commit
git add web/package.json
git commit -m "chore(web): add typecheck script"

Task 2: Fix any pre-existing typecheck errors in web/

Only do this task if Task 1 step 3 surfaced errors. Skip if it was clean. Files:
  • Modify: whatever files tsc --noEmit complains about
  • Step 1: Capture the full error list
Run: cd web && pnpm typecheck 2>&1 | tee /tmp/web-tsc-errors.log
  • Step 2: Triage
Read /tmp/web-tsc-errors.log. For each error:
  • If it’s a real type bug → fix it
  • If it’s noise from a misconfigured tsconfig.json (e.g. excluded files being included) → fix the tsconfig.json instead
Do NOT widen types with any or add // @ts-ignore to make errors disappear.
  • Step 3: Re-run until clean
Run: cd web && pnpm typecheck Expected: exits 0 with no output
  • Step 4: Commit
git add web/
git commit -m "fix(web): resolve typecheck errors surfaced by new script"

Task 3: Audit and guard heavy Go tests with testing.Short()

The CI workflow will run go test -short ./.... Tests that should be excluded need a t.Skip guard. Files to modify (the audit list — verify each before editing):
  • v3/websocket/load_test.goTestLoadSimulation_50Clients_HighThroughput
  • v3/websocket/load_stress_test.goTestLoadStress_50Clients_MaxThroughput, TestLoadStress_200Clients
  • v3/websocket/hub_performance_test.goTestHubThroughput, TestHubBackpressure, TestHubLastKnownStateCleanup (audit each first — skip only the truly heavy ones; the file also has lightweight tests like TestHubClientFiltering, TestHubSportFilter, TestHubStatusFilter that should NOT be guarded)
  • v3/websocket/replay_benchmark_test.goTestReplayBatchProcessingLatency
  • v3/websocket/throughput_benchmark_test.goTestThroughputComparison, TestMessageDeliveryGuarantee, TestBackpressureHandling
  • v3/websocket/peak_hour_fixes_test.go — only TestPeakHourCoalescingIntegration. The other Test funcs in that file (TestCoalescing*, TestWorkerPool*, TestBookmakerSet*, TestGetBookmakerSetConsistency) are simple unit tests — leave them.
How to decide what’s “heavy”: A test is heavy if it takes longer than ~2 seconds, spawns >100 goroutines, runs >1k iterations, or sleeps for hundreds of ms in loops. When in doubt, run it locally and time it. If go test -run TestX -v ./websocket takes >5s on your machine, skip it under -short.
  • Step 1: Audit one file
Pick one file from the list. Read it. For each func Test* confirm whether it should be skipped under -short. Make a list.
  • Step 2: Add the skip guard
For each test you decided to guard, add this as the FIRST line inside the function body:
if testing.Short() {
    t.Skip("skipping heavy test in -short mode")
}
Example, for TestLoadSimulation_50Clients_HighThroughput in v3/websocket/load_test.go:
func TestLoadSimulation_50Clients_HighThroughput(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping heavy test in -short mode")
	}
	hub := NewHub()
	defer hub.Stop()
	go hub.Run()
	// ...rest unchanged
}
  • Step 3: Repeat for all files in the audit list
  • Step 4: Verify the short suite passes locally
Run: cd v3 && go test -short -race ./... Expected: all tests pass; the heavy ones report as --- SKIP:. Should complete in well under a minute. If you see any test panic or hang, that’s a real bug — fix the root cause, don’t skip the test.
  • Step 5: Verify the full suite still works (sanity check, optional but recommended)
Run a single heavy test without -short to confirm it still runs: Run: cd v3 && go test -run TestLoadSimulation_50Clients_HighThroughput -v ./websocket Expected: PASS (not skipped)
  • Step 6: Commit
git add v3/websocket/
git commit -m "test(v3): guard heavy websocket tests with testing.Short()"

Task 4: Verify all subproject test/lint commands work locally

This catches problems with the local commands BEFORE we put them in CI, where failures are slower to debug. Files: none
  • Step 1: Confirm Go suite passes under -short
Run:
cd v3
gofmt -l .
go vet ./...
go build ./...
go test -short -race ./...
Expected: gofmt -l . produces no output; everything else exits 0. If gofmt -l . lists files, run gofmt -w <files> and commit those changes separately:
git add v3/
git commit -m "style(v3): apply gofmt"
  • Step 2: Confirm account-service commands work
Run:
cd account-service
pnpm install --frozen-lockfile
pnpm typecheck
pnpm exec biome check .
pnpm test
Expected: every command exits 0. If biome check reports errors, run pnpm exec biome check . --write and commit. If pnpm typecheck reports errors, fix them — same rule as Task 2: no any, no @ts-ignore band-aids.
  • Step 3: Confirm web commands work
Run:
cd web
pnpm install --frozen-lockfile
pnpm typecheck
pnpm lint
pnpm test
pnpm build
Expected: every command exits 0. If pnpm lint reports errors, run pnpm lint:fix and commit. If pnpm build fails, fix the underlying issue.
  • Step 4: Commit any fixups
git status
# If there are fixups:
git add <files>
git commit -m "chore: fix lint/format issues surfaced by CI prep"

Task 5: Create the workflow file

Files:
  • Create: .github/workflows/pr-tests.yml
  • Step 1: Create directory if needed
Run: mkdir -p .github/workflows
  • Step 2: Write the workflow
Create .github/workflows/pr-tests.yml with this exact content:
name: PR Tests

on:
  pull_request:
    branches: [main]

# Cancel in-progress runs when a new commit is pushed to the same PR
concurrency:
  group: pr-tests-${{ github.event.pull_request.number }}
  cancel-in-progress: true

jobs:
  v3-go:
    name: v3 (Go)
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: v3
    steps:
      - uses: actions/checkout@v4

      - name: Check if v3 changed
        id: changes
        uses: dorny/paths-filter@v3
        with:
          filters: |
            v3:
              - 'v3/**'
              - '.github/workflows/pr-tests.yml'

      - uses: actions/setup-go@v5
        if: steps.changes.outputs.v3 == 'true'
        with:
          go-version-file: v3/go.mod
          cache-dependency-path: v3/go.sum

      - name: gofmt
        if: steps.changes.outputs.v3 == 'true'
        run: |
          unformatted=$(gofmt -l .)
          if [ -n "$unformatted" ]; then
            echo "::error::Files are not gofmt'd:"
            echo "$unformatted"
            exit 1
          fi

      - name: go vet
        if: steps.changes.outputs.v3 == 'true'
        run: go vet ./...

      - name: go build
        if: steps.changes.outputs.v3 == 'true'
        run: go build ./...

      - name: go test (-short)
        if: steps.changes.outputs.v3 == 'true'
        run: go test -short -race ./...

  account-service:
    name: account-service (TS)
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: account-service
    steps:
      - uses: actions/checkout@v4

      - name: Check if account-service changed
        id: changes
        uses: dorny/paths-filter@v3
        with:
          filters: |
            account:
              - 'account-service/**'
              - '.github/workflows/pr-tests.yml'

      - uses: pnpm/action-setup@v4
        if: steps.changes.outputs.account == 'true'
        with:
          version: 9

      - uses: actions/setup-node@v4
        if: steps.changes.outputs.account == 'true'
        with:
          node-version: 22
          cache: pnpm
          cache-dependency-path: account-service/pnpm-lock.yaml

      - name: Install
        if: steps.changes.outputs.account == 'true'
        run: pnpm install --frozen-lockfile

      - name: Typecheck
        if: steps.changes.outputs.account == 'true'
        run: pnpm typecheck

      - name: Lint (biome)
        if: steps.changes.outputs.account == 'true'
        run: pnpm exec biome check .

      - name: Test
        if: steps.changes.outputs.account == 'true'
        run: pnpm test

  web:
    name: web (Next.js)
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: web
    steps:
      - uses: actions/checkout@v4

      - name: Check if web changed
        id: changes
        uses: dorny/paths-filter@v3
        with:
          filters: |
            web:
              - 'web/**'
              - '.github/workflows/pr-tests.yml'

      - uses: pnpm/action-setup@v4
        if: steps.changes.outputs.web == 'true'
        with:
          version: 9

      - uses: actions/setup-node@v4
        if: steps.changes.outputs.web == 'true'
        with:
          node-version: 22
          cache: pnpm
          cache-dependency-path: web/pnpm-lock.yaml

      - name: Install
        if: steps.changes.outputs.web == 'true'
        run: pnpm install --frozen-lockfile

      - name: Typecheck
        if: steps.changes.outputs.web == 'true'
        run: pnpm typecheck

      - name: Lint (biome)
        if: steps.changes.outputs.web == 'true'
        run: pnpm lint

      - name: Test
        if: steps.changes.outputs.web == 'true'
        run: pnpm test

      - name: Build
        if: steps.changes.outputs.web == 'true'
        run: pnpm build

  all-checks:
    name: all-checks
    needs: [v3-go, account-service, web]
    if: always()
    runs-on: ubuntu-latest
    steps:
      - name: Verify all required jobs succeeded or were skipped
        run: |
          declare -A results
          results[v3-go]="${{ needs.v3-go.result }}"
          results[account-service]="${{ needs.account-service.result }}"
          results[web]="${{ needs.web.result }}"

          failed=0
          for job in "${!results[@]}"; do
            result="${results[$job]}"
            echo "$job: $result"
            if [ "$result" = "failure" ] || [ "$result" = "cancelled" ]; then
              failed=1
            fi
          done

          if [ $failed -eq 1 ]; then
            echo "::error::One or more required jobs failed or were cancelled"
            exit 1
          fi
          echo "All required jobs passed or were correctly skipped."
Note on the path-filter pattern: Each job uses dorny/paths-filter@v3 inside the job (after checkout) instead of GitHub’s top-level paths: filter, then guards every meaningful step with if: steps.changes.outputs.X == 'true'. This pattern lets the job always run (so it always reports a status) but no-op when its files didn’t change. It’s the workaround for the well-known “skipped path-filtered jobs block required-checks branch protection” issue.
  • Step 3: Validate the YAML locally
Run: python3 -c "import yaml; yaml.safe_load(open('.github/workflows/pr-tests.yml'))" Expected: no output, exit 0. If you don’t have Python with PyYAML, alternatively use yq or just rely on the GitHub validation when you push — but local validation catches syntax errors before the CI round-trip.
  • Step 4: Commit
git add .github/workflows/pr-tests.yml
git commit -m "ci: add PR tests workflow for v3, account-service, and web"

Task 6: Document branch-protection setup

Files:
  • Create: .github/workflows/README.md
  • Step 1: Write the README
Create .github/workflows/README.md with this content:
# CI Workflows

## `pr-tests.yml`

Runs unit tests, typecheck, and lint for every PR targeting `main`.

**Jobs:**
- `v3-go` — Go: gofmt, vet, build, `go test -short -race ./...`
- `account-service` — TypeScript: typecheck, biome lint, vitest
- `web` — Next.js: typecheck, biome lint, vitest, `next build`
- `all-checks` — aggregator that fails if any of the above failed

Each job uses an in-job path filter so it no-ops when its subproject's files didn't change.
The `all-checks` job always runs and is the single status check that branch protection requires.

## Required setup (one-time, manual)

Branch protection cannot be enabled by a workflow — toggle it in the GitHub UI:

1. Go to **Repo Settings → Branches → Branch protection rules**
2. Click **Add branch protection rule** (or edit the existing rule for `main`)
3. **Branch name pattern:** `main`
4. Enable **Require a pull request before merging**
5. Enable **Require status checks to pass before merging**
   - Enable **Require branches to be up to date before merging**
   - In the search box, type `all-checks` and select it
6. Recommended: enable **Do not allow bypassing the above settings** (applies to admins too)
7. Save

After this is set, PRs cannot be merged into `main` until `all-checks` is green.

## Excluding heavy tests from CI

Go load/stress/throughput tests are guarded with `testing.Short()` and skipped under `go test -short`.
Run them locally with `go test ./websocket -run TestLoadStress -v` (drop the `-short` flag).

## Adding a new subproject

1. Add a new job to `pr-tests.yml` modeled on the existing three
2. Add it to the `needs:` list of `all-checks`
3. Add it to the `results[...]` map in the `all-checks` script
  • Step 2: Commit
git add .github/workflows/README.md
git commit -m "docs(ci): document PR test workflow and branch-protection setup"

Task 7: End-to-end validation on a real PR

Files: none — this validates the workflow against GitHub.
  • Step 1: Push the branch and open a PR
git push -u origin HEAD
gh pr create --title "ci: add PR tests workflow" --body "$(cat <<'EOF'
## Summary
- Adds `.github/workflows/pr-tests.yml` running unit tests, typecheck, and lint for `v3/`, `account-service/`, and `web/` on every PR
- Adds an `all-checks` aggregator job to use as the single required status check
- Guards heavy Go load/stress tests with `testing.Short()` so CI runs fast
- Adds `typecheck` script to `web/package.json`
- Documents branch-protection setup in `.github/workflows/README.md`

See `docs/superpowers/specs/2026-05-06-ci-pr-tests-design.md` for design.

## Test plan
- [ ] All three jobs pass on this PR
- [ ] `all-checks` passes
- [ ] Branch protection enabled in repo settings (manual step — see workflow README)
- [ ] Verify on a follow-up PR that touching only one subproject correctly skips the others while still letting `all-checks` pass
EOF
)"
  • Step 2: Watch the run
Run: gh pr checks --watch Expected: all four jobs (v3 (Go), account-service (TS), web (Next.js), all-checks) report success.
  • Step 3: Triage failures
If a job fails:
  1. Run gh run view --log-failed to get failing step output
  2. Reproduce locally with the exact command from that step (e.g. cd web && pnpm test)
  3. Fix the root cause — do NOT loosen the check to make it green. If a test is flaky in CI but passes locally, that’s a CI environment issue worth fixing (timing, missing env var, etc.), not something to retry-loop around.
  4. Push fix; checks rerun automatically.
  • Step 4: Enable branch protection
Once all checks are green, follow the instructions in .github/workflows/README.md to enable branch protection. This is a manual step in the GitHub UI.
  • Step 5: Verify branch protection works
After enabling, confirm in the PR view that the merge button is gated on all-checks. If all-checks is not listed as required, the rule was misconfigured — go back to step 4.
  • Step 6: Merge the PR
Once green and protection is verified, merge.
gh pr merge --squash

Done criteria

  • .github/workflows/pr-tests.yml exists and is valid YAML
  • .github/workflows/README.md exists with setup instructions
  • web/package.json has a typecheck script
  • Heavy Go tests skip under -short; lightweight ones still run
  • All four jobs report success on the bootstrapping PR
  • Branch protection is enabled and gates merges on all-checks
  • PR is merged