From 2ab737f8f895547ca4a5adb89003fea056322c6e Mon Sep 17 00:00:00 2001 From: jenkins Date: Sat, 12 Sep 2026 22:37:23 -0500 Subject: [PATCH] fix(hermes): hide the worker overlay on dashboard pages --- services/hermes/agent-deployment.yaml | 2 +- .../scripts/patch_web_session_activity.py | 19 ++- .../test_hermes_chat_session_continuity.py | 1 + .../tests/test_hermes_dashboard_bootstrap.py | 118 ++++++++++++++++++ 4 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 testing/tests/test_hermes_dashboard_bootstrap.py diff --git a/services/hermes/agent-deployment.yaml b/services/hermes/agent-deployment.yaml index d139d1f8..907ed1fe 100644 --- a/services/hermes/agent-deployment.yaml +++ b/services/hermes/agent-deployment.yaml @@ -25,7 +25,7 @@ spec: ai.bstein.dev/execution: Hermes Kanban with durable direct Codex and Claude Code CLI workers ai.bstein.dev/model-policy: Jetson-assisted AUTO routing, low through xhigh, cross-provider fallback ai.bstein.dev/placement: primary amd64 accelerator titan-22; arm64 rpi5 fleet fallback; storage-backbone nodes excluded - ai.bstein.dev/config-rev: "20260825-claude-quota-expiry" + ai.bstein.dev/config-rev: "20260912-dashboard-bootstrap-visibility" prometheus.io/scrape: "true" prometheus.io/path: /metrics prometheus.io/port: "9010" diff --git a/services/hermes/scripts/patch_web_session_activity.py b/services/hermes/scripts/patch_web_session_activity.py index e1a1825d..ede66f76 100644 --- a/services/hermes/scripts/patch_web_session_activity.py +++ b/services/hermes/scripts/patch_web_session_activity.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Merge display-only worker activity into dashboard session transcripts.""" +"""Serve worker activity and keep its loading overlay scoped to resumed chats.""" from __future__ import annotations @@ -197,6 +197,20 @@ LATEST_SELECTION_AFTER = ''' children = {} ''' +SPA_INDEX_BEFORE = ''' html = _index_path.read_text(encoding="utf-8") +''' + +SPA_INDEX_AFTER = SPA_INDEX_BEFORE + ''' # The bundled overlay's inline display:flex overrides HTML hidden. + # Honor hidden before React mounts, including ordinary dashboard URLs. + html = html.replace( + "", + "", + 1, + ) +''' + + def patch(source: Path, destination: Path) -> None: """Apply the activity projection and fail closed on upstream drift.""" content = source.read_text(encoding="utf-8") @@ -208,9 +222,12 @@ def patch(source: Path, destination: Path) -> None: raise RuntimeError("Hermes dashboard lineage row context changed") if LATEST_SELECTION_BEFORE not in content: raise RuntimeError("Hermes dashboard lineage selection context changed") + if content.count(SPA_INDEX_BEFORE) != 1: + raise RuntimeError("Hermes dashboard bootstrap context changed") content = content.replace(HELPER_MARKER, HELPER_REPLACEMENT, 1) content = content.replace(LATEST_ROWS_BEFORE, LATEST_ROWS_AFTER, 1) content = content.replace(LATEST_SELECTION_BEFORE, LATEST_SELECTION_AFTER, 1) + content = content.replace(SPA_INDEX_BEFORE, SPA_INDEX_AFTER, 1) destination.parent.mkdir(parents=True, exist_ok=True) destination.write_text( content.replace(MESSAGES_BEFORE, MESSAGES_AFTER, 1), diff --git a/testing/tests/test_hermes_chat_session_continuity.py b/testing/tests/test_hermes_chat_session_continuity.py index f4f52428..8bd056ea 100644 --- a/testing/tests/test_hermes_chat_session_continuity.py +++ b/testing/tests/test_hermes_chat_session_continuity.py @@ -264,6 +264,7 @@ def test_web_session_activity_patch_projects_bounded_events(tmp_path: Path): + module.HELPER_MARKER + " db = object()\n" + module.MESSAGES_BEFORE + + module.SPA_INDEX_BEFORE + "suffix\n", encoding="utf-8", ) diff --git a/testing/tests/test_hermes_dashboard_bootstrap.py b/testing/tests/test_hermes_dashboard_bootstrap.py new file mode 100644 index 00000000..be44f887 --- /dev/null +++ b/testing/tests/test_hermes_dashboard_bootstrap.py @@ -0,0 +1,118 @@ +"""Regression checks for the worker dashboard's first-paint loading overlay.""" + +from __future__ import annotations + +import importlib.util +import os +import textwrap +from pathlib import Path + +import pytest + + +ROOT = Path(__file__).resolve().parents[2] +SPEC = importlib.util.spec_from_file_location( + "dashboard_activity_patch", + ROOT / "services/hermes/scripts/patch_web_session_activity.py", +) +assert SPEC and SPEC.loader +PATCH = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(PATCH) + + +def _backend_source() -> str: + """Provide the upstream anchors required by the deployment's patcher.""" + return "\n".join( + ( + PATCH.HELPER_MARKER, + PATCH.MESSAGES_BEFORE, + PATCH.LATEST_ROWS_BEFORE, + PATCH.LATEST_SELECTION_BEFORE, + PATCH.SPA_INDEX_BEFORE, + ) + ) + + +@pytest.mark.parametrize("occurrences", [0, 2]) +def test_bootstrap_patch_rejects_upstream_drift(tmp_path, occurrences): + """An ambiguous HTML insertion point must fail before writing a patch.""" + source = tmp_path / "web_server.py" + destination = tmp_path / "patched.py" + source.write_text( + _backend_source().replace( + PATCH.SPA_INDEX_BEFORE, PATCH.SPA_INDEX_BEFORE * occurrences + ), + encoding="utf-8", + ) + with pytest.raises(RuntimeError, match="bootstrap context changed"): + PATCH.patch(source, destination) + assert not destination.exists() + + +def _render_bootstrap(tmp_path: Path) -> str: + """Run the patched HTML handler against the image's actual bootstrap.""" + source = tmp_path / "web_server.py" + destination = tmp_path / "patched.py" + source.write_text(_backend_source(), encoding="utf-8") + PATCH.patch(source, destination) + patched = destination.read_text(encoding="utf-8") + html_handler = patched[patched.index(PATCH.SPA_INDEX_BEFORE):] + namespace = {} + exec( + "def serve(_index_path):\n" + + textwrap.indent(textwrap.dedent(html_handler), " ") + + " return html\n", + namespace, + ) + dockerfile = (ROOT / "dockerfiles/Dockerfile.hermes-agent").read_text() + body = dockerfile.split("` \n", 1)[1].split( + '
`', 1 + )[0] + index = tmp_path / "index.html" + index.write_text( + "" + + body + + '
Dashboard
', + encoding="utf-8", + ) + return namespace["serve"](index) + + +def test_bootstrap_patch_preserves_the_page_and_resume_script(tmp_path): + """HTML serving retains the app mount and the existing resume behavior.""" + html = _render_bootstrap(tmp_path) + assert '
Dashboard
' in html + assert 'loading.hidden = false' in html + assert html.count("") == html.count("") == 1 + assert html.count("") == 1 + + +@pytest.mark.parametrize("engine", ["chromium", "firefox"]) +@pytest.mark.parametrize( + ("route", "visible"), + [("/", False), ("/chat", False), ("/sessions?resume=worker", False), + ("/chat?resume=worker", True)], +) +def test_bootstrap_visibility_without_the_bundle(tmp_path, engine, route, visible): + """A failed or delayed bundle cannot cover ordinary dashboard routes.""" + playwright = pytest.importorskip("playwright.sync_api") + html = _render_bootstrap(tmp_path) + with playwright.sync_playwright() as runtime: + browser = getattr(runtime, engine).launch( + executable_path=os.environ.get(f"HERMES_TEST_{engine.upper()}_EXECUTABLE"), + headless=True, + ) + try: + page = browser.new_page() + page.route("**/*", lambda request: request.fulfill( + content_type="text/html", body=html + )) + page.goto("http://hermes.test" + route) + overlay = page.locator("#hermes-resume-bootstrap") + assert overlay.is_visible() is visible + if visible: + # SessionActivityPanel removes the overlay when it mounts. + overlay.evaluate("element => element.remove()") + assert overlay.count() == 0 + finally: + browser.close()