Provider RESOLUTION ran an installer. `_detect_local_backend` called `_try_lazy_install_stt` -> `pm.ensure_import`, and the TTS requirement entries called the SDK importers, which install their extra on import. Both sit in status paths: `wake.status` fires on every desktop gateway-open, `/voice status` and `GET /api/audio/voice-config` resolve the same chain, and `check_tts_requirements` is the `text_to_speech` tool's `check_fn`, so it runs on every tool listing. The install is a whole-environment rebuild (a fresh generation, `--all-packages` over the enabled set) under the per-install lock, so a passive probe could hold a sibling profile's backend off its port for tens of seconds — the `hermes-setup` onboarding backend gave up at 40 s while an unrelated status probe's rebuild held the lock. Availability now reads state: `pm.extras.available` for the extra (pm owns the extra->anchor table, counts sys.modules fakes, and honours platform gates), `find_spec` for the rest. A provider whose SDK installs at first use still counts as READY when lazy installs are allowed and its own credential and platform gate permit — otherwise the tool would vanish from the schema for exactly the users that install serves. Synthesis keeps its on-demand install, and the lazy STT path now asks for `stt-whisper`, the extra carrying only faster-whisper. Validated: tests/tools/ for the TTS/STT suites (test_tts_mistral's importable-SDK contract included). A/B on HEAD: the two new files' non-guard tests fail there (3 + 1), pass here; the guard test that transcription still installs passes on both.
50 lines
1.8 KiB
Python
50 lines
1.8 KiB
Python
"""A failed uv-cache seed must not record completion.
|
|
|
|
Regression: the seed copy ran inside ``except OSError: pass`` and wrote the ``.seeded`` marker
|
|
unconditionally, so a copy that died half-way was permanent — no later install retried it, and
|
|
every offline sync that needed a missing entry failed closed with "requested data wasn't found
|
|
in the cache".
|
|
"""
|
|
|
|
import shutil
|
|
|
|
import pytest
|
|
|
|
from hermes_constants import get_default_hermes_root
|
|
from pm import packages
|
|
from pm import paths
|
|
|
|
|
|
@pytest.fixture
|
|
def payload_cache(tmp_path, monkeypatch):
|
|
"""A sealed payload shipping ``uv-cache/`` beside its store, and a machine cache that is empty."""
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "home"))
|
|
payload = tmp_path / "payload"
|
|
entry = payload / "uv-cache" / "archive-v0" / "bucket"
|
|
entry.mkdir(parents=True)
|
|
(entry / "module.py").write_text("seeded = True\n", encoding="utf-8")
|
|
store = payload / "tools"
|
|
store.mkdir()
|
|
monkeypatch.setattr(paths, "store_root", lambda: store)
|
|
return get_default_hermes_root() / "cache" / "uv"
|
|
|
|
|
|
def test_partial_seed_is_retried_on_the_next_install(payload_cache, monkeypatch):
|
|
real_copytree = shutil.copytree
|
|
attempts: list[int] = []
|
|
|
|
def flaky(*args, **kwargs):
|
|
"""Fails the first copy only; shutil recurses through this same module attribute."""
|
|
attempts.append(1)
|
|
if len(attempts) == 1:
|
|
raise OSError("no space left on device")
|
|
return real_copytree(*args, **kwargs)
|
|
|
|
monkeypatch.setattr(shutil, "copytree", flaky)
|
|
packages.uv_cache_dir()
|
|
assert not (payload_cache / ".seeded").is_file(), "a failed seed recorded completion"
|
|
|
|
packages.uv_cache_dir()
|
|
assert (payload_cache / ".seeded").is_file()
|
|
assert (payload_cache / "archive-v0" / "bucket" / "module.py").read_text(encoding="utf-8") == "seeded = True\n"
|