diff --git a/testing/quality_contract.json b/testing/quality_contract.json
index d94de9be..871b957f 100644
--- a/testing/quality_contract.json
+++ b/testing/quality_contract.json
@@ -17,6 +17,7 @@
"ci/scripts/hermes_image_release.py",
"dockerfiles/hermes-kaniko-heredoc-runner.py",
"services/harbor/scripts/harbor_hermes_agent_immutability_ensure.py",
+ "services/hermes/scripts/hermes_image_release_status.py",
"services/hermes/scripts/jenkins_image_build_trigger.py",
"ci/scripts/publish_test_metrics.py",
"ci/scripts/publish_test_metrics_quality.py",
@@ -107,6 +108,7 @@
"ci/scripts/hermes_image_release.py",
"dockerfiles/hermes-kaniko-heredoc-runner.py",
"services/harbor/scripts/harbor_hermes_agent_immutability_ensure.py",
+ "services/hermes/scripts/hermes_image_release_status.py",
"services/hermes/scripts/jenkins_image_build_trigger.py",
"ci/scripts/publish_test_metrics.py",
"ci/scripts/publish_test_metrics_quality.py",
@@ -341,6 +343,7 @@
"ci/scripts/hermes_image_release.py",
"dockerfiles/hermes-kaniko-heredoc-runner.py",
"services/harbor/scripts/harbor_hermes_agent_immutability_ensure.py",
+ "services/hermes/scripts/hermes_image_release_status.py",
"services/hermes/scripts/jenkins_image_build_trigger.py",
"ci/scripts/publish_test_metrics.py",
"ci/scripts/publish_test_metrics_quality.py",
diff --git a/testing/tests/test_hermes_release_followthrough.py b/testing/tests/test_hermes_release_followthrough.py
new file mode 100644
index 00000000..f95cd9de
--- /dev/null
+++ b/testing/tests/test_hermes_release_followthrough.py
@@ -0,0 +1,331 @@
+"""Contracts for Hermes' read-only image release follow-through helper."""
+
+from __future__ import annotations
+
+import importlib.util
+import json
+import subprocess
+import sys
+from pathlib import Path
+
+import pytest
+
+
+ROOT = Path(__file__).resolve().parents[2]
+STATUS = ROOT / "services/hermes/scripts/hermes_image_release_status.py"
+TRIGGER = ROOT / "services/hermes/scripts/jenkins_image_build_trigger.py"
+
+
+def _load(path: Path, name: str):
+ spec = importlib.util.spec_from_file_location(name, path)
+ assert spec is not None and spec.loader is not None
+ module = importlib.util.module_from_spec(spec)
+ sys.modules[name] = module
+ spec.loader.exec_module(module)
+ return module
+
+
+def _condition(ready: bool = True, generation: int = 3) -> list[dict]:
+ return [
+ {
+ "type": "Ready",
+ "status": "True" if ready else "False",
+ "observedGeneration": generation,
+ }
+ ]
+
+
+def _policy(module, revision: str, digest: str, *, ready: bool = True) -> dict:
+ return {
+ "metadata": {"generation": 3},
+ "status": {
+ "conditions": _condition(ready),
+ "latestRef": {
+ "tag": f"git-{revision}-build-17-release",
+ "digest": digest,
+ },
+ },
+ }
+
+
+def _controller(kind: str, repository: str, digest: str, *, ready: bool = True) -> dict:
+ status = {
+ "observedGeneration": 4,
+ "readyReplicas": 1 if ready else 0,
+ "updatedReplicas": 1,
+ "availableReplicas": 1,
+ "currentReplicas": 1,
+ "currentRevision": "revision-2",
+ "updateRevision": "revision-2",
+ }
+ if kind == "statefulset" and not ready:
+ status["currentRevision"] = "revision-1"
+ return {
+ "metadata": {"generation": 4},
+ "spec": {
+ "replicas": 1,
+ "template": {
+ "spec": {
+ "initContainers": [{"image": "busybox:1"}],
+ "containers": [{"image": f"{repository}@{digest}"}],
+ }
+ },
+ },
+ "status": status,
+ }
+
+
+def _pods(repository: str, digest: str, *, ready: bool = True) -> dict:
+ return {
+ "items": [
+ {
+ "status": {
+ "conditions": [
+ {"type": "Ready", "status": "True" if ready else "False"}
+ ],
+ "containerStatuses": [
+ {
+ "image": f"{repository}@{digest}",
+ "imageID": f"{repository}@{digest}",
+ }
+ ],
+ }
+ }
+ ]
+ }
+
+
+def _healthy_objects(module, component: str, revision: str, digest: str) -> dict:
+ config = module.COMPONENTS[component]
+ objects = {
+ ("hermes", "imagepolicy", config["policy"]): _policy(
+ module, revision, digest
+ ),
+ ("hermes", "imageupdateautomation", "hermes"): {
+ "metadata": {"generation": 3},
+ "status": {"conditions": _condition()},
+ },
+ ("flux-system", "kustomization", "hermes"): {
+ "metadata": {"generation": 3},
+ "status": {
+ "conditions": _condition(),
+ "lastAppliedRevision": "main@sha1:" + "f" * 40,
+ },
+ },
+ }
+ for kind, name, label in config["workloads"]:
+ objects[("hermes", kind, name)] = _controller(
+ kind, config["repository"], digest
+ )
+ objects[("hermes", "pods", f"app={label}")] = _pods(
+ config["repository"], digest
+ )
+ return objects
+
+
+def _install_fake(module, monkeypatch: pytest.MonkeyPatch, objects: dict) -> None:
+ def fake(*command):
+ namespace = command[1]
+ kind = command[3]
+ if kind == "pods":
+ key = (namespace, kind, command[5])
+ else:
+ key = (namespace, kind, command[4])
+ return objects[key]
+
+ monkeypatch.setattr(module, "_kubectl_json", fake)
+
+
+@pytest.mark.parametrize("component", ["agent", "webui", "stt", "tts"])
+def test_exact_release_convergence_covers_every_component(
+ monkeypatch: pytest.MonkeyPatch, component: str
+) -> None:
+ """Every lane binds policy source/digest to Ready desired and running state."""
+ module = _load(STATUS, f"release_status_{component}")
+ revision = "a" * 40
+ digest = "sha256:" + "b" * 64
+ _install_fake(module, monkeypatch, _healthy_objects(module, component, revision, digest))
+
+ result = module.inspect_release(component, revision)
+
+ assert result["converged"] is True
+ assert result["stage"] == "converged"
+ assert result["selected_revision"] == revision
+ assert result["digest"] == digest
+ assert result["build"] == 17
+ assert all(item["converged"] for item in result["workloads"])
+
+
+def test_pending_and_different_policy_never_claim_deployment(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ """No or different immutable source remains visibly incomplete."""
+ module = _load(STATUS, "release_status_policy_pending")
+ revision = "a" * 40
+ digest = "sha256:" + "b" * 64
+ config = module.COMPONENTS["tts"]
+ objects = _healthy_objects(module, "tts", revision, digest)
+ policy_key = ("hermes", "imagepolicy", config["policy"])
+ objects[policy_key]["status"]["latestRef"] = {}
+ _install_fake(module, monkeypatch, objects)
+ assert module.inspect_release("tts", revision)["stage"] == "image_policy_pending"
+
+ objects[policy_key] = _policy(module, "c" * 40, digest)
+ result = module.inspect_release("tts", revision)
+ assert result["stage"] == "different_revision_selected"
+ assert result["selected_revision"] == "c" * 40
+ assert result["converged"] is False
+
+
+@pytest.mark.parametrize(
+ ("mutation", "expected"),
+ [
+ ("automation", "image_automation_pending"),
+ ("flux", "flux_apply_pending"),
+ ("desired", "flux_apply_pending"),
+ ("pod", "rollout_pending"),
+ ],
+)
+def test_followthrough_reports_the_first_incomplete_delivery_stage(
+ monkeypatch: pytest.MonkeyPatch, mutation: str, expected: str
+) -> None:
+ """The helper separates promotion, GitOps apply, and live rollout gaps."""
+ module = _load(STATUS, f"release_status_stage_{mutation}")
+ revision = "d" * 40
+ digest = "sha256:" + "e" * 64
+ objects = _healthy_objects(module, "stt", revision, digest)
+ if mutation == "automation":
+ objects[("hermes", "imageupdateautomation", "hermes")]["status"][
+ "conditions"
+ ] = _condition(False)
+ elif mutation == "flux":
+ objects[("flux-system", "kustomization", "hermes")]["status"][
+ "conditions"
+ ] = _condition(False)
+ elif mutation == "desired":
+ objects[("hermes", "deployment", "hermes-stt")]["spec"]["template"][
+ "spec"
+ ]["containers"][0]["image"] = "registry.bstein.dev/bstein/hermes-jetson-stt@sha256:" + "1" * 64
+ else:
+ objects[("hermes", "pods", "app=hermes-stt")] = _pods(
+ module.COMPONENTS["stt"]["repository"], digest, ready=False
+ )
+ _install_fake(module, monkeypatch, objects)
+
+ result = module.inspect_release("stt", revision)
+
+ assert result["stage"] == expected
+ assert result["converged"] is False
+
+
+def test_helpers_fail_closed_on_stale_generation_and_malformed_references() -> None:
+ """Malformed release evidence and stale Ready conditions are not accepted."""
+ module = _load(STATUS, "release_status_helpers")
+ assert module._condition_ready(
+ {"metadata": {"generation": 4}, "status": {"conditions": _condition(True, 3)}}
+ ) is False
+ assert module._condition_ready({}) is False
+ assert module._release_ref({"status": {"latestRef": {"tag": "latest"}}}) == (
+ "latest",
+ None,
+ None,
+ None,
+ )
+ assert module._rollout_ready("deployment", _controller("deployment", "r", "d"))[2]
+ stale = _controller("deployment", "r", "d")
+ stale["status"]["observedGeneration"] = 1
+ assert module._rollout_ready("deployment", stale)[2] is False
+ with pytest.raises(ValueError):
+ module.inspect_release("invalid", "a" * 40)
+ with pytest.raises(ValueError):
+ module.inspect_release("agent", "main")
+
+
+def test_kubectl_reader_is_bounded_and_requires_an_object(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ """Status uses argument arrays, a timeout, and object-shaped JSON only."""
+ module = _load(STATUS, "release_status_kubectl")
+ seen = {}
+
+ def run(command, **kwargs):
+ seen["command"] = command
+ seen["kwargs"] = kwargs
+ return subprocess.CompletedProcess(command, 0, '{"ok": true}', "")
+
+ monkeypatch.setattr(module.subprocess, "run", run)
+ assert module._kubectl_json("get", "pod", "demo") == {"ok": True}
+ assert seen["command"] == ["kubectl", "get", "pod", "demo", "-o", "json"]
+ assert seen["kwargs"]["timeout"] == 30
+ monkeypatch.setattr(
+ module.subprocess,
+ "run",
+ lambda *_args, **_kwargs: subprocess.CompletedProcess([], 0, "[]", ""),
+ )
+ with pytest.raises(ValueError, match="non-object"):
+ module._kubectl_json("get", "pods")
+
+
+def test_cli_waits_for_convergence_and_returns_safe_json(
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
+) -> None:
+ """Wait mode polls bounded status and terminates only at convergence."""
+ module = _load(STATUS, "release_status_main")
+ revision = "7" * 40
+ states = [
+ {"stage": "image_policy_pending", "converged": False},
+ {"stage": "converged", "converged": True, "digest": "sha256:" + "8" * 64},
+ ]
+ monkeypatch.setattr(module, "inspect_release", lambda *_args: states.pop(0))
+ monkeypatch.setattr(module.time, "sleep", lambda _seconds: None)
+ assert module.main(
+ ["--component", "agent", "--revision", revision, "--wait", "--poll", "1"]
+ ) == 0
+ assert json.loads(capsys.readouterr().out)["converged"] is True
+
+ assert module.main(
+ ["--component", "agent", "--revision", revision, "--timeout", "0"]
+ ) == 2
+ assert "must be positive" in capsys.readouterr().err
+
+
+def test_trigger_returns_a_token_free_exact_follow_command(tmp_path: Path) -> None:
+ """A successful trigger tells Hermes how to prove the approved release landed."""
+ module = _load(TRIGGER, "release_status_trigger")
+ revision = "9" * 40
+ token = tmp_path / "token"
+ token.write_text("never-print-me\n", encoding="utf-8")
+
+ class Response:
+ status = 201
+ headers = {"Location": "/queue/item/91/"}
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *_args):
+ return None
+
+ result = module.trigger_build(
+ revision,
+ component="webui",
+ token_file=token,
+ opener=lambda *_args, **_kwargs: Response(),
+ )
+
+ assert result["follow_command"] == (
+ "/opt/coordinator/hermes_image_release_status.py --component webui "
+ f"--revision {revision} --wait"
+ )
+ assert "never-print-me" not in json.dumps(result)
+
+
+def test_runtime_bundle_and_guidance_require_live_convergence() -> None:
+ """The helper ships to Worker and merged code alone is explicitly insufficient."""
+ kustomization = (ROOT / "services/hermes/kustomization.yaml").read_text()
+ guidance = (ROOT / "services/hermes/agent-configmap.yaml").read_text()
+ assert "hermes_image_release_status.py=scripts/hermes_image_release_status.py" in kustomization
+ assert "safe `follow_command`" in guidance
+ assert "Merged code is not a completed release" in guidance.replace(
+ "merged code", "Merged code"
+ )
diff --git a/testing/tests/test_hermes_webui_brand.py b/testing/tests/test_hermes_webui_brand.py
index 48ae1b58..053fb49d 100644
--- a/testing/tests/test_hermes_webui_brand.py
+++ b/testing/tests/test_hermes_webui_brand.py
@@ -142,6 +142,11 @@ def test_production_patchers_apply_title_icons_theme_and_cache_contract(
assert '
Hermes Chat' in index
assert '

' in index
+ assert (
+ '
![Hermes Agent]()
'
+ ) in index
+ assert 'aria-label="Hermes caduceus"' not in index
assert "favicon.svg" not in index
assert "favicon-32.png" not in index
@@ -291,12 +296,35 @@ def test_brand_css_is_accessible_dark_and_reduced_motion_aware() -> None:
assert "--accent: #48cfcc" in dark
assert "--voice-accent: 72, 207, 204" in css
assert "--voice-accent-secondary: 76, 164, 205" in css
+ assert "--hermes-violet: #9d8ee0" in dark
+ assert "--hermes-grid: rgba(72, 207, 204, 0.025)" in dark
+ assert ".empty-logo .hermes-agent-portrait" in css
+ assert "width: 112px" in css
+ assert ":root.dark .messages" in css
+ assert "background-size: 32px 32px, 32px 32px, auto, auto" in css
+ assert ":root.dark .session-item.active" in css
+ assert ":root.dark .suggestion:focus-visible" in css
assert "@media (prefers-reduced-motion: reduce)" in css
reduced = css.split("@media (prefers-reduced-motion: reduce)", 1)[1]
assert "transition: none !important" in reduced
+ assert "transform: none !important" in reduced
assert "animation:" not in css
+def test_empty_state_uses_canonical_character_without_inline_staff(
+ tmp_path: Path,
+) -> None:
+ """The new-chat identity reuses the established full character artwork."""
+ index = (_patched_fixture(tmp_path) / "static/index.html").read_text(
+ encoding="utf-8"
+ )
+ empty_state = index.split('
', 1)[1]
+ empty_state = empty_state.split('
', 1)[0]
+
+
def test_dockerfile_copies_and_verifies_every_tracked_brand_asset() -> None:
"""The immutable image, not a runtime coordinator path, owns PWA assets."""
dockerfile = (DOCKERFILES / "Dockerfile.hermes-webui").read_text(encoding="utf-8")