mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-17 09:41:58 +00:00
test(deps): guard @assistant-ui cluster on one tap version
Lockfile invariant that would have caught the desktop build break: the single hoisted @assistant-ui/tap must satisfy every @assistant-ui/* package's declared tap requirement (deps or non-optional peer). It is a contract, not a snapshot -- no hardcoded versions -- so it stays green across routine bumps but fails the moment the cluster splits its tap requirement again.
This commit is contained in:
parent
eae3836eb6
commit
f02484feba
1 changed files with 141 additions and 0 deletions
141
tests/test_assistant_ui_tap_compat.py
Normal file
141
tests/test_assistant_ui_tap_compat.py
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
"""Invariant: the @assistant-ui dependency cluster agrees on one tap version.
|
||||
|
||||
The Hermes desktop app (``apps/desktop``) is built from source on every
|
||||
install/update via ``scripts/install.ps1`` → ``npm ci``/``npm install`` →
|
||||
``tsc -b && vite build``. The ``@assistant-ui`` packages share an internal
|
||||
reactivity lib, ``@assistant-ui/tap``, and they only interoperate when they
|
||||
all resolve the *same* tap version:
|
||||
|
||||
* ``@assistant-ui/react@0.12.28`` and ``@assistant-ui/core`` pin
|
||||
``@assistant-ui/tap@^0.5.x`` (which exports ``.`` and ``./react``).
|
||||
* ``@assistant-ui/store@0.2.18`` bumped its tap peer to ``^0.9.0`` and started
|
||||
importing ``@assistant-ui/tap/react-shim`` — an entry point that only exists
|
||||
in the tap ``0.9.x`` line.
|
||||
|
||||
Because ``react@0.12.28`` requests ``store@^0.2.9`` (a caret range), a fresh
|
||||
install silently floated ``store`` up to ``0.2.18``, which then could not find
|
||||
``./react-shim`` in the hoisted ``tap@0.5.x`` and crashed ``vite build`` with::
|
||||
|
||||
"./react-shim" is not exported ... from package @assistant-ui/tap
|
||||
|
||||
i.e. the opaque "apps/desktop build failed (exit 1)" every user hit when
|
||||
updating. The fix pins ``@assistant-ui/store`` (via root ``overrides``) to the
|
||||
last release that targets ``tap@^0.5.x``.
|
||||
|
||||
This is a *contract* test, not a snapshot: it does not assert specific version
|
||||
numbers, only that whatever tap the lockfile hoists satisfies every
|
||||
``@assistant-ui/*`` package's declared tap requirement. It fails if any future
|
||||
bump reintroduces a split tap requirement across the cluster.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
TAP = "@assistant-ui/tap"
|
||||
|
||||
|
||||
def _caret_satisfies(version: str, spec: str) -> bool:
|
||||
"""Minimal npm semver check for the ranges this cluster actually uses.
|
||||
|
||||
Supports exact versions, ``^x.y.z`` (with correct 0.x semantics), and
|
||||
``||`` unions. Pre-release tags are ignored (none are used here).
|
||||
"""
|
||||
|
||||
def parse(v: str) -> tuple[int, int, int]:
|
||||
core = v.lstrip("^~>=<v ").split("-")[0].split("+")[0]
|
||||
parts = (core.split(".") + ["0", "0", "0"])[:3]
|
||||
return tuple(int(p) for p in parts) # type: ignore[return-value]
|
||||
|
||||
ver = parse(version)
|
||||
for clause in spec.split("||"):
|
||||
clause = clause.strip()
|
||||
if not clause:
|
||||
continue
|
||||
if clause.startswith("^"):
|
||||
lo = parse(clause)
|
||||
if ver < lo:
|
||||
continue
|
||||
major, minor, _ = lo
|
||||
if major > 0:
|
||||
hi = (major + 1, 0, 0)
|
||||
elif minor > 0:
|
||||
hi = (0, minor + 1, 0)
|
||||
else:
|
||||
hi = (0, 0, lo[2] + 1)
|
||||
if ver < hi:
|
||||
return True
|
||||
elif clause[0].isdigit() or clause.startswith("v"):
|
||||
if ver == parse(clause):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _lock_packages() -> dict:
|
||||
lock_path = REPO_ROOT / "package-lock.json"
|
||||
if not lock_path.exists():
|
||||
pytest.skip("package-lock.json not materialized in this CI shard")
|
||||
with lock_path.open("r", encoding="utf-8") as fh:
|
||||
return json.load(fh).get("packages", {})
|
||||
|
||||
|
||||
def _hoisted_tap_version(packages: dict) -> str:
|
||||
entry = packages.get(f"node_modules/{TAP}")
|
||||
assert entry is not None, (
|
||||
"package-lock.json has no hoisted node_modules/@assistant-ui/tap "
|
||||
"entry — the @assistant-ui cluster should resolve a single shared "
|
||||
"tap version."
|
||||
)
|
||||
return entry["version"]
|
||||
|
||||
|
||||
def test_assistant_ui_cluster_agrees_on_one_tap() -> None:
|
||||
"""Every @assistant-ui/* package's tap requirement must be satisfiable.
|
||||
|
||||
Encodes the contract that broke the desktop build: a single hoisted
|
||||
@assistant-ui/tap must satisfy the tap range declared by react, core,
|
||||
store, and any sibling — otherwise the missing ``./react-shim`` export
|
||||
(or a similar API split) breaks ``vite build``.
|
||||
"""
|
||||
packages = _lock_packages()
|
||||
tap_version = _hoisted_tap_version(packages)
|
||||
|
||||
offenders: list[str] = []
|
||||
for key, meta in packages.items():
|
||||
name = key.rsplit("node_modules/", 1)[-1]
|
||||
if not name.startswith("@assistant-ui/") or name == TAP:
|
||||
continue
|
||||
peer_meta = meta.get("peerDependenciesMeta", {}).get(TAP, {})
|
||||
if peer_meta.get("optional"):
|
||||
continue
|
||||
spec = meta.get("dependencies", {}).get(TAP) or meta.get(
|
||||
"peerDependencies", {}
|
||||
).get(TAP)
|
||||
if not spec:
|
||||
continue
|
||||
if not _caret_satisfies(tap_version, spec):
|
||||
offenders.append(f"{name} requires {TAP}{spec!r}")
|
||||
|
||||
assert not offenders, (
|
||||
f"Hoisted {TAP}@{tap_version} does not satisfy: "
|
||||
+ "; ".join(offenders)
|
||||
+ ". The @assistant-ui cluster has split tap requirements — pin the "
|
||||
"offending package (e.g. via root package.json `overrides`) so the "
|
||||
"whole cluster shares one tap line. See this test's module docstring."
|
||||
)
|
||||
|
||||
|
||||
def test_caret_satisfies_helper() -> None:
|
||||
"""Guard the tiny semver helper the invariant relies on."""
|
||||
assert _caret_satisfies("0.5.14", "^0.5.10")
|
||||
assert _caret_satisfies("0.5.14", "^0.5.14")
|
||||
assert not _caret_satisfies("0.5.14", "^0.9.0")
|
||||
assert not _caret_satisfies("0.5.14", "^0.6.0")
|
||||
assert _caret_satisfies("1.2.5", "^1.2.0")
|
||||
assert not _caret_satisfies("2.0.0", "^1.2.0")
|
||||
assert _caret_satisfies("0.5.14", "^0.5.0 || ^0.9.0")
|
||||
Loading…
Add table
Add a link
Reference in a new issue