mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
Add a module-scoped `shared_container` fixture to tests/docker/conftest.py that boots one `sleep infinity` container per test module and tears it down at module exit. Convert read-only tests that previously used `docker run --rm --entrypoint sh/cat/test/su` (bypassing s6 to check static image properties) or `docker run -d` + `docker exec` (starting identical containers per test) to use `docker exec` on the shared container instead. Converted files: test_immutable_install_permissions.py — 2 throwaway runs → 2 execs test_license_file_present.py — 1 throwaway run → 1 exec test_tini_compat_shim.py — 1 throwaway run → 1 exec test_tui_prebuilt_bundle.py — 2 throwaway runs → 2 execs test_dump_build_sha.py — 2 throwaway runs → 2 execs test_immutable_install.py — 3 detached runs → 1 shared + 1 isolated test_dashboard.py — 2 detached runs → 0 (use shared) Local profiling shows docker run calls in these 7 files dropped from ~25 to 7 (the 7 are shared_container boots per module + the one test that needs a restart). Each eliminated `docker run` was paying 1-9s of s6 cont-init startup; the replacement `docker exec` calls average 0.10s — an ~50x speedup per operation. Tests that mutate state (restarts, config changes, gateway starts) still use their own containers via `container_name` + `start_container`.
74 lines
3.2 KiB
Python
74 lines
3.2 KiB
Python
"""Harness: the image ships a prebuilt TUI bundle, not a runtime npm install.
|
|
|
|
Regression guard for the hosted-chat failure where the embedded dashboard
|
|
Chat tab died with a 502 / "[session ended]". Root cause: the image installs
|
|
only a subset of the npm monorepo workspaces (root/web/ui-tui, never apps/*),
|
|
so the actualized node_modules permanently disagrees with the canonical
|
|
package-lock.json. Without HERMES_TUI_DIR set, ``_make_tui_argv`` falls
|
|
through to ``_tui_need_npm_install`` (which returns True forever) and tries a
|
|
runtime ``npm install`` that can never converge and races itself across
|
|
concurrent /api/pty connections → ENOTEMPTY.
|
|
|
|
The fix is ``ENV HERMES_TUI_DIR=/opt/hermes/ui-tui`` in the Dockerfile, which
|
|
makes the launcher take the prebuilt-bundle fast path (``node --expose-gc
|
|
.../dist/entry.js``) and skip the install check entirely. These tests assert
|
|
that invariant holds in the built image.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import shlex
|
|
|
|
from tests.docker.conftest import docker_exec_sh
|
|
|
|
|
|
def _exec_py(container: str, py: str) -> str:
|
|
"""Run a Python snippet inside the container as the hermes user, return stdout."""
|
|
inner = (
|
|
". /opt/hermes/.venv/bin/activate && "
|
|
"cd /opt/hermes && "
|
|
f"python3 -c {shlex.quote(py)}"
|
|
)
|
|
r = docker_exec_sh(container, inner, timeout=60)
|
|
assert r.returncode == 0, f"in-container python failed:\n{r.stderr[-2000:]}"
|
|
return r.stdout.strip()
|
|
|
|
|
|
def test_hermes_tui_dir_env_is_set(shared_container: str) -> None:
|
|
"""HERMES_TUI_DIR must point at the prebuilt bundle dir in the image."""
|
|
r = docker_exec_sh(
|
|
shared_container, 'printf "%s" "$HERMES_TUI_DIR"', timeout=30,
|
|
)
|
|
assert r.returncode == 0, r.stderr[-2000:]
|
|
assert r.stdout.strip() == "/opt/hermes/ui-tui", (
|
|
f"HERMES_TUI_DIR={r.stdout.strip()!r} (expected /opt/hermes/ui-tui)"
|
|
)
|
|
|
|
|
|
def test_prebuilt_bundle_present_and_no_runtime_install(
|
|
shared_container: str,
|
|
) -> None:
|
|
"""The launcher must (a) find the prebuilt bundle and (b) NOT want an
|
|
npm install — i.e. it takes the same path as a nix/packaged release."""
|
|
py = (
|
|
"import json\n"
|
|
"from pathlib import Path\n"
|
|
"from hermes_cli.main import _tui_need_npm_install, _find_bundled_tui, _make_tui_argv\n"
|
|
"ui = Path('/opt/hermes/ui-tui')\n"
|
|
"argv, cwd = _make_tui_argv(ui, tui_dev=False)\n"
|
|
"out = {\n"
|
|
" 'dist_entry_exists': (ui / 'dist' / 'entry.js').is_file(),\n"
|
|
" 'need_npm_install': _tui_need_npm_install(ui),\n"
|
|
" 'argv': argv,\n"
|
|
" 'uses_prebuilt': ('dist/entry.js' in ' '.join(argv)) and ('npm' not in argv[0].lower()),\n"
|
|
"}\n"
|
|
"print(json.dumps(out))\n"
|
|
)
|
|
out = json.loads(_exec_py(shared_container, py))
|
|
assert out["dist_entry_exists"], "prebuilt ui-tui/dist/entry.js missing from image"
|
|
# With HERMES_TUI_DIR set, _make_tui_argv returns the prebuilt path BEFORE
|
|
# ever reaching the install check — so the resolved argv is what matters.
|
|
assert out["uses_prebuilt"], f"launcher did not take prebuilt path: argv={out['argv']!r}"
|
|
assert "npm" not in out["argv"][0].lower(), (
|
|
f"launcher resolved to an npm invocation, not the prebuilt bundle: {out['argv']!r}"
|
|
)
|