Skip to content

Commit 0bca4f7

Browse files
authored
Finish pytest-owned test architecture and cut lane runtime (#1316)
* refactor(tests): finish pytest-owned suite architecture * fix(tests): include exhaustive lane in full CI reproduction * fix(ci): avoid full benchmarks for root Makefile changes * fix(ci): release superseded runs before aggregate gates
1 parent 413e384 commit 0bca4f7

60 files changed

Lines changed: 471 additions & 3154 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 7 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ jobs:
3535
package-manager-cache: false
3636
- run: make lint-full
3737
- run: make typecheck
38-
- run: make test-architecture
3938
- run: make architecture
4039
- run: make todo-check
4140
- run: make import-contracts
@@ -62,12 +61,12 @@ jobs:
6261
timeout-minutes: 15
6362
- lane: domain
6463
timeout-minutes: 15
64+
- lane: provider
65+
timeout-minutes: 25
6566
- lane: composition
6667
timeout-minutes: 25
6768
- lane: e2e
6869
timeout-minutes: 15
69-
- lane: provider
70-
timeout-minutes: 25
7170
steps:
7271
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
7372
with:
@@ -196,23 +195,6 @@ jobs:
196195
uses: ./.github/actions/setup-lean
197196
- run: make test-lean
198197

199-
optional-providers:
200-
name: Optional providers
201-
if: >-
202-
github.event_name != 'pull_request' ||
203-
contains(github.event.pull_request.labels.*.name, 'ci:provider') ||
204-
contains(github.event.pull_request.labels.*.name, 'ci:full')
205-
runs-on: ubuntu-latest
206-
timeout-minutes: 20
207-
steps:
208-
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
209-
with:
210-
persist-credentials: false
211-
- uses: ./.github/actions/setup-python-tests
212-
with:
213-
python-version: "3.12"
214-
- run: make test-provider
215-
216198
coverage:
217199
name: Coverage
218200
if: ${{ always() && !cancelled() }}
@@ -296,8 +278,8 @@ jobs:
296278

297279
required:
298280
name: required
299-
if: ${{ always() }}
300-
needs: [static, python, boundaries, wheel, coverage, lean, optional-providers]
281+
if: ${{ always() && !cancelled() }}
282+
needs: [static, python, boundaries, wheel, coverage, lean]
301283
runs-on: ubuntu-latest
302284
timeout-minutes: 5
303285
steps:
@@ -309,7 +291,6 @@ jobs:
309291
WHEEL_RESULT: ${{ needs.wheel.result }}
310292
COVERAGE_RESULT: ${{ needs.coverage.result }}
311293
LEAN_RESULT: ${{ needs.lean.result }}
312-
PROVIDERS_RESULT: ${{ needs.optional-providers.result }}
313294
run: |
314295
test "$STATIC_RESULT" = success
315296
test "$PYTHON_RESULT" = success
@@ -320,14 +301,10 @@ jobs:
320301
success|skipped) ;;
321302
*) exit 1 ;;
322303
esac
323-
case "$PROVIDERS_RESULT" in
324-
success|skipped) ;;
325-
*) exit 1 ;;
326-
esac
327304
328305
python-test:
329306
name: Python Tests
330-
if: ${{ always() }}
307+
if: ${{ always() && !cancelled() }}
331308
needs: [required]
332309
runs-on: ubuntu-latest
333310
timeout-minutes: 5
@@ -339,7 +316,7 @@ jobs:
339316

340317
lean-test:
341318
name: Lean Tests
342-
if: ${{ always() }}
319+
if: ${{ always() && !cancelled() }}
343320
needs: [lean]
344321
runs-on: ubuntu-latest
345322
timeout-minutes: 5
@@ -355,7 +332,7 @@ jobs:
355332
356333
deployment-test:
357334
name: Deployment Tests
358-
if: ${{ always() }}
335+
if: ${{ always() && !cancelled() }}
359336
needs: [static]
360337
runs-on: ubuntu-latest
361338
timeout-minutes: 5

.github/workflows/scheduled-validation.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ jobs:
2525
- run: make test-stress
2626
env:
2727
PYTEST_ARGS: --durations=20
28+
- run: make test-exhaustive
29+
env:
30+
PYTEST_ARGS: --durations=20
2831

2932
ordering:
3033
name: Ordering ${{ matrix.lane }} seed ${{ matrix.seed }}

CONTRIBUTING.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ pre-push hook stays `make lint typecheck`. Focused debugging uses
3535
`uv run pytest path/to/test.py`. Default `uv run pytest` collects the ordinary
3636
Lean-free `testpaths`; it does not run storage, process, MCP, or Lean trees.
3737

38-
CI always runs that ordinary Python surface plus storage/process/MCP
39-
boundaries and the wheel smoke. Lean and optional native/formal providers run
40-
on merge/main or with the `ci:lean` / `ci:provider` / `ci:full` labels. You do
41-
not need to reproduce those locally for a routine change.
38+
CI always runs that ordinary Python surface plus storage/process/MCP,
39+
maintained Python provider boundaries, and the wheel smoke. Lean runs on
40+
merge/main or with the `ci:lean` / `ci:full` labels. You do not need to
41+
reproduce those locally for a routine change.
4242

4343
Specialist lanes (`make test-lean`, `make test-provider`, `make test-storage`,
4444
`make test-process`, `make test-mcp`, `make test-e2e`, `make test-domain`, and
@@ -230,15 +230,16 @@ Test directories define semantic ownership: `tests/unit`, `tests/component`,
230230
`tests/domain`, `tests/composition`, `tests/boundary`, and `tests/e2e`. Use the
231231
matching `make test-*` target as the canonical entry point. Markers are retained
232232
only when they alter execution: `requires_provider(name)`, `performance`,
233-
`property`, and `destructive_process`. They do not replace directory ownership.
233+
`property`, `exhaustive`, and `destructive_process`. They do not replace
234+
directory ownership. Scheduled validation owns `make test-exhaustive`; keep a
235+
representative behavioral case in the ordinary owning lane.
234236

235237
Lane execution follows the test directory layout. Storage, process, MCP, and
236238
Lean stay on named Make targets because they need serial SQLite or kill-safe
237239
process supervision. Prefer the hydration ladder in the
238240
[testing strategy](docs/reference/testing-strategy.md): domain services before
239241
`attached_complete_runtime` before `authorized_complete_runtime` before
240-
`fresh_complete_runtime`. Inventory complete-runtime usage with
241-
`make test-runtime-inventory`.
242+
`fresh_complete_runtime`.
242243

243244
Tests may reuse concept-specific helpers under `tests/support`, but must not
244245
import helpers from a sibling semantic lane. Keep fixtures in the narrowest

Makefile

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ test-unit: ## Pure contracts and models (sequential, 10s).
3535
$(PYTEST_DIAGNOSTIC_ARGS) $(PYTEST_ARGS)
3636

3737
test-component: ## One-service component tests (4 workers, 30s).
38-
$(UV_RUN) pytest -n 4 --dist worksteal --timeout=30 \
38+
$(UV_RUN) pytest -n 4 --dist worksteal --timeout=30 -m "not exhaustive" \
3939
$(if $(TESTS),$(TESTS),tests/component) \
4040
$(PYTEST_DIAGNOSTIC_ARGS) $(PYTEST_ARGS)
4141

@@ -95,6 +95,7 @@ test-compatibility: ## Supported-version import/API compatibility smoke.
9595
test-all-ci: ## Explicitly run every semantic lane locally (exceptional).
9696
$(MAKE) test-unit
9797
$(MAKE) test-component
98+
$(MAKE) test-exhaustive
9899
$(MAKE) test-domain
99100
$(MAKE) test-composition
100101
$(MAKE) test-storage
@@ -109,6 +110,11 @@ test-stress: ## Repeat explicitly marked property tests on the scheduled lane.
109110
--count=$(STRESS_COUNT) $(if $(TESTS),$(TESTS),tests) \
110111
$(PYTEST_DIAGNOSTIC_ARGS) $(PYTEST_ARGS)
111112

113+
test-exhaustive: ## Broad finite reference sweeps reserved for scheduled validation.
114+
$(UV_RUN) pytest -n 0 --timeout=180 --timeout-method=thread -m exhaustive \
115+
$(if $(TESTS),$(TESTS),tests) \
116+
$(PYTEST_DIAGNOSTIC_ARGS) $(PYTEST_ARGS)
117+
112118
test-ordering: ## Reproduce scheduled ordering (default seed 17; override with PYTEST_ARGS).
113119
@test -n "$(ORDERING_LANE)" || { echo "ORDERING_LANE is required" >&2; exit 2; }
114120
$(MAKE) test-$(ORDERING_LANE) \
@@ -147,7 +153,7 @@ precommit: ## Fix and run every routine local handoff check.
147153
$(MAKE) fix
148154
$(MAKE) quick
149155

150-
check-static: lint-full typecheck test-architecture import-contracts test-runtime-inventory architecture todo-check build ## CI-owned static checks plus a local package build.
156+
check-static: lint-full typecheck import-contracts architecture todo-check build ## CI-owned static checks plus a local package build.
151157

152158
clean: ## Remove local caches, build outputs, and coverage artifacts.
153159
rm -rf .pytest_cache .mypy_cache .ruff_cache dist build htmlcov

benchmarks/tooling/validation_plan.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ def _discover_conjecture_host_tests() -> tuple[str, ...]:
5353
"benchmarks/validation/test_heldout_bundle.py",
5454
"benchmarks/validation/test_heldout_runner.py",
5555
),
56+
# The root Makefile is parsed by the benchmark contract targets, but its
57+
# product-test targets do not alter host verifier behavior. Harbor-owned
58+
# execution commands live in make/harbor.mk below.
59+
"Makefile": (),
5660
"benchmarks/tooling/benchmark_timings.py": (
5761
"benchmarks/validation/test_benchmark_timings.py",
5862
),
@@ -92,7 +96,7 @@ def _discover_conjecture_host_tests() -> tuple[str, ...]:
9296
),
9397
}
9498
SHARED_HOST_HARNESS_PATHS = {
95-
"Makefile",
99+
"make/harbor.mk",
96100
"tools/pytest_lifecycle.py",
97101
}
98102

benchmarks/validation/test_benchmark_planner.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,29 @@ def test_non_host_control_utilities_do_not_select_full_verifier_corpus(
207207
_assert_plan_valid(result)
208208

209209

210+
def test_root_makefile_runs_contracts_without_host_verifier_replay() -> None:
211+
result = planner.plan(["Makefile"], event="pull_request")
212+
213+
assert result["run-benchmark-check"] == "true"
214+
assert result["run-benchmark-record-schema"] == "true"
215+
assert result["run-benchmark-host-validation"] == "false"
216+
assert _host_matrix(result) == []
217+
_assert_plan_valid(result)
218+
219+
220+
def test_harbor_makefile_change_keeps_full_host_verifier_coverage() -> None:
221+
result = planner.plan(["make/harbor.mk"], event="pull_request")
222+
223+
assert result["run-benchmark-host-validation"] == "true"
224+
assert len(_host_matrix(result)) == 4
225+
assert any(
226+
"shared verifier execution harness requires full host validation: "
227+
"make/harbor.mk" in reason
228+
for reason in json.loads(result["benchmark-plan-reasons"])
229+
)
230+
_assert_plan_valid(result)
231+
232+
210233
def test_execution_configuration_change_defers_oracle_to_merge_queue() -> None:
211234
path = "benchmarks/config/jacobian.mcp.json"
212235

benchmarks/validation/test_validation_plan.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,14 +102,22 @@ def test_unknown_tooling_still_falls_back_to_full_validation(tmp_path: Path) ->
102102
)
103103

104104

105-
def test_makefile_change_remains_fail_closed_without_hunk_ownership(
105+
def test_root_makefile_change_relies_on_contract_gate_not_host_verifiers(
106106
tmp_path: Path,
107107
) -> None:
108108
plan = host_validation_plan(tmp_path, ["Makefile"], {})
109109

110+
assert plan.entries == ()
111+
assert plan.reasons == ()
112+
113+
114+
def test_harbor_makefile_change_remains_fail_closed(tmp_path: Path) -> None:
115+
plan = host_validation_plan(tmp_path, ["make/harbor.mk"], {})
116+
110117
assert len(plan.entries) == 4
111118
assert plan.reasons == (
112-
"shared verifier execution harness requires full host validation: Makefile",
119+
"shared verifier execution harness requires full host validation: "
120+
"make/harbor.mk",
113121
)
114122

115123

docs/reference/testing-strategy.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ make test-mcp
7777
make test-provider
7878
make test-lean
7979
make test-e2e
80+
make test-exhaustive
8081
make quick
8182
make check
8283
make check-external
@@ -88,6 +89,12 @@ Before starting it, confirm that no other pytest job from this checkout is
8889
running. Concurrent runtime/store/subprocess suites can turn per-test
8990
timeouts into host-contention noise.
9091

92+
Broad finite reference sweeps that are valuable but disproportionate for pull
93+
requests use the `exhaustive` marker. The component lane excludes them, and
94+
scheduled validation owns `make test-exhaustive`. Keep a representative
95+
behavioral case in the ordinary owning lane; do not use the marker to defer
96+
boundary, authorization, persistence, or public-API coverage.
97+
9198
## Test principles
9299

93100
Tests exercise public behavior and stable artifacts rather than private helper
@@ -105,6 +112,11 @@ contract better than examples. Use representative storage and process seams
105112
when the claim depends on SQLite, filesystem publication, subprocesses, or
106113
cancellation.
107114

115+
Let pytest own collection and fixture lifetime through the narrowest owning
116+
`conftest.py`. Broaden fixture scope only for reusable installation state whose
117+
tests do not mutate it; tests that revoke authority, patch shared services, or
118+
otherwise change runtime state retain an isolated fixture.
119+
108120
Do not substitute source-reading tests for caller-visible behavior. If a
109121
behavioral regression proof is infeasible, state the proof gap.
110122

make/development.mk

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: uv-version-check setup doctor setup-lean doctor-lean doctor-external setup-agent container-image eval-image eval-image-pull eval-image-bind deploy-check hooks fix lint complexity-check lint-full security-audit typecheck test-architecture test-runtime-inventory architecture docs-command-check docs-linkcheck
1+
.PHONY: uv-version-check setup doctor setup-lean doctor-lean doctor-external setup-agent container-image eval-image eval-image-pull eval-image-bind deploy-check hooks fix lint complexity-check lint-full security-audit typecheck architecture docs-command-check docs-linkcheck
22

33
uv-version-check: ## Require the repository-pinned uv release.
44
@test "$$(uv --version | awk '{print $$2}')" = "$$(tr -d '[:space:]' < .uv-version)" || { echo "install uv $$(tr -d '[:space:]' < .uv-version) before using this checkout" >&2; exit 2; }
@@ -66,15 +66,9 @@ security-audit: ## Audit dependencies for known vulnerabilities.
6666
typecheck: ## Run strict static type checking.
6767
$(UV_RUN) mypy
6868

69-
test-architecture: ## Enforce semantic test-layer and provider-import boundaries.
70-
$(UV_RUN) python -m tools.check_test_architecture .
71-
7269
import-contracts: ## Enforce declared package dependency direction.
7370
$(UV_RUN) lint-imports
7471

75-
test-runtime-inventory: ## Fail when authorized complete-runtime uses lack verify/authority signals.
76-
$(UV_RUN) python -m tools.inventory_test_runtime --fail-on-unjustified
77-
7872
architecture: ## Enforce product source boundary invariants (subprocess, shutil.which, environ, contracts, surfaces).
7973
$(UV_RUN) python tools/check_architecture.py
8074

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ markers = [
9393
"requires_provider(name): test requires a production provider readiness probe",
9494
"performance: benchmark or timing evidence excluded from correctness lanes",
9595
"property: property-based invariant tests",
96+
"exhaustive: broad finite reference sweeps run by scheduled validation",
9697
"destructive_process: test requires child-process isolation and group termination",
9798
]
9899

0 commit comments

Comments
 (0)