Files
hermes-agent/tests/test_project_metadata.py
ethernet 6d344be588 fix(wake): upgrade sherpa for native Windows ARM64
Sherpa 1.13.8 supplies native Windows ARM64 Python and core wheels.
Remove its platform exclusion so auto selects the keyless engine there.

Declare pypinyin for both wake install paths. The tokenizer imports it
unconditionally, including for English phrases. Include sentencepiece
in the lazy engine extra too. Keep the matching core in the lockfile
and admit the reviewed release with bounded package cutoffs.

Verified native ARM64 Python 3.14 with the unchanged Hermes engine:
three phrases detected twice each, with no triggers in 180 seconds of
silence. Engine construction failed without pypinyin and passed with it.
The tested wheels match the lockfile hashes. Focused tests: 78 passed,
2 skipped. uv lock --check passed. No MSIX rebuild or microphone test.

The dependency audit reports the same existing httpx2/httpcore2
advisories as the parent commit. No unrelated packages changed.
2026-09-11 16:30:47 -04:00

226 lines
9.3 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_wake_dependencies_and_runtime_gate_agree_on_supported_targets():
from packaging.markers import Marker, default_environment
from packaging.requirements import Requirement
root = Path(__file__).resolve().parents[1]
metadata = tomllib.loads((root / "pyproject.toml").read_text(encoding="utf-8-sig"))
optional = metadata["project"]["optional-dependencies"]
gates = metadata["tool"]["hermes"]["extras-platforms"]
targets = [
("darwin", "Darwin", "x86_64", False),
("darwin", "Darwin", "arm64", True),
("linux", "Linux", "x86_64", True),
("linux", "Linux", "aarch64", True),
("win32", "Windows", "AMD64", True),
("win32", "Windows", "ARM64", False),
]
for system, platform_system, machine, supported in targets:
environment = {**default_environment(), "sys_platform": system,
"platform_system": platform_system, "platform_machine": machine}
for extra in ("wake", "wake-openwakeword"):
requirement = next(req for spec in optional[extra]
if (req := Requirement(spec)).name == "pyopen-wakeword")
assert requirement.marker.evaluate(environment) is supported, (extra, system, machine)
assert Marker(gates["wake-openwakeword"]).evaluate(environment) is supported
selected = {req.name for spec in optional["wake"]
if (req := Requirement(spec)).marker is None or req.marker.evaluate(environment)}
assert {"sherpa-onnx", "pvporcupine", "sentencepiece", "pypinyin"} <= selected
sherpa_deps = {req.name for spec in optional["wake-sherpa"]
if (req := Requirement(spec)).marker is None or req.marker.evaluate(environment)}
assert {"sherpa-onnx", "sentencepiece", "pypinyin"} <= sherpa_deps
for extra in ("wake-sherpa", "wake-porcupine"):
assert all(req.marker is None or req.marker.evaluate(environment)
for req in map(Requirement, optional[extra]))
gate = gates.get(extra)
assert gate is None or Marker(gate).evaluate(environment)
def test_direct_overrides_preserve_the_declared_exact_version():
from packaging.requirements import Requirement
root = Path(__file__).resolve().parents[1]
metadata = tomllib.loads((root / "pyproject.toml").read_text(encoding="utf-8-sig"))
locked = tomllib.loads((root / "uv.lock").read_text(encoding="utf-8-sig"))
direct = {req.name: req for spec in metadata["project"]["dependencies"]
if (req := Requirement(spec)).specifier}
for spec in metadata["tool"]["uv"].get("override-dependencies", []):
override = Requirement(spec)
requirement = direct.get(override.name)
if requirement is None or not any(s.operator == "==" for s in requirement.specifier):
continue
assert override.specifier == requirement.specifier, (
f"{override.name}: override {override.specifier} contradicts exact requirement {requirement.specifier}"
)
versions = {row["version"] for row in locked["package"] if row["name"] == override.name}
assert versions and all(version in requirement.specifier for version in versions)
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-sig")
return {
m.group(1)
for m in re.finditer(
rf'\[\[package\]\]\nname = "{re.escape(package)}"\nversion = "([^"]+)"',
lock,
)
}