Skip to content

test(inference): a CI job runs the inference suite with the runtime installed - #490

Merged
JArmandoAnaya merged 1 commit into
mainfrom
ci/inference-smoke
Aug 10, 2026
Merged

test(inference): a CI job runs the inference suite with the runtime installed#490
JArmandoAnaya merged 1 commit into
mainfrom
ci/inference-smoke

Conversation

@JArmandoAnaya

Copy link
Copy Markdown
Contributor

Closes #488.

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. This adds the
job that installs it.

What changed

A new inference-smoke job, on the format-smoke mould: its own job because
the extra is roughly two gigabytes of CUDA wheels, uv sync --locked --extra local-inference so the install comes from uv.lock rather than from a
resolution 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.py and tests/jobs/test_weights_job.py.

The python job is untouched. Its lack of the extra is the other half of the
matrix and the only thing proving a base install imports none of the runtime.

VISIONSET_REQUIRE_LOCAL_INFERENCE=1, the VISIONSET_REQUIRE_FFMPEG rule:
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 the
EXTRA_INSTALLED predicate that five test modules each spelled out separately.

HF_HUB_OFFLINE=1, and it is not decorative — see the mutation below, where
an unstubbed code path reached for huggingface.co and this is what stopped it.

The two skip reasons are split. test_fp16.py's reproduction hung on one
skipif over a _cuda_is_available() that answered False both for "torch is
not 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:

  • The lazy-import contract, which was vacuous in CI.
    tests/architecture/test_optional_runtime.py asserts that importing the
    server, the CLI, the job registry and visionset.inference leaves 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 torch would raise rather than register. This job is the
    only place it means anything.
  • Family resolution reading real transformers configs, and the capability
    derived from the family.
  • The download and download-size paths driving the real huggingface_hub
    client against the faked hub surface.
  • provider_for resolving a ready connection to something conforming to the
    ModelProvider port with the real libraries loaded.
  • Tensor-to-domain conversion, NMS and mask narrowing, and the fp16 shims in
    their CPU-verifiable form.
  • The missing-extra refusal's absence: the four tests asserting
    LOCAL_INFERENCE_UNAVAILABLE now skip, which is only correct if the extra is
    genuinely 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: -rs prints every skip with its
reason, 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 stand
in for a runner with no device:

271 passed, 8 skipped

The 8, all deliberate: 7 are the without-runtime half — the refusals that are
the python job's tests, skipping here because the runtime is present — and
1 is the GPU (tests/inference/test_fp16.py, "needs a CUDA device"). On a
machine 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 -R of its own recorded diff.

  1. The skip-to-error branch removed from require_local_inference
    test_a_missing_runtime_is_an_error_where_the_job_installed_it fails:
    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 try rather
    than as pytest.raises(RuntimeError). A skip is raised as a BaseException,
    which sails straight through a raises looking for RuntimeError — so
    against 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.Exception by name and pytest.fail on it.

  2. EXTRA_INSTALLED forced to False — a broken install under the require
    variable — → 9 failed, 270 passed on the job's own command. One of those
    nine is the finding that HF_HUB_OFFLINE is load-bearing: with the predicate
    lying, 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.

  3. CUDA_VISIBLE_DEVICES="" — a GPU-less runner, runtime present, require
    variable set → 17 passed, 1 skipped, the skip reading needs 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):

Stage Result
The job's command, CUDA_VISIBLE_DEVICES="" 271 passed, 8 skipped — exit 0
The job's command, GPU present 272 passed, 7 skipped — exit 0

Base environment (uv sync --locked), proving the python job is unchanged:

Stage Result
pytest tests/inference tests/architecture tests/server tests/cli tests/jobs 1202 passed, 2 skipped — exit 0
pytest tests/examples tests/fixtures tests/formats tests/kernel tests/mcp tests/packaging tests/scripts tests/test_versioning.py 1882 passed, 7 skipped — exit 0
ruff check . exit 0
ruff format --check . exit 0
mypy src/visionset/kernel / mypy src/visionset exit 0 / exit 0
lint-imports 4 contracts kept, 0 broken — exit 0
export_openapi.py + git diff --exit-code openapi.json exit 0
export_mcp_tools.py --check exit 0
pnpm -r build exit 0
pnpm test 77 node tests pass — exit 0
pnpm -r lint exit 0
node --test tests/scripts/docs_links.test.mjs … 16 pass — exit 0, and it is the gate for the new CONTRIBUTING.md anchor
CI=1 pnpm --filter @visionset/app e2e 238 passed — exit 0

The 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-e2e job is the
arbiter.

Found, not fixed

  • format-smoke hides its own count. pyproject.toml sets addopts = "-q",
    and that job's step adds another -q, so it runs at -qq — which suppresses
    the N passed, M skipped line. The new job deliberately passes no -q.
  • The annotator-bench comment block is split across the file. It begins
    above the wheel job and its second half sits above annotator-bench, with
    wheel and format-smoke in between; the sentence starting "The recorded
    baseline lives in docs/annotations.md and was taken on a" completes about 170
    lines later.
  • docs/architecture/backend/inference.md calls local-inference a
    "dependency group".
    It is an extra, a distinction pyproject.toml spends a
    paragraph on — a group is for developing this repository, an extra is something
    a user installs.

cf. #418, #469, #480

…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.
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.

CI never runs the inference suite with the runtime installed

1 participant