fix(tests+ci): test-credibility and quality fixes - #132
Conversation
|
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. |
|
Correction + root causes for the 10 pre-existing failures (from the long-running fix agent's diagnosis, verified against both branch and main):
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.
…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
c261de4 to
33c6c03
Compare
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.
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|| echo "No tests yet"fallback that swallowed collection errors and failures silently (badge was green even when 0 tests ran).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.HAS_MLX/HAS_MCPguards (conftest.py) rather than erroring.2.
test_version.py— 2 missing vad stub attrsis_pipecat_vad_availableandvoice_confidenceto the_stub_heavy_deps()vad stub.http_api.pyimports both at module level; their absence causedImportErroron setup for the two health/capabilities endpoint tests every run.3.
pyproject.toml—[tool.pytest.ini_options] testpaths = ["tests"]4.
conftest.py— sys.modules stub isolation (save/restore pattern)restore_sys_modulesfixture saves/restoressys.modulesaround each test.test_version.py's_stub_heavy_deps()was permanently injecting fakeengine/vad/mlx/etc. modules, causing ~28F/42E in combined runs whentest_engine.py(and others) later imported the real modules.conftest.pyalso installs CI-mode stubs fortorch,sounddevice,mcp(with all required submodules),mlx, etc. at session start so that collection always succeeds on Linux CI.HAS_MLX,HAS_MCP,HAS_TORCHbooleans (checked BEFORE stubs are installed) soimportorskip-equivalent guards in individual test files reflect real availability, not whether a stub exists.HAS_MLXmodule-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.HAS_MCP(module-level skip or@needs_mcpclass/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 ignoredchatterbox,chatterbox-turbo, andvoxtralaccept thespeedparameter but discard it at synthesis time. Added a single WARNING pergenerate_audio()call (outside the per-sentence loop) whenspeed != 1.0and the engine doesn't support it. Engines withsupports_speed=Trueandsparkare unaffected.6.
CHANGELOG.md— breaking change entryGET /healthqueue.depth→queue.jobs_totalrename (commit65cf1b5, PR fix(queue): harden drain threads against BaseException #129) as a breaking change with migration note.Test plan
pytest tests/ --collect-onlyshows 889 tests, 0 collection errorspytest tests/ -qruns honestly green (no|| echofallback suppressing failures)test_version.py: all 4 tests passtest_engine.py: all 4 tests pass (no stub pollution from test_version.py)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 withmlx not availablereasontest_speaking_lock.py,test_mcp_route.py, skip classes in test_speech_queue.py etc.): skip cleanlypytest tests/ -v --tb=shortexits non-zero on failures (no masking)