"""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("^~>= 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")