hermes-agent/tests/docker/test_dump_build_sha.py

109 lines
4.1 KiB
Python

"""Regression test: ``hermes dump`` reports a real git SHA inside the container.
``.dockerignore`` excludes ``.git``, so ``git rev-parse HEAD`` fails inside
the published image. CI writes ``install-stamp.json`` before ``docker build``
(scripts/write_install_stamp.py) and it is COPY'd to the canonical
``/opt/hermes/install-stamp.json``. ``hermes dump`` reads the
commit from that stamp through ``hermes_cli.version_info``.
A local ``docker build`` (the ``built_image`` fixture in
``tests/docker/conftest.py``) has no CI stamp — only the Dockerfile's
distribution-only fallback, whose all-zero commit version_info skips. In
that case ``hermes dump`` falls back to ``(unknown)``.
This test asserts both cases:
* When the stamp exists in the image, ``hermes dump`` must show the first 8
characters of its commit, not ``(unknown)``.
* When the stamp is absent, ``hermes dump`` must show ``(unknown)`` — a guard
against the helper inventing a SHA from another source.
"""
from __future__ import annotations
import json
import re
import subprocess
_VERSION_LINE = re.compile(r"^version:\s+(?P<rest>.+)$", re.MULTILINE)
_SHA_BRACKET = re.compile(r"\[(?P<sha>[^\]]+)\]\s*$")
def _run_dump(image: str) -> str:
"""Return the stdout of ``docker run <image> dump``.
Relies on Docker's anonymous VOLUME for ``/opt/data`` (declared by the
Dockerfile) so the container's hermes user (UID 10000) can bootstrap
its config. Anonymous volumes are auto-cleaned by ``--rm``, so unlike
a host bind-mount we don't have to chown anything to UID 10000 (which
would break cleanup on non-root hosts).
"""
r = subprocess.run(
["docker", "run", "--rm", image, "dump"],
capture_output=True, text=True, timeout=120,
)
assert r.returncode == 0, (
f"hermes dump exited {r.returncode}: "
f"stderr={r.stderr[-1000:]!r}\nstdout={r.stdout[-1000:]!r}"
)
return r.stdout
def _read_stamp_commit_from_image(image: str) -> str | None:
"""Return the stamp commit from the image, or None when absent/unusable."""
r = subprocess.run(
[
"docker", "run", "--rm", "--entrypoint", "cat", image,
"/opt/hermes/install-stamp.json",
],
capture_output=True, text=True, timeout=30,
)
if r.returncode != 0:
return None
try:
commit = json.loads(r.stdout).get("commit") or ""
except ValueError:
return None
# An all-zero commit is the writer's fallback placeholder, and
# version_info skips it the same way.
if not commit or set(commit) == {"0"}:
return None
return commit
def test_dump_reports_stamp_commit_when_present(built_image: str) -> None:
"""When the image carries an install stamp, dump must surface its commit.
Together with the smoke-test action (which exercises ``--help``), this
closes the regression loop for the missing-sha bug: any future change
that breaks the stamp -> dump pipeline will fail CI here.
"""
stamped = _read_stamp_commit_from_image(built_image)
stdout = _run_dump(built_image)
match = _VERSION_LINE.search(stdout)
assert match, f"no `version:` line in dump output:\n{stdout[:2000]}"
sha_match = _SHA_BRACKET.search(match.group("rest"))
assert sha_match, (
f"`version:` line missing [<sha>] bracket: {match.group('rest')!r}"
)
reported = sha_match.group("sha")
if stamped is None:
# Local-build path: no stamp in the image. The fallback must stay
# '(unknown)' — a guard against the helper inventing a SHA.
assert reported == "(unknown)", (
f"expected '(unknown)' when no stamp is baked, got {reported!r}"
)
return
# CI path: the stamp exists. ``hermes dump`` shows the first 8 chars.
assert reported != "(unknown)", (
"install stamp present in image but dump still reported "
f"'(unknown)' — the stamp fallback is broken. Stamp commit: {stamped!r}"
)
assert reported == stamped[:8], (
f"dump reported {reported!r} but the stamp commit is {stamped!r} "
f"(expected first 8 chars: {stamped[:8]!r})"
)