Skip to content

ci: run integration + acceptance tests, stabilize a flaky perf gate - #56

Merged
ethan-scitix merged 4 commits into
mainfrom
ci/expand-test-scope-and-stabilize-perf
Jul 30, 2026
Merged

ci: run integration + acceptance tests, stabilize a flaky perf gate#56
ethan-scitix merged 4 commits into
mainfrom
ci/expand-test-scope-and-stabilize-perf

Conversation

@ethan-scitix

@ethan-scitix ethan-scitix commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Type

  • chore — CI, tooling, dependencies, config

Summary

CI ran only tests/unit, so the entire integration and acceptance layer went unverified on PRs. Two bugs shipped through that gap and had to be caught by hand during the 0.7.0 release (#55): a resume fixture that predated the version gate (#37), and a perf test still calling the pre-#7 Dataset.select().

  • Add tests/integration + tests/acceptance to the matrix job, deselecting a new benchmark marker.
  • The benchmark marker exists because tests/acceptance mixes two kinds of test. Nine are deterministic checks of the regression-detection helper. One (test_benchmark_scenarios) is a wall-clock throughput gate whose thresholds are calibrated on a dedicated box — long_output holds only 1.10× headroom there (0.771 measured vs 0.70 required), and the coverage tracer skews the very latency it measures. CI gains the nine; the gate stays a local tool that a plain pytest still runs.
  • Make test_dependency_loading_overhead robust. It compares two ~20 ms hydrations as a ratio, so timing each arm once fed any transient stall straight into the metric. Each arm is now hydrated 5× and compared on its minimum — contention only ever adds time, never removes it.
  • Enforce the gate in the release flow (.claude/commands/sieval-release.md). Since CI deselects benchmark, the release becomes the only place those thresholds get checked, so the step is now explicit. It also fixes a latent defect: the gates already ran there, but inside the batch the doc says to run in parallel — a wall-clock assertion sharing a box with ruff, ty, and a 2723-test pytest run measures that load, not the code, which is the very contention that produced the 876% reading. They are now held back with -m "not benchmark" and run alone afterwards, writing benchmark_summary.json to gitignored outputs/ so each release records a performance baseline.
  • Document both markers in tests/README.md, including why a benchmark failure means "retry on an idle machine" rather than "regression".

Related Issues

Refs #55, #37, #7.

Test Plan

Automated

  • Lint/format clean (ruff check && ruff format --check)
  • Type check clean (ty check on both changed test modules: "All checks passed")
  • The exact new CI command passes locally: 2723 passed, 1 deselected
  • tests/performance: 66 passed, 5 deselected
  • Plain local pytest tests/acceptance still collects all 10 (marker does not hide it locally)
  • pytest -m benchmark selects exactly the 1 gate
  • markdownlint clean on tests/README.md and .claude/commands/sieval-release.md
  • Both new release-flow commands run as documented: the batch reports 2723 passed, 1 deselected; the serial gate reports 1 passed, 2794 deselected and writes the artifact without dirtying the tree

Manual

Flakiness fix verified empirically, not just reasoned about.

  • Idle: 27.5% before → 25.1% after (same measurement, so the bound still means what it meant).
  • Under the same concurrent-suite load that originally produced 876%: six consecutive runs now land in an 18–37% band, all passing.
  • Estimator comparison in one process, 8 paired trials: single-shot spread 7–107% vs min-of-5 spread 2–16% — a ~7× reduction in spread, which is the property that stops the 300% bound being breached by noise.

Measured acceptance headroom on a dedicated box, the basis for excluding the gate from CI:

scenario metric threshold measured headroom
long_output efficiency 0.70 0.771 1.10×
single_turn efficiency 0.70 0.936 1.34×
high_concurrency efficiency 0.65 0.859 1.32×
multi_iteration efficiency 0.60 0.895 1.49×
framework_overhead sps 500 1630 3.26×
resume_90pct elapsed 5.0 s 0.199 s 27.9×

Checklist

Required (all PRs)

  • PR title follows conventional format (type(scope): description)
  • No internal paths, credentials, or personal info in committed files
  • AI-generated code has AI-Generated Code - <model> (<provider>) in module docstring — n/a, no new modules
  • No new upper-layer dependencies added to core/
  • Deleted code verified — no remaining call sites depend on it (the removed timer_dep/timer_nodep locals were test-local)

Notes for reviewers

  • CI wall-clock cost: the new command takes ~70s locally vs ~40s for tests/unit alone. No new install groups needed — everything tests/integration and tests/acceptance import (anyio, datasets, orjson, loguru in default; psutil, pytest in test) is already covered by INSTALL_GROUPS.
  • addopts is deliberately left at -m "not stress", so benchmark tests keep running on a plain local pytest. Only the CI invocation deselects them.

🤖 Generated with Claude Code

ethan-scitix and others added 4 commits July 30, 2026 21:43
CI ran only `tests/unit`, so the whole integration and acceptance layer went
unverified on PRs. Two bugs shipped through that gap and were caught by hand
during the 0.7.0 release (#55): a resume fixture that predated the version
gate, and a perf test still calling the pre-#7 `Dataset.select()`.

Add `tests/integration` and `tests/acceptance` to the matrix job, deselecting
a new `benchmark` marker. That marker exists because `tests/acceptance` mixes
two kinds of test: nine deterministic checks of the regression-detection
helper, and one wall-clock throughput gate whose thresholds are calibrated on
a dedicated box (long_output holds only 1.10x headroom there, and coverage
tracing skews the latency it measures). CI gains the nine; the gate stays a
local tool, which a plain `pytest` still runs.

Also make `test_dependency_loading_overhead` robust. It compares two ~20ms
hydrations as a ratio, so timing each arm once fed any transient stall
straight into the metric: a loaded machine measured 876% against its 300%
bound, an idle one 27%. Each arm is now hydrated 5 times and compared on its
minimum, since contention only ever adds time. Measured under the same
concurrent-suite load that produced the 876%, the ratio now stays in an
18-37% band across six runs; the per-arm spread narrows from 7-107% to 2-16%.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CI deselects `benchmark`, so the release becomes the only place the wall-clock
throughput gates are checked. Make that explicit rather than incidental.

The gates already ran here, but inside the batch this doc says to "run in
parallel" — a wall-clock assertion sharing a box with `ruff`, `ty`, and a
2723-test pytest run measures that load, not the code. That is the same
contention that made test_dependency_loading_overhead read 876%. Hold them
back with `-m "not benchmark"` and run them alone afterwards.

Also write benchmark_summary.json to gitignored outputs/ so the release has a
recorded performance baseline, and point at the summary table's actual
location in the output (it prints after the engine's log lines).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The same two facts — why CI cannot hold the thresholds, and why a ratio of
20ms timings needs repeats — were restated in the pyproject marker comment,
the ci.yml comment, the test docstring, tests/README.md, and the release doc.
Keep each where it is closest to the code and drop the echoes: the marker
description points at tests/README.md, which owns the CI rationale, and the
docstring owns the estimator rationale. Net 39 lines out, 20 in; no behaviour
change (same 2723 passed / 1 deselected, same 27.5% idle reading).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ethan-scitix
ethan-scitix merged commit 10f5d1f into main Jul 30, 2026
9 checks passed
@ethan-scitix
ethan-scitix deleted the ci/expand-test-scope-and-stabilize-perf branch July 30, 2026 15:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant