Skip to content

Test coverage plan: the unit suite has never caught a bug #128

Description

@solderzzc

Tracking issue for test-coverage work, derived from the nine defects found during the #108/#110/#112 fix cycle rather than from a generic coverage target.

The observation

Sorting those nine defects by what actually caught them:

Caught by Defects
Loading a real checkpoint #118 (OptiQ norm shift), #120 (Gemma 4 KV-shared), the b674 e2b regression, per_layer_model_projection quantization
Code review stop-sequence leak (#126), a delete-guard bypass, CLI cache-root mismatch, listed-but-not-loadable models
The unit suite none

~250 unit tests exist and every one of them was written after its bug was already known. They lock in regressions, which is worth having, but they have never surfaced a defect. The defects lived in two places the suite does not reach: weight-and-config-shape assumptions, which only a real checkpoint exercises, and control flow, which only a reader caught.

Tier 1 — Architecture smoke matrix

Load + generate one token per checkpoint shape, not per model name. Shapes, and the defect each covers:

Shape Representative Covers
KV-shared, weights present gemma-4-e2b-it-4bit the b674 regression — landed in #127
KV-shared, weights absent gemma-4-e4b-it-4bit #120
Mixed precision + files outside the weight index Qwen3.6-27B-OptiQ-4bit #118
MoE with nested config Qwen3.6-35B-A3B-4bit #112
Dense baseline Qwen2.5-0.5B-Instruct-4bit —

Split by size so the cost is bounded: small shapes on every PR, the 19–20 GB ones nightly or only on submodule-bump PRs.

Tier 2 — API contract tests

One tiny model, asserting server behaviour end to end: stop sequences (including one split across token chunks), streaming deltas, the reasoning_content / content split, the two-round tool-call flow, JSON mode. This is the class review caught and the suite cannot see, and #126 is currently open with no scaffolding to verify a fix against.

Tier 3 — Make the local suite runnable

A bare swift test aborts on the second test with Failed to load the default metallib — locally about 1 of 250 tests runs. CI is unaffected because it installs the metallib explicitly (ci.yml, "Install MLX Metal library"), so this is a contributor trap: a local run looks green while testing almost nothing. Needs a test bootstrap step or a documented make test.

Tier 4 — Discipline

Open questions

  • CI-minute budget per PR, which sets the small/nightly split for Tier 1.
  • Whether large checkpoints live in a persistent cache or are fetched per run.

Activity

  1. solderzzc commented on Aug 15, 2026

    @solderzzc
    MemberAuthor

    Tier 4 done — coverage measured rather than estimated. Tooling in #146 (scripts/coverage-report.sh).

    Baseline, both suites. Reading either alone misleads: from the umbrella package the model architectures look like 0%, which is an artifact of their tests living in the submodule.

    suite area lines coverage
    SwiftLM package server 5,602 24.4%
    inference core 3,113 57.7%
    SwiftBuddy app 18,701 5.3%
    mlx-swift-lm model architectures (LLM) 17,126 24.1%
    model architectures (VLM) 13,304 0.3%
    MLXLMCommon 11,076 41.3%

    The example this issue named is confirmed, and it is the worst single case. MLXVLM/Models/Gemma4.swift — where SharpAI/mlx-swift-lm#45 landed — has no tests, and at 1,771 lines it is the largest untested file in the model layer.

    The pattern around it matters more than the one file. The entire VLM half of the model layer is unexecuted by unit tests: Qwen3VL, Qwen35, LFM2VL, GlmOcr, Qwen25VL, Gemma3, Pixtral, FastVLM all at zero. CI runs vision and omni end-to-end, so these paths do get exercised — but a shape or dtype error inside one surfaces as a wrong answer about an image rather than a failing assertion. That is the same failure shape this issue was opened about: real defects reaching users through code the suite never runs.

    Where that leaves the tiers:

    Tier Status
    1 — Architecture shapes done — #118, #120, b674, #112 each covered by a synthetic fixture, each red-green verified against the code that shipped the bug (#143, #144, #145)
    2 — API contract tests done — 10 assertions incl. the chunked-prefill case
    3 — Local suite runnable done
    4 — Discipline coverage measurable as of #146; red-checking every new test has been practice throughout

    On the open question this issue raised — "CI-minute budget per PR, which sets the small/nightly split for Tier 1" — that constraint dissolved rather than being answered. The fixtures are 1.5 MB total and the job runs in ~20 s with no downloads, against a 10 GB per-repository cache budget that a single real checkpoint (gemma-4-e2b, 3.6 GB) would have dominated. No split is needed.

    What I would do next, if anything: the VLM unit gap is now the largest measured hole, and it is addressable the same way Tier 1 was — a synthetic VLM fixture exercising the vision tower's shape handling would cost kilobytes. Whether that is worth it depends on whether VLM defects are actually reaching users; the ones this cycle produced were all LLM-side.

  2. solderzzc commented on Aug 29, 2026

    @solderzzc
    MemberAuthor

    Status update — most of this plan is already done:

    • Tier 1 (architecture smoke matrix) — done. 5 synthetic checkpoint fixtures (dense, kv-shared-absent, kv-shared-present, moe-nested, stray-shard) cover the shapes in the table above.
    • Tier 2 (API contract tests) — done, running in CI as the integration_matrix (contract, ...) job on every PR.
    • Tier 4's example (MLXVLM/Models/Gemma4.swift had zero tests) — fixed, along with 5 other VLM architectures (Qwen3VL, Qwen35, LFM2VL, GlmOcr, Qwen25VL).

    Still open: Tier 3 (make the local suite runnable without CI's help). A bare swift test in a fresh checkout still aborts with Failed to load the default metallib — CI has a dedicated install step, but locally you have to manually cmake+make mlx-metallib and copy the .metallib into the built .xctest bundle's Contents/MacOS/ (not just .build/.../debug/). I hit this myself repeatedly working on other PRs this session. Planning to add a bootstrap script/target to fix this next.

    Leaving this open, scoped down to Tier 3 + the two open questions (CI-minute budget, checkpoint caching strategy).

  3. HamedShams commented on Oct 9, 2026

    @HamedShams
    Contributor

    Fresh-clone data point for Tier 3, since the last status here predates Xcode 27. M1 Max 64 GB, macOS 27.0, Xcode 27.0 (Swift 6.4), main @ 1b6befd.

    build.sh. Fails on a fresh machine at the metallib step: cannot execute tool 'metal' due to missing Metal Toolchain. Xcode 26+ ships the Metal toolchain as a separate download, and the script's own hint (xcodebuild -downloadComponent MetalToolchain) is the right fix. After that, ./build.sh takes about 5 minutes end to end and the binary loads a model straight from ~/.cache/huggingface/hub without re-downloading.

    Unit suites. swift build --build-tests takes about 4.5 minutes, then both suites run as in ci.yml:

    suite result wall time
    swift test --skip-build --filter SwiftLMTests --disable-swift-testing 276 passed, 0 failed 6 s
    swift test --skip-build --filter SwiftBuddyTests --disable-swift-testing 127 run, 10 skipped, 0 failed 15 s

    The metallib step no longer seems necessary. SwiftPM compiles the Cmlx kernels itself and puts default.metallib (2.8 MB) into mlx-swift_Cmlx.bundle/Contents/Resources/ inside each .xctest. To check that this is what the tests actually use, I removed the mlx.metallib / default.metallib copies from SwiftLMTests.xctest/Contents/MacOS and re-ran the full suite: still 276/276. So with the Metal toolchain installed, a bare swift build --build-tests && swift test --skip-build is enough here. I did not test what swift build does without the Metal toolchain, because build.sh had already failed earlier in the cmake step on that machine state.

    Two script details, in case the helpers stay around. scripts/install-test-metallib.sh searches .build/*/release/mlx.metallib, but Swift 6.4 writes .build/out/Products/Release/mlx.metallib (.build/release is now a symlink), so it never finds the one build.sh just built and falls through to pip-installing the mlx wheel. scripts/bootstrap_local_tests.sh always takes the pip route. If the bundled metallib is enough, both could reduce to the Metal toolchain check, or the search pattern could gain the out/Products/Release path.

  4. HamedShams commented on Oct 9, 2026

    @HamedShams
    Contributor

    One more data point on the same setup, found right after posting: the manual step from the Aug 29 comment (copy mlx.metallib into SwiftLMTests.xctest/Contents/MacOS/) is now actively harmful on Xcode 27. The suite runs once, but the next incremental swift build --build-tests fails at the CodeSign SwiftLMTests.xctest step and leaves the bundle unsigned, so swift test --skip-build then dies with code signature … not valid for use in process: Trying to load an unsigned library. Deleting the two copied files and rebuilding fixes it (codesign --verify passes again, 280 tests green). So with the Metal toolchain installed, the bundled default.metallib is both sufficient and the only copy that should be there.

  5. solderzzc commented on Oct 9, 2026

    @solderzzc
    MemberAuthor

    Follow-up on the local test setup, in #222 (merged).

    What I verified on Swift 6.4 / Xcode 27 (M-series Mac):

    • SwiftPM compiles the Cmlx kernels and embeds default.metallib in mlx-swift_Cmlx.bundle inside each .xctest, so scripts/install-test-metallib.sh / bootstrap_local_tests.sh do not need to copy or pip-install a metallib there.
    • Copying a metallib into <bundle>.xctest/Contents/MacOS invalidates the bundle signature, and the next incremental swift build --build-tests fails at CodeSign (reproduced).
    • I could not reproduce the "unsigned library" abort from a bare swift test.

    What I did not verify: Swift 6.2 / Xcode 26 (the CI runner) uses the older .build/arm64-apple-macosx/ layout and does not embed the metallib, so the copy is still needed there. My first version of the PR removed it from CI and broke the run; that was corrected before merge.

    So the scripts now detect the Swift 6.4 .build/out layout and only run a new scripts/check-metal-toolchain.sh (a clear error pointing to xcodebuild -downloadComponent MetalToolchain when the Metal Toolchain is missing); older toolchains keep the previous behaviour. The old-layout path is covered by CI only.

  6. HamedShams commented on Oct 9, 2026

    @HamedShams
    Contributor

    Tested and confirmed on my machine (the same M1 Max / Xcode 27) with #222: scripts/install-test-metallib.sh exits with the "nothing to install" msg, scripts/bootstrap_local_tests.sh builds and hands off to the toolchain check, swift test --skip-build is green (280 on my #220 branch, i.e. main’s 276 plus its 4 new tests), and the bundle’s Contents/MacOS holds only the binary.

    I could not reproduce the "unsigned library" abort from a bare swift test.

    One clarification: it never came from a bare swift test here either, only after the manual copy, when the next incremental build failed at CodeSign and left the bundle unsigned. So nothing left to reproduce.

  7. solderzzc commented on Oct 10, 2026

    @solderzzc
    MemberAuthor

    Status summary, as of main @ 562babb.

    Tier Status
    1 — Architecture smoke matrix Done (5 synthetic checkpoint fixtures, per the Aug 29 update).
    2 — API contract tests Done, running as integration_matrix (contract, ...) on every PR. tests/test-contract.sh has since grown with #220 (seeded sampling is reproducible, a negative seed does not kill the server) and #223 (stop as a plain string, a non-decoding body answers 400).
    3 — Make the local suite runnable Done on Swift 6.4 / Xcode 27 (#222). Not verified on Swift 6.2 (see below).
    4 — Discipline Coverage tooling landed (#146). Gemma4.swift and 5 other VLM architectures now have tests (Aug 29 update). The other VLM architectures listed in the baseline table (Gemma3, Pixtral, FastVLM) were not part of that update and have not been re-measured.

    Tier 3, what is and is not established

    • On Swift 6.4, SwiftPM embeds default.metallib in each .xctest, so no copy is needed. This was verified independently on two machines (thanks @HamedShams for the fresh-clone run). Copying a metallib into Contents/MacOS breaks the next incremental build at CodeSign, so scripts/install-test-metallib.sh and scripts/bootstrap_local_tests.sh now skip it on that layout and run scripts/check-metal-toolchain.sh instead, which points to xcodebuild -downloadComponent MetalToolchain when the Metal Toolchain is missing.
    • On Swift 6.2 / Xcode 26 (the CI runner, .build/arm64-apple-macosx/ layout) the metallib is not embedded, so the scripts and ci.yml keep the old copy there. That path is covered by CI only; we have no Swift 6.2 toolchain locally.
    • The original symptom (a bare swift test aborting on the second test) was not reproduced on Swift 6.4. The "unsigned library" abort only appears after the manual copy.

    Still open

    • Whether Swift 6.2 / Xcode 26 users still hit the original abort. If someone on that toolchain can run scripts/bootstrap_local_tests.sh from a fresh checkout and report back, that would close Tier 3 fully.
    • The two open questions from the issue: the CI-minute budget per PR (which sets the small/nightly split for Tier 1), and whether large checkpoints live in a persistent cache or are fetched per run.

    Once those are decided or split into their own issues, this one can be closed.

  8. solderzzc commented on Oct 11, 2026

    @solderzzc
    MemberAuthor

    Data for the two open questions (CI-minute budget, checkpoint caching), from the last 24 successful ci.yml runs.

    Job durations (minutes, median; range in brackets)

    • build_and_unit_test: 27.3 [14.3–33.8]. Every other job needs it.
    • dflash-speculative-decoding: 16.3 [13.2–21.6]; speculative-decoding: 15.6 [10.1–23.1]; speculative-decoding-eval: 15.4 [9.8–25.1]
    • integration_matrix entries: 1.1–4.2 each (omni, opencode, audio, server ≈ 4; vision, hybrid-cache, contract, fixtures, graph ≈ 1–2). ssd-draft-memory-guard: 2.1.
    • Sum of medians ≈ 101 job-minutes; a fully green run takes about 50 minutes wall-clock (max 67), roughly the 27-minute build plus the longest parallel job.

    What that says

    • The Tier 1 / Tier 2 matrix is the cheap part: about 29 minutes in total across 11 entries. The cost is the build and the three speculative/dflash jobs.
    • The repository is public, so these are not billed minutes; the constraint is wait time.
    • Suggested split: keep the current matrix on every PR. If we add the 19–20 GB shapes (Qwen3.6-27B-OptiQ, 35B-A3B), run them nightly or only on submodule-bump PRs, as the issue proposed, rather than on every PR.

    Caching

    • Models are cached per job via actions/cache on ~/.cache/huggingface, keyed on the test script's hash. There are currently 9 entries, about 21.5 GB (largest: opencode and audio at 4.1 GB each). ci.yml assumes a 10 GB per-repository cache budget; I did not check why the usage reported here is above that, so treat both numbers as unverified.
    • That suggests large checkpoints should not go through the Actions cache. Fetching them per nightly run (pinned to a revision, as the LFM2.5 entry already does) is simpler than a persistent cache.

    Caveats: failed and cancelled runs are excluded, and queue time is not subtracted.

  9. solderzzc commented on Oct 11, 2026

    @solderzzc
    MemberAuthor

    Tier 3 on Swift 6.2: verified.

    Setup: a fresh clone of main (7e219b4) with submodules, built with Xcode 26.3 (Apple Swift 6.2.4, swiftlang-6.2.4.1.4) on macOS 27, selected through DEVELOPER_DIR only, so the default toolchain was untouched.

    Step Result
    swift build --build-tests succeeds (234 s), .build/arm64-apple-macosx/ layout, the same one CI uses
    bare swift test aborts on the second test: MLX error: Failed to load the default metallib
    scripts/bootstrap_local_tests.sh succeeds
    swift test --skip-build afterwards 435 tests, 9 skipped, 0 failures

    So on the pre-6.4 layout the original symptom from this issue reproduces, and the bootstrap script resolves it. Together with the Swift 6.4 results above, both toolchains are now covered by a manual run and not only by CI. Tier 3 can be considered closed.

    Also: #226 (--thinking-budget) and the contract tests added since are in; #228 adds contract coverage for finish_reason and stop-sequence token accounting across the streaming and non-streaming paths (it found and fixes a non-streaming path that kept generating past a stop sequence).

    What remains open on this issue is the two decisions: the CI-minute budget per PR, and whether large checkpoints live in a persistent cache or are fetched per run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions