deer-flow/.github/workflows/backend-unit-tests.yml
spud fe379c4486
feat(ci): split backend unit tests into parallel shards (#5137)
* feat(ci): split backend unit tests into parallel CI shards

Split the single offline backend `make test` job into four GitHub Actions
matrix shards (SPLITS=4, GROUP=1..4) via pytest-split, so the ~12k-test suite
runs in parallel instead of in one 15-minute job. Each shard runs on its own
runner with its own Postgres/Redis services; fail-fast: false lets a failing
shard report its owned tests without cancelling its peers.

`make test` stays the canonical full-suite entry point; CI now calls the new
`make test-shard SPLITS=4 GROUP=N`. tests/blocking_io remains owned solely by
the dedicated blocking-I/O workflow (excluded via --ignore), extending #5105.

Fixes #5088

* test(ci): make backend test shards duration-aware and pin the contract

Make `make test-shard` an explicit least_duration split that READS
backend/.test_durations (read-only for shards, so concurrent CI jobs never
race writes on it), and add `make test-shard-durations` to regenerate that
file from the full offline suite. Update the CI unit-test workflow contract to
call `make test-shard SPLITS=4 GROUP=<n>` and assert the shard command carries
--splits 4, --group 2, -m "not live", --ignore=tests/blocking_io and
--splitting-algorithm least_duration. Verified on the real 13,140-test normal
suite that the four shards are pairwise disjoint and their union equals the
unsplit suite.

Refs #5088

* test(ci): fail fast when the duration baseline is missing

`make test-shard` now requires backend/.test_durations and exits with a clear
error instead of letting pytest-split silently degrade to an even (count-based)
split. Harden the CI contract test to pin `--durations-path=.test_durations` and
to assert the repo ships the committed duration baseline.

Refs #5088

* docs: trim backend/AGENTS.md within guidance budget

* test(ci): add backend test duration baseline

Add the duration baseline generated by a full offline backend run on a
GitHub-hosted ubuntu-latest runner (the same runner type the shards use), so
`make test-shard` balances the four matrix shards by real wall-clock cost.

Refs #5088

* test(ci): make the duration writer honor DURATIONS_FILE

`test-shard-durations` now writes `--durations-path=$(DURATIONS_FILE)` instead of a
hard-coded .test_durations, so the reader and writer stay consistent when the
path is overridden.

Refs #5088

* test(ci): address sharding review feedback

* test: isolate subagent execution capacity state

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-02 11:54:40 +08:00

117 lines
3.5 KiB
YAML

name: Unit Tests
on:
push:
branches: [ 'main', '2.0.x-dev' ]
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
concurrency:
group: unit-tests-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
default-install-collection:
# The main job installs --extra postgres; this job proves the documented
# contributor path (uv sync --group dev, no extras) still collects the
# whole suite, so optional-dependency imports must stay lazy.
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.12'
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
# Must match backend/Dockerfile's UV_IMAGE tag; pinned by backend/tests/test_ci_uv_version_pin.py
version: "0.11.1"
- name: Install backend dependencies (documented default)
working-directory: backend
run: uv sync --group dev
- name: Collect backend tests
working-directory: backend
run: uv run pytest --collect-only -q
backend-unit-tests:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
# Duration-aware split: `make test-shard` balances the shards by real
# wall-clock cost from backend/.test_durations (read-only for shards, so
# concurrent jobs never race writes on it). Every test runs exactly once.
# Fail-fast is off so a failing shard reports its tests without cancelling
# its peers.
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
services:
postgres:
image: postgres:17
env:
POSTGRES_USER: deerflow
POSTGRES_PASSWORD: deerflow
POSTGRES_DB: deerflow_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
TEST_POSTGRES_URI: postgresql://deerflow:deerflow@localhost:5432/deerflow_test?sslmode=disable
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.12'
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
# Must match backend/Dockerfile's UV_IMAGE tag; pinned by backend/tests/test_ci_uv_version_pin.py
version: "0.11.1"
- name: Install backend dependencies
working-directory: backend
run: uv sync --group dev --extra postgres
- name: Run unit tests of backend (shard ${{ matrix.shard }} of 4)
working-directory: backend
env:
# Expose the job's Postgres service to the cross-pod dedupe integration
# tests (issue #4120), which read DEDUPE_TEST_POSTGRES_URL. Without this
# mapping those tests silently skip and the headline capability is never
# exercised in CI.
DEDUPE_TEST_POSTGRES_URL: ${{ env.TEST_POSTGRES_URI }}
run: make test-shard SPLITS=4 GROUP=${{ matrix.shard }}