test(inference): a CI job runs the inference suite with the runtime installed - #490
Merged
Conversation
…nstalled CI installs the base distribution and never the `local-inference` extra, so every test written for the with-runtime side has skipped on every run. The `python` job's absence of the extra is deliberate and stays — it is the half that proves a base install imports none of the runtime and refuses with the install command rather than an `ImportError` — but nothing anywhere ran the other half, which left `tests/architecture/test_optional_runtime.py` asserting a contract that is true by construction wherever torch is not installed, and left a lock bump that broke the adapter, family resolution or the download path free to pass green. A second job on the `format-smoke` mould installs the extra from the lockfile and runs the inference surface on a CPU-only runner, with `VISIONSET_REQUIRE_LOCAL_INFERENCE=1` turning a missing runtime from a skip into an error. Missing-GPU skips stay skips: the runner has no CUDA device and never will, so the half-precision reproduction asks for the runtime and the device as two separate questions, in that order. The predicate five test modules each spelled out now lives once, in `tests/fixtures/local_inference.py`, beside the rule itself — and the rule has a test, written as a `try` rather than as `pytest.raises`, because a skip is a `BaseException` and sails through a `raises` looking for `RuntimeError`: against a build with the guard reverted, `raises` reported the guard's own test as skipped, and a skip is not red.
This was referenced Aug 10, 2026
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #488.
CI installs the base distribution and never the
local-inferenceextra, so everytest written for the with-runtime side has skipped on every run. This adds the
job that installs it.
What changed
A new
inference-smokejob, on theformat-smokemould: its own job becausethe extra is roughly two gigabytes of CUDA wheels,
uv sync --locked --extra local-inferenceso the install comes fromuv.lockrather than from aresolution nobody applied the cool-down to, and a CPU-only runner — the locked
torch wheels carry CUDA and run fine without a device. It runs
tests/inference,tests/architecture/test_optional_runtime.py,tests/server/test_inference.py,tests/server/test_suggest.py,tests/cli/test_inference_commands.pyandtests/jobs/test_weights_job.py.The
pythonjob is untouched. Its lack of the extra is the other half of thematrix and the only thing proving a base install imports none of the runtime.
VISIONSET_REQUIRE_LOCAL_INFERENCE=1, theVISIONSET_REQUIRE_FFMPEGrule:with the runtime meant to be present, a missing one is an error rather than a
skip. Implemented in
tests/fixtures/local_inference.py, which also holds theEXTRA_INSTALLEDpredicate that five test modules each spelled out separately.HF_HUB_OFFLINE=1, and it is not decorative — see the mutation below, wherean unstubbed code path reached for
huggingface.coand this is what stopped it.The two skip reasons are split.
test_fp16.py's reproduction hung on oneskipifover a_cuda_is_available()that answeredFalseboth for "torch isnot installed" and for "there is no device", with a reason naming both. It now asks for the runtime first (an error under the
variable) and for the device second, in the body (a skip, forever). No CI runner
has a GPU, and a CUDA test erroring under the require variable would make the job
permanently red for the one reason nobody can fix.
What this newly protects
Everything below ran only against stand-ins until now:
tests/architecture/test_optional_runtime.pyasserts that importing theserver, the CLI, the job registry and
visionset.inferenceleaves torch,torchvision, transformers, accelerate and huggingface_hub out of
sys.modules.Where none of them is installed that holds whatever the code does — a
module-level
import torchwould raise rather than register. This job is theonly place it means anything.
transformersconfigs, and the capabilityderived from the family.
huggingface_hubclient against the faked hub surface.
provider_forresolving a ready connection to something conforming to theModelProviderport with the real libraries loaded.their CPU-verifiable form.
LOCAL_INFERENCE_UNAVAILABLEnow skip, which is only correct if the extra isgenuinely there — and the require variable is what makes that check rather
than an assumption.
Counts, on the record
The job's own log carries them on every run:
-rsprints every skip with itsreason, so a future silent shrink shows up as a changed list rather than a
changed number.
Measured locally with the extra installed and
CUDA_VISIBLE_DEVICES=""to standin for a runner with no device:
The 8, all deliberate: 7 are the without-runtime half — the refusals that are
the
pythonjob's tests, skipping here because the runtime is present — and1 is the GPU (
tests/inference/test_fp16.py, "needs a CUDA device"). On amachine that does have CUDA the same command gives
272 passed, 7 skipped.Mutation verification
Each guard was broken deliberately, the anchor asserted present before and gone
after, and each reverted by
git apply -Rof its own recorded diff.The skip-to-error branch removed from
require_local_inference→test_a_missing_runtime_is_an_error_where_the_job_installed_itfails:Failed: a missing runtime skipped under VISIONSET_REQUIRE_LOCAL_INFERENCE=1.The first run of this mutation is why the test is written as a
tryratherthan as
pytest.raises(RuntimeError). A skip is raised as aBaseException,which sails straight through a
raiseslooking forRuntimeError— soagainst the mutated build the guard's own test reported skipped, not
failed, and a skip is not red. The test was rewritten to catch
pytest.skip.Exceptionby name andpytest.failon it.EXTRA_INSTALLEDforced toFalse— a broken install under the requirevariable — →
9 failed, 270 passedon the job's own command. One of thosenine is the finding that
HF_HUB_OFFLINEis load-bearing: with the predicatelying, an unstubbed size lookup reached the real hub client and the offline
guard turned a network call into an assertion failure rather than a download.
CUDA_VISIBLE_DEVICES=""— a GPU-less runner, runtime present, requirevariable set →
17 passed, 1 skipped, the skip readingneeds a CUDA device.The second gate stays a skip, which is the split working.
Removing the require variable from the workflow turns nothing red locally, and
that is the point: the protection is the job. The mechanism is what carries a
test.
Test plan
Ran in the worktree, exit codes verbatim.
With the extra (
uv sync --locked --extra local-inference, exit 0):CUDA_VISIBLE_DEVICES=""271 passed, 8 skipped— exit 0272 passed, 7 skipped— exit 0Base environment (
uv sync --locked), proving thepythonjob is unchanged:pytest tests/inference tests/architecture tests/server tests/cli tests/jobs1202 passed, 2 skipped— exit 0pytest tests/examples tests/fixtures tests/formats tests/kernel tests/mcp tests/packaging tests/scripts tests/test_versioning.py1882 passed, 7 skipped— exit 0ruff check .ruff format --check .mypy src/visionset/kernel/mypy src/visionsetlint-importsexport_openapi.py+git diff --exit-code openapi.jsonexport_mcp_tools.py --checkpnpm -r buildpnpm testpnpm -r lintnode --test tests/scripts/docs_links.test.mjs …CONTRIBUTING.mdanchorCI=1 pnpm --filter @visionset/app e2eThe two skips in the base run are the with-runtime tests, carrying the new hint —
the same two that skipped before this change, so nothing shrank.
The browser cycle suite was not run. The diff touches no
src/visionset/module, no frontend source, and no generated artifact; it changes test
infrastructure, a workflow file and three documents. CI's
cycle-e2ejob is thearbiter.
Found, not fixed
format-smokehides its own count.pyproject.tomlsetsaddopts = "-q",and that job's step adds another
-q, so it runs at-qq— which suppressesthe
N passed, M skippedline. The new job deliberately passes no-q.annotator-benchcomment block is split across the file. It beginsabove the
wheeljob and its second half sits aboveannotator-bench, withwheelandformat-smokein between; the sentence starting "The recordedbaseline lives in
docs/annotations.mdand was taken on a" completes about 170lines later.
docs/architecture/backend/inference.mdcallslocal-inferencea"dependency group". It is an extra, a distinction
pyproject.tomlspends aparagraph on — a group is for developing this repository, an extra is something
a user installs.
cf. #418, #469, #480