Skip to content

fix(tests+ci): test-credibility and quality fixes - #132

Merged
chazmaniandinkle merged 21 commits into
mainfrom
fix/test-credibility-quality
Jul 6, 2026
Merged

fix(tests+ci): test-credibility and quality fixes#132
chazmaniandinkle merged 21 commits into
mainfrom
fix/test-credibility-quality

Conversation

@chazmaniandinkle

Copy link
Copy Markdown
Contributor

Summary

Batch of test-credibility and quality fixes audited against main. All 6 scope items addressed.

1. CI workflow (ci.yml) — remove || echo "No tests yet" masking

  • Removed the || echo "No tests yet" fallback that swallowed collection errors and failures silently (badge was green even when 0 tests ran).
  • Replaced pip install -r requirements.txt (which includes Apple-Silicon-only libs that fail on ubuntu-latest) with an explicit CI-safe install list: fastapi, uvicorn, httpx, pysbd, numpy, scipy, pydantic, pytest, pytest-asyncio, python-multipart, mcp, mistral-common.
  • Tests that need mlx/torch/sounddevice skip via HAS_MLX/HAS_MCP guards (conftest.py) rather than erroring.

2. test_version.py — 2 missing vad stub attrs

  • Added is_pipecat_vad_available and voice_confidence to the _stub_heavy_deps() vad stub. http_api.py imports both at module level; their absence caused ImportError on setup for the two health/capabilities endpoint tests every run.

3. pyproject.toml[tool.pytest.ini_options] testpaths = ["tests"]

  • Without this, pytest walked the worktree root and vendor/ dirs during collection.

4. conftest.py — sys.modules stub isolation (save/restore pattern)

  • New restore_sys_modules fixture saves/restores sys.modules around each test. test_version.py's _stub_heavy_deps() was permanently injecting fake engine/vad/mlx/etc. modules, causing ~28F/42E in combined runs when test_engine.py (and others) later imported the real modules.
  • conftest.py also installs CI-mode stubs for torch, sounddevice, mcp (with all required submodules), mlx, etc. at session start so that collection always succeeds on Linux CI.
  • Added HAS_MLX, HAS_MCP, HAS_TORCH booleans (checked BEFORE stubs are installed) so importorskip-equivalent guards in individual test files reflect real availability, not whether a stub exists.
  • Applied HAS_MLX module-level skips to: test_voice_profiles.py, test_voice_profiles_e2e.py, test_voice_profiles_api.py, test_voice_profile_curation.py, test_compositions_api.py.
  • Applied HAS_MCP (module-level skip or @needs_mcp class/function marker) to: test_speaking_lock.py, test_mcp_route.py, test_speech_queue.py (3 classes), test_dashboard_chat.py (1 class), test_bargein_provider_registry.py (3 functions).

5. engine.py — log WARNING when speed= is silently ignored

  • chatterbox, chatterbox-turbo, and voxtral accept the speed parameter but discard it at synthesis time. Added a single WARNING per generate_audio() call (outside the per-sentence loop) when speed != 1.0 and the engine doesn't support it. Engines with supports_speed=True and spark are unaffected.

6. CHANGELOG.md — breaking change entry

Test plan

  • Collection: pytest tests/ --collect-only shows 889 tests, 0 collection errors
  • pytest tests/ -q runs honestly green (no || echo fallback suppressing failures)
  • test_version.py: all 4 tests pass
  • test_engine.py: all 4 tests pass (no stub pollution from test_version.py)
  • mlx-gated files (test_voice_profiles.py, test_voice_profiles_e2e.py, test_voice_profiles_api.py, test_voice_profile_curation.py, test_compositions_api.py): skip cleanly with mlx not available reason
  • mcp-gated tests (test_speaking_lock.py, test_mcp_route.py, skip classes in test_speech_queue.py etc.): skip cleanly
  • CI workflow: pytest tests/ -v --tb=short exits non-zero on failures (no masking)

@chazmaniandinkle

Copy link
Copy Markdown
Contributor Author

Verification note: full suite on Darkstar = 940 passed / 10 failed, with all 10 failures reproduced identically on origin/main (pre-existing, machine-environment: kernel_session_callback ×6, rtvi lifecycle ×2, audio_subscribers disconnect, stt-executor timing). The kernel_session_callback cluster is the same seam the 2026-06-10 boundary audit ranked D1-critical — live test evidence for that finding.

Also note for reviewer: this branch carries the two VAD commits (feat/vad-confidence-endpoint work) via clone lineage — operator call whether to bless (completes the prod-back-to-main provenance item) or re-cut before merge.

@chazmaniandinkle

Copy link
Copy Markdown
Contributor Author

Correction + root causes for the 10 pre-existing failures (from the long-running fix agent's diagnosis, verified against both branch and main):

  1. kernel_session_callback ×6: NOT seam behavior failures — the test fixture omits base_url="http://localhost:7860", so the CSRF guard rejects the testserver Host header before the callback logic runs. One-line fixture fix. This corrects my earlier comment: these tests are evidence the D1 registration seam is UNTESTED, not evidence it misbehaves.
  2. audio_subscribers / rtvi hangs: tests pass; the process fails to exit due to WebSocket TestClient thread cleanup — this is what made full-suite runs appear to hang. Pre-existing infrastructure issue.
  3. stt_executor_isolation: timing-flaky on loaded machines, fails on main too.

All three are cheap follow-ups, queued for tomorrow. None block this PR.

…ndpoint

Adds a lightweight per-packet VAD endpoint for barge-in loops (Discord VC,
etc.) that do not need full utterance-level speech detection.

Why:
- /v1/vad accepts a WAV multipart upload and runs the full Silero torch path.
  That's the right API for utterance VAD but overkill for 512-sample frames
  arriving at 20ms intervals from a Discord SocketReader thread.
- The vendored pipecat SileroVADAnalyzer (vendor/pipecat_vad/) uses ONNX,
  has no torch dependency, returns per-frame confidence in <5ms, and was
  already present in main but had no HTTP surface.

Contract:
  POST /v1/vad/confidence[?sample_rate=16000]
  Body: raw int16 little-endian PCM (exactly 512 samples @ 16kHz or 256 @ 8kHz)
  Response: {confidence: float, available: bool, latency_ms: float}

  available=false (and confidence=0.0) when onnxruntime is not installed.
  Empty body returns {confidence: 0.0, available: <bool>, latency_ms: 0.0}.

Also:
- Imports is_pipecat_vad_available + voice_confidence from vad.py (already in main)
- Listed in GET /capabilities endpoints manifest as 'vad_confidence'
- 12 tests covering happy path, unavailable path, empty body, sample_rate
  param forwarding, capabilities manifest

Updates: fix mod3/http_api.py file header comment and endpoints dict.
The model __call__ returns shape (batch, frames). With batch_size=1
and a single frame, out[0] is shape (1,) — a 1D array. float() on a
1D array raises 'only 0-dimensional arrays can be converted to Python
scalars', so voice_confidence() was silently returning 0.0 on every
frame. VAD was effectively disabled despite reporting available=true.

Fix: float(np.squeeze(new_confidence).item()) — works for any batch
size and output shape.

Root cause found via direct venv test after the HTTP endpoint showed
0.0 confidence on sine waves and noise. The error was swallowed by the
bare except in voice_confidence().
…o vad stub

_stub_heavy_deps() in test_version.py built a vad stub module but omitted the
two attributes that http_api imports at module level (the F2 pipecat wrapper),
causing ImportError on setup for test_health_version_matches_package_version
and test_capabilities_version_matches_package_version on every run.
…st_version.py

test_version.py's _stub_heavy_deps() injected fake engine/vad/numpy/mlx
modules into sys.modules without restoring them. Any test file collected
after test_version.py got the stub engine instead of the real module, causing
~28F/42E in combined runs. Added conftest.py with a restore_sys_modules
fixture (save-and-restore pattern) and changed http_app in test_version.py
to request it, scoping it down from module to function so each test is
fully isolated.
Without testpaths, pytest defaulted to collecting from the worktree root,
picking up vendor/, integrations/, and production modules as potential test
sources, causing spurious collection errors. Pin discovery to tests/.
…-lib test files

conftest.py now:
  - Checks real lib availability (HAS_MLX, HAS_MCP, HAS_TORCH) before any
    stubs are installed, so importorskip checks reflect genuine presence.
  - Installs lightweight MagicMock stubs for torch, sounddevice, mcp, mlx, etc.
    when absent so module-level imports in production code don't abort
    collection on linux/CI.

test_voice_profiles.py, test_voice_profiles_e2e.py: skip via HAS_MLX check
before the hard mlx_audio module-level imports rather than importorskip (which
would succeed against the stub).

test_speaking_lock.py, test_mcp_route.py: skip via HAS_MCP before the hard
server import that requires mcp at module level.
The test job was running pytest with || echo 'No tests yet', which swallowed
collection errors and failures silently — the badge stayed green even when 0
tests ran or the entire collection errored.

Changes:
- Remove the || echo fallback; pytest exit code now propagates as-is.
- Replace pip install -r requirements.txt (which includes Apple-Silicon-only
  libs that fail to install on ubuntu-latest) with an explicit CI-safe list:
  fastapi, uvicorn, httpx, pysbd, numpy, scipy, pydantic, pytest,
  pytest-asyncio, python-multipart, mcp, mistral-common.
- Tests needing mlx/torch/sounddevice skip via HAS_MLX / HAS_MCP guards
  (conftest.py) rather than erroring — collection always succeeds and the
  job is honestly green.
…x, chatterbox-turbo, voxtral

These engines accept speed as a parameter signature but discard it at synthesis
time (only Kokoro and Spark honour supports_speed). Callers passing speed != 1.0
now receive a single WARNING per generate_audio() call so the silent drop is
visible in logs and operator tooling.

Warning fires once per call (outside the per-sentence loop) to avoid log spam.
Engines with supports_speed=True and spark (which maps speed to a discrete set)
are unaffected.
…king change

The rename happened silently in commit 65cf1b5 (PR #129) with no changelog
entry. Callers reading queue.depth to check backlog size need to update to
queue.jobs_total.
…d by CI stubs

Five test files had tests calling @mcp.tool-decorated server functions or
mlx_audio internals at call time, which become MagicMock under CI stubs:

- test_speech_queue.py: @needs_mcp on TestSpeakReturnFormat,
  TestStopReturnFormat, TestSpeechStatusReturnFormat (call server.speak/stop/
  speech_status which are @mcp.tool decorated).
- test_dashboard_chat.py: @needs_mcp on TestMod3DashboardPostTool.
- test_bargein_provider_registry.py: @needs_mcp on the three
  test_await_voice_input_* functions that import server.
- test_voice_profiles_api.py: HAS_MLX module-level skip (calls
  _make_synthetic_conditionals which imports mlx_audio.tts.models.chatterbox).
- test_voice_profile_curation.py: same HAS_MLX module-level skip.
…positions_api

conftest.py: add mcp.shared.message to the submodule stub list so that
channel_client.py can be exec_module'd in tests (it imports from mcp.server.stdio
and mcp.shared.message at module level).

test_compositions_api.py: _make_synthetic_conditionals() imports mlx_audio at
call time; skip the whole module via HAS_MLX guard like the other mlx-dependent
test files.
…MagicMock

test_version_module_matches_pyproject: try tomllib (3.11+ stdlib), fall
back to regex parse of pyproject.toml on 3.10 — no tomli dependency.

_stub_heavy_deps: stub only third-party/native packages. Stubbing local
pure-Python modules (bus, modality, session_registry, audio_subscribers)
with bare MagicMock poisoned server.diagnostics() with non-JSON-serializable
values when tests shared a session. Documented the rule in the docstring.

Full suite on this machine: 940 passed, 10 failed — all 10 verified
PRE-EXISTING on origin/main (kernel_session_callback, rtvi lifecycle,
stt-executor timing; machine-environment dependent). Hang reports during
verification were three concurrent pytest sessions deadlocking, not a test.
…guard

- ruff I001: sort imports in http_api.py (vad import), tests/conftest.py,
  and tests/test_bargein_provider_registry.py
- ci.yml: restore soundfile to both pip-install steps (test-import and
  test-api jobs); encode_ogg/compose endpoints need it and tests were
  going red once the "|| echo No tests yet" mask was removed
- tests/test_voice_profile_compose_api.py: add the HAS_MLX skip-guard
  used by its sibling voice-profile test files so it skips cleanly
  instead of collection-erroring under the CI dependency set
@chazmaniandinkle
chazmaniandinkle force-pushed the fix/test-credibility-quality branch from c261de4 to 33c6c03 Compare July 6, 2026 12:50
The HTTP API Smoke Test job installs a CI-safe dependency subset that
deliberately excludes torch (matching the Import & Unit Tests job's
comment on Apple-Silicon-only/native libs). Its bare `python -c` script
never loads tests/conftest.py, so the module-level `import torch` in
vad.py had no stub to fall back on and crashed the import chain
(http_api -> vad -> torch) with ModuleNotFoundError.

Make the torch import lazy/optional in vad.py, following the same
HAS_MLX/HAS_TORCH graceful-degradation convention already used
elsewhere in the test suite: _get_model() now raises a clear
RuntimeError only if Silero model loading is actually attempted
without torch installed, instead of failing at import time.

Also fixes Lint & Type Check:
- ruff check: drop two unused imports (MagicMock, patch) and reorder
  import blocks in tests/test_vad_confidence_endpoint.py
- ruff format --check: reformat 4 more files (conftest.py,
  test_bargein_provider_registry.py, test_dashboard_chat.py,
  test_version.py) that had drifted from ruff's formatter in earlier
  commits on this branch; origin/main formats clean, confirming this
  drift was introduced by this branch and not pre-existing
- pyright: relax reportOptionalMemberAccess for vad.py's existing
  pyright suppression comment, since the try/except torch import
  makes `torch` Optional from pyright's view

Verified locally against the exact CI toolchain (ruff 0.15.20, pyright
1.1.411, Python 3.12) and by reproducing the smoke test's dependency
subset (no torch) to confirm http_api now imports and serves cleanly.
…dshake

The ws_audio handler's disconnect-bot path broke out of its loop straight
into finally: subs.unregister(...) without ever calling
await websocket.close(), so no close frame was sent. Worse, a
disconnect-bot frame arriving before the RTVI handshake completed was
silently ignored and fell through into the drain loop's receive(), which
then blocked forever since the client sent nothing further.

Guard close() with an application_state CONNECTED check (matching the
pattern in the existing RTVI version-mismatch close a few lines up) so
it's a no-op if the socket is already closing, and return/unregister
still happens via finally.

Fixes the hang in
tests/test_audio_subscribers.py::TestDisconnectBot::test_disconnect_bot_closes_and_unregisters
that was blocking the Import & Unit Tests CI job.
tests/test_kernel_session_callback.py's mutating client.post/delete calls
never overrode the default "Host: testserver" TestClient sends, so the
app's _localhost_csrf_guard middleware correctly 403'd every one of them
before reaching the handler. Adopt the same _GOOD_HOST header pattern
already established in test_localhost_csrf_guard.py.
mlx_lm is Apple-Silicon/Metal-only and is intentionally excluded from
the Import & Unit Tests CI job's Linux install list, so a bare
`import mlx_lm` raised ModuleNotFoundError instead of skipping cleanly.
Gate TestMlxLmTokenizerRegistration with the existing skip_without_mlx
marker, matching the convention already used for the rest of the mlx
stack in conftest.py.
test_slow_stt_does_not_block_default_pool imported the shared, process-
global channels._STT_EXECUTOR by reference. Any earlier test file in the
same pytest process that runs `with TestClient(app) as c:` triggers the
app's full lifespan teardown, which shuts down that same executor for
real via shutdown_stt_executor(). Depending on collection order, the
executor was already dead by the time this test ran, raising
"cannot schedule new futures after shutdown".

Give the test its own throwaway ThreadPoolExecutor instance instead,
mirroring the pattern test_shutdown_stt_executor_is_safe already uses,
so the isolation assertion no longer depends on the shared singleton's
lifecycle.
test_synthesize_with_subscriber_emits_rtvi_over_ws exercises the real
POST /v1/synthesize endpoint with no engine mocking, requiring an actual
TTS backend and model weights that the Linux test-import job never
installs. The test already carries a SKIP_TTS_TESTS-gated skipif guard,
but the workflow never set that env var, so the guard was dead code.
Wire SKIP_TTS_TESTS=1 into the job's test step.
The /health endpoint's queue.jobs_total field is reverted back to
queue.depth, restoring consistency with origin/main and with
server.py and output_queue.py, which both still emit "depth" for
the same concept. The rename was unrelated to this PR's actual
scope (test-suite credibility and CI honesty) and had no test
coverage depending on it.

CHANGELOG.md's entry documenting the breaking rename is replaced
with an accurate entry describing this PR's real change: unmasking
the test suite and fixing/skipping the pre-existing failures it had
been hiding.
@chazmaniandinkle
chazmaniandinkle merged commit ff3e1a3 into main Jul 6, 2026
7 checks passed
@chazmaniandinkle
chazmaniandinkle deleted the fix/test-credibility-quality branch July 6, 2026 14:45
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