requires-python >=3.14,<3.15 (was <3.14 ceiling): the old cap existed because Rust-backed transitives lacked cp314 wheels; tflite-runtime (openwakeword's hard linux dep) still caps at cp311, so the wake engine moves to pyopen-wakeword 1.1.0 — py3 wheels + bundled tensorflowlite_c lib, whose bundled melspectrogram/embedding models are byte-identical to the openWakeWord v0.5.1 files (verified by sha256), and it loads the shipped hey_hermes.tflite. Drops openwakeword/onnxruntime/ai-edge-litert/wake-tflite machinery entirely. win32-arm64 gets a marker gate (no pyopen-wakeword wheel there; porcupine covers wake). uv.lock regenerated for 3.14 (254 pkgs, all platforms). Real-lib smoke verified: engine builds against the actual wheel, silence scores 0, noise scores ~0.003 (threshold 0.6), scores flow 1:1 per frame.
169 lines
6.1 KiB
Python
169 lines
6.1 KiB
Python
"""Regression tests for packaging metadata in pyproject.toml."""
|
|
|
|
from pathlib import Path
|
|
import tomllib
|
|
|
|
def _load_optional_dependencies():
|
|
pyproject_path = Path(__file__).resolve().parents[1] / "pyproject.toml"
|
|
with pyproject_path.open("rb") as handle:
|
|
project = tomllib.load(handle)["project"]
|
|
return project["optional-dependencies"]
|
|
|
|
|
|
def _load_package_data():
|
|
pyproject_path = Path(__file__).resolve().parents[1] / "pyproject.toml"
|
|
with pyproject_path.open("rb") as handle:
|
|
tool = tomllib.load(handle)["tool"]
|
|
return tool["setuptools"]["package-data"]
|
|
|
|
|
|
def test_matrix_extra_not_in_all():
|
|
"""The [matrix] extra pulls `mautrix[encryption]` -> `python-olm`,
|
|
which has Linux-only wheels and no native build path on Windows or
|
|
modern macOS (archived libolm, C++ errors with Clang 21+).
|
|
|
|
With matrix in [all], `uv sync --locked` on Windows tried to build
|
|
python-olm from sdist and failed on `make`. As of 2026-05-12 the
|
|
[matrix] extra is excluded from [all] entirely and installs on first
|
|
use (pm.ensure_import("matrix")), where the user is expected to have
|
|
a toolchain.
|
|
"""
|
|
optional_dependencies = _load_optional_dependencies()
|
|
|
|
assert "matrix" in optional_dependencies, "[matrix] extra must still exist for `uv sync --extra matrix`"
|
|
# Must NOT appear in [all] in any form — neither unconditional nor
|
|
# platform-gated. Lazy-install handles it.
|
|
matrix_in_all = [
|
|
dep for dep in optional_dependencies["all"]
|
|
if "matrix" in dep
|
|
]
|
|
assert not matrix_in_all, (
|
|
"matrix must not appear in [all] — it installs on first use via "
|
|
f"pm.ensure_import('matrix'). Found: {matrix_in_all}"
|
|
)
|
|
|
|
|
|
def test_lazy_installable_extras_excluded_from_all():
|
|
"""Policy (2026-05-12): opt-in backends stay out of [all].
|
|
|
|
On-demand install exists so one quarantined PyPI release
|
|
(e.g. mistralai 2.4.6) can't break every fresh install. Putting a
|
|
backend in [all] defeats that — fresh installs eager-install it and
|
|
inherit whatever's broken upstream. Opt-in backends are extras that
|
|
install at first use via pm.ensure_import(extra).
|
|
"""
|
|
optional_dependencies = _load_optional_dependencies()
|
|
|
|
# The on-demand backends as of 2026-05-12. Deliberately a literal
|
|
# list so the test stays a contract — adding a new opt-in backend
|
|
# means updating this list AND verifying [all] doesn't contain it.
|
|
lazy_covered_extras = {
|
|
"anthropic", "bedrock",
|
|
"exa", "firecrawl", "parallel-web",
|
|
"fal",
|
|
"edge-tts", "tts-premium",
|
|
"voice", # faster-whisper / sounddevice / numpy (composes stt-whisper + audio-io)
|
|
"stt-whisper",
|
|
"modal", "daytona", "vercel",
|
|
"messaging", "slack", "matrix", "dingtalk", "feishu",
|
|
"telegram", "discord",
|
|
"wake", "wake-openwakeword", "wake-sherpa", "wake-porcupine",
|
|
"google-chat",
|
|
"honcho", "hindsight",
|
|
"supermemory", "mem0",
|
|
"mistral", # mistralai — Voxtral STT/TTS, lazy-installed (stt.mistral / tts.mistral)
|
|
}
|
|
all_extra_specs = optional_dependencies["all"]
|
|
for extra in lazy_covered_extras:
|
|
offending = [
|
|
spec for spec in all_extra_specs
|
|
if f"hermes-agent[{extra}]" in spec
|
|
]
|
|
assert not offending, (
|
|
f"[{extra}] is in [all] but also in LAZY_DEPS. "
|
|
f"Remove it from [all] in pyproject.toml — it lazy-installs "
|
|
f"at first use. Found in [all]: {offending}"
|
|
)
|
|
|
|
|
|
def _exact_pins(specs):
|
|
pins = {}
|
|
for spec in specs:
|
|
requirement = spec.split(";", 1)[0].strip()
|
|
if "==" not in requirement:
|
|
continue
|
|
package, version = requirement.split("==", 1)
|
|
package = package.split("[", 1)[0].lower().replace("_", "-")
|
|
pins[package] = version
|
|
return pins
|
|
|
|
|
|
|
|
|
|
def test_extras_pin_each_package_at_one_version():
|
|
"""One package, one version, across every extra.
|
|
|
|
tools/lazy_deps.py is gone — pyproject.toml is the single authority for
|
|
optional-dependency pins (pm syncs the venv from uv.lock, which resolves
|
|
from here). The drift class that killed us before (#31817: two documents
|
|
pinning the same package differently, update ping-ponging the version)
|
|
is now only possible BETWEEN extras — so pin consistency across extras
|
|
is the whole remaining contract.
|
|
"""
|
|
optional_dependencies = _load_optional_dependencies()
|
|
|
|
pins: dict[str, dict[str, set[str]]] = {}
|
|
for extra, specs in optional_dependencies.items():
|
|
for package, version in _exact_pins(specs).items():
|
|
pins.setdefault(package, {}).setdefault(version, set()).add(extra)
|
|
|
|
drift = {
|
|
package: {v: sorted(extras) for v, extras in versions.items()}
|
|
for package, versions in pins.items()
|
|
if len(versions) > 1
|
|
}
|
|
assert not drift, (
|
|
"extras pin the same package at different versions — uv sync would "
|
|
f"resolve whichever wins and silently downgrade the other: {drift}"
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_dingtalk_extra_includes_qrcode_for_qr_auth():
|
|
"""DingTalk's QR-code device-flow auth (hermes_cli/dingtalk_auth.py)
|
|
needs the qrcode package."""
|
|
optional_dependencies = _load_optional_dependencies()
|
|
|
|
dingtalk_extra = optional_dependencies["dingtalk"]
|
|
assert any(dep.startswith("qrcode") for dep in dingtalk_extra)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _uv_lock_version(package: str) -> str:
|
|
"""Resolved version of ``package`` in uv.lock, or fail loudly."""
|
|
versions = _uv_lock_versions(package)
|
|
assert versions, f"{package} not found in uv.lock"
|
|
assert len(versions) == 1, f"{package} resolves to multiple versions in uv.lock: {versions}"
|
|
return next(iter(versions))
|
|
|
|
|
|
def _uv_lock_versions(package: str) -> set[str]:
|
|
"""All resolved versions of ``package`` in uv.lock (normally 0 or 1)."""
|
|
import re
|
|
|
|
lock_path = Path(__file__).resolve().parents[1] / "uv.lock"
|
|
lock = lock_path.read_text(encoding="utf-8")
|
|
return {
|
|
m.group(1)
|
|
for m in re.finditer(
|
|
rf'\[\[package\]\]\nname = "{re.escape(package)}"\nversion = "([^"]+)"',
|
|
lock,
|
|
)
|
|
}
|