mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-17 09:41:58 +00:00
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.
141 lines
5.3 KiB
Python
141 lines
5.3 KiB
Python
"""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")
|