2026-08-16 20:41:14 -03:00
|
|
|
"""Core configuration and workload contracts for Hermes chat."""
|
2026-08-08 23:24:58 -03:00
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import importlib.util
|
2026-08-10 01:47:51 -03:00
|
|
|
import sys
|
2026-08-16 20:41:14 -03:00
|
|
|
from types import SimpleNamespace
|
2026-08-08 23:24:58 -03:00
|
|
|
|
|
|
|
|
import yaml
|
|
|
|
|
|
2026-08-16 20:41:14 -03:00
|
|
|
from testing.tests.test_hermes_chat_support import (
|
|
|
|
|
HERMES,
|
|
|
|
|
ROOT,
|
|
|
|
|
_documents,
|
|
|
|
|
)
|
2026-08-12 00:35:50 -03:00
|
|
|
|
2026-08-08 23:24:58 -03:00
|
|
|
def test_chat_config_enables_real_research_compute_and_delegation():
|
|
|
|
|
configmap = _documents(HERMES / "chat-configmap.yaml")[0]
|
|
|
|
|
config = yaml.safe_load(configmap["data"]["config.yaml"])
|
|
|
|
|
|
2026-08-11 20:22:26 -03:00
|
|
|
# AUTO leaves effort unset so the target chosen by Switchyard owns it.
|
|
|
|
|
assert "reasoning_effort" not in config["agent"]
|
|
|
|
|
assert config["model"]["model"] == "atlas/auto/fast"
|
2026-08-08 23:24:58 -03:00
|
|
|
assert config["web"] == {
|
|
|
|
|
"backend": "ddgs",
|
|
|
|
|
"search_backend": "ddgs",
|
|
|
|
|
"extract_backend": "public-extract",
|
|
|
|
|
}
|
|
|
|
|
assert config["delegation"]["max_concurrent_children"] == 2
|
2026-08-09 02:42:11 -03:00
|
|
|
assert config["delegation"]["max_iterations"] == 80
|
|
|
|
|
assert config["agent"]["max_turns"] == 120
|
|
|
|
|
assert config["tool_loop_guardrails"]["hard_stop_enabled"] is True
|
2026-08-08 23:24:58 -03:00
|
|
|
for platform in ("cli", "api_server"):
|
|
|
|
|
toolsets = config["platform_toolsets"][platform]
|
|
|
|
|
assert "delegation" in toolsets
|
2026-08-09 12:55:20 -03:00
|
|
|
assert "browser" in toolsets
|
2026-08-08 23:24:58 -03:00
|
|
|
assert "python_sandbox" in toolsets
|
2026-08-09 12:55:20 -03:00
|
|
|
assert "vision" in toolsets
|
2026-08-08 23:24:58 -03:00
|
|
|
assert "web" in toolsets
|
2026-08-11 01:14:46 -03:00
|
|
|
assert "image_gen" in toolsets
|
2026-08-08 23:24:58 -03:00
|
|
|
assert "terminal" not in toolsets
|
|
|
|
|
assert "code_execution" not in toolsets
|
|
|
|
|
|
|
|
|
|
|
2026-08-15 06:31:16 -03:00
|
|
|
def test_agent_config_keeps_delegated_reviewers_from_owning_task_lifecycle():
|
|
|
|
|
configmap = _documents(HERMES / "agent-configmap.yaml")[0]
|
|
|
|
|
instructions = configmap["data"]["AGENTS.md"]
|
2026-08-16 06:55:49 -03:00
|
|
|
soul = configmap["data"]["SOUL.md"]
|
2026-08-16 04:31:00 -03:00
|
|
|
config = yaml.safe_load(configmap["data"]["config.yaml"])
|
2026-08-15 06:31:16 -03:00
|
|
|
|
2026-08-16 04:31:00 -03:00
|
|
|
assert (
|
|
|
|
|
"Only the foreground durable worker owns its Kanban task lifecycle"
|
|
|
|
|
in instructions
|
|
|
|
|
)
|
|
|
|
|
assert "Never unblock and then complete" in instructions
|
|
|
|
|
assert "gateway dispatcher can claim the transient" in instructions
|
|
|
|
|
for platform in ("cli", "api_server"):
|
|
|
|
|
assert "kanban" in config["platform_toolsets"][platform]
|
2026-08-15 06:31:16 -03:00
|
|
|
assert "must never complete, block, unblock, reclaim" in instructions
|
|
|
|
|
assert "task's final structured result itself" in instructions
|
2026-08-16 01:10:47 -03:00
|
|
|
assert "Atlas organization has private visibility" in instructions
|
2026-08-16 06:55:49 -03:00
|
|
|
assert "may be public or private" in instructions
|
|
|
|
|
assert "do not infer a\nrepository's visibility" in instructions
|
2026-08-16 20:41:14 -03:00
|
|
|
assert "brokered Git for clone, fetch" in instructions
|
|
|
|
|
assert "client carries no repository credential" in instructions
|
2026-08-16 06:06:26 -03:00
|
|
|
assert "Never call `kanban_show` without a known, non-empty task ID" in instructions
|
|
|
|
|
assert "bounded ad-hoc inspection and acceptance checks may" in instructions
|
|
|
|
|
assert "load implementation or TDD skills" in instructions
|
2026-08-16 06:55:49 -03:00
|
|
|
assert "Never call `kanban_show` without\na known, non-empty task ID" in soul
|
|
|
|
|
assert "must load a skill only when its workflow\nmaterially applies" in soul
|
2026-08-16 20:41:14 -03:00
|
|
|
assert "no repository token is present in this pod" in soul
|
2026-08-16 19:54:41 -03:00
|
|
|
assert "`scm.bstein.dev` is Forgejo/Gitea, not GitHub" in instructions
|
|
|
|
|
assert "GitHub/`gh` skill for an Atlas remote" in instructions
|
|
|
|
|
assert "load\n`$manage-atlas-pull-requests`" in instructions
|
2026-08-16 20:18:49 -03:00
|
|
|
assert "Updates, merge,\napprove, close, delete, comments" in instructions
|
|
|
|
|
assert "Leave every PR\nunmerged for Brad's review" in instructions
|
|
|
|
|
assert "Use the Gitea pull-request merge endpoint" not in instructions
|
|
|
|
|
assert "reconcile the open PR" not in instructions
|
|
|
|
|
assert "use Flux reconciliation after" not in instructions
|
|
|
|
|
assert "PR publication ends at the verified open draft" in instructions
|
2026-08-16 07:59:35 -03:00
|
|
|
assert "JENKINS_BASE_URL" in instructions
|
|
|
|
|
assert "Do not\nuse `git reset --hard`" in instructions
|
|
|
|
|
rendered = (HERMES / "agent-deployment.yaml").read_text()
|
|
|
|
|
assert 'git config --global user.name "Hermes Agent"' in rendered
|
|
|
|
|
assert 'git config --global user.email "hermes@bstein.dev"' in rendered
|
2026-08-15 06:31:16 -03:00
|
|
|
|
|
|
|
|
|
2026-08-16 04:31:00 -03:00
|
|
|
def test_agent_image_completes_parked_kanban_tasks_atomically():
|
|
|
|
|
dockerfile = (ROOT / "dockerfiles" / "Dockerfile.hermes-agent").read_text()
|
|
|
|
|
|
|
|
|
|
assert (
|
|
|
|
|
"AND status IN ('running', 'ready', 'blocked', 'scheduled')"
|
|
|
|
|
in dockerfile
|
|
|
|
|
)
|
|
|
|
|
assert (
|
|
|
|
|
"Atomically mark running, ready, blocked, or scheduled tasks done"
|
|
|
|
|
in dockerfile
|
|
|
|
|
)
|
2026-08-16 20:53:22 +00:00
|
|
|
assert "kind IN ('created', 'blocked', 'unblocked')" in dockerfile
|
|
|
|
|
assert "payload.get(\"status\") == \"blocked\"" in dockerfile
|
|
|
|
|
assert "hermes-kanban-blocked-regression.py" in dockerfile
|
2026-08-16 04:31:00 -03:00
|
|
|
|
|
|
|
|
|
2026-08-10 00:42:37 -03:00
|
|
|
def test_sandbox_shares_only_the_tenant_workspace_without_credentials():
|
2026-08-08 23:24:58 -03:00
|
|
|
sandbox_docs = _documents(HERMES / "chat-sandbox.yaml")
|
2026-08-10 00:42:37 -03:00
|
|
|
deployments = [doc for doc in sandbox_docs if doc["kind"] == "Deployment"]
|
2026-08-14 23:54:35 -03:00
|
|
|
assert len(deployments) == 8
|
2026-08-10 00:42:37 -03:00
|
|
|
for ordinal, deployment in enumerate(deployments):
|
|
|
|
|
pod_spec = deployment["spec"]["template"]["spec"]
|
|
|
|
|
container = pod_spec["containers"][0]
|
|
|
|
|
assert pod_spec["automountServiceAccountToken"] is False
|
|
|
|
|
assert container["securityContext"]["readOnlyRootFilesystem"] is True
|
|
|
|
|
assert container["securityContext"]["runAsNonRoot"] is True
|
|
|
|
|
assert container["securityContext"]["runAsGroup"] == 10000
|
|
|
|
|
assert not container.get("env")
|
|
|
|
|
assert {mount["mountPath"] for mount in container["volumeMounts"]} == {
|
|
|
|
|
"/tmp",
|
|
|
|
|
"/workspace",
|
|
|
|
|
"/opt/data/workspace",
|
|
|
|
|
}
|
|
|
|
|
workspace_volume = next(
|
|
|
|
|
item for item in pod_spec["volumes"] if item["name"] == "workspace"
|
|
|
|
|
)
|
|
|
|
|
assert workspace_volume["persistentVolumeClaim"]["claimName"] == (
|
|
|
|
|
f"workspace-hermes-chat-tenant-{ordinal}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
statefulset = _documents(HERMES / "chat-statefulset.yaml")[0]
|
|
|
|
|
templates = statefulset["spec"]["volumeClaimTemplates"]
|
|
|
|
|
workspace = next(item for item in templates if item["metadata"]["name"] == "workspace")
|
|
|
|
|
assert workspace["spec"]["resources"]["requests"]["storage"] == "10Gi"
|
|
|
|
|
assert workspace["spec"]["accessModes"] == ["ReadWriteMany"]
|
|
|
|
|
|
2026-08-08 23:24:58 -03:00
|
|
|
pod_spec = statefulset["spec"]["template"]["spec"]
|
2026-08-10 00:42:37 -03:00
|
|
|
hermes = next(item for item in pod_spec["containers"] if item["name"] == "hermes")
|
|
|
|
|
startup = hermes["args"][0]
|
|
|
|
|
assert "hermes-chat-sandbox-${ordinal}.hermes-chat-sandbox" in startup
|
|
|
|
|
assert any(
|
|
|
|
|
mount["name"] == "workspace" and mount["mountPath"] == "/opt/data/workspace"
|
|
|
|
|
for mount in hermes["volumeMounts"]
|
|
|
|
|
)
|
2026-08-08 23:24:58 -03:00
|
|
|
|
|
|
|
|
policies = _documents(HERMES / "networkpolicy.yaml")
|
|
|
|
|
deny = next(
|
2026-08-10 00:42:37 -03:00
|
|
|
item
|
|
|
|
|
for item in policies
|
|
|
|
|
if item["metadata"]["name"] == "hermes-chat-sandbox-deny"
|
2026-08-08 23:24:58 -03:00
|
|
|
)
|
2026-08-10 00:42:37 -03:00
|
|
|
assert deny["spec"]["ingress"] == []
|
2026-08-08 23:24:58 -03:00
|
|
|
assert deny["spec"]["egress"] == []
|
|
|
|
|
for ordinal in range(4):
|
|
|
|
|
policy = next(
|
2026-08-10 00:42:37 -03:00
|
|
|
item
|
|
|
|
|
for item in policies
|
|
|
|
|
if item["metadata"]["name"] == f"hermes-chat-sandbox-tenant-{ordinal}"
|
2026-08-08 23:24:58 -03:00
|
|
|
)
|
|
|
|
|
assert policy["spec"]["podSelector"]["matchLabels"][
|
2026-08-10 00:42:37 -03:00
|
|
|
"ai.bstein.dev/tenant-ordinal"
|
2026-08-08 23:24:58 -03:00
|
|
|
] == str(ordinal)
|
|
|
|
|
source = policy["spec"]["ingress"][0]["from"][0]["podSelector"][
|
|
|
|
|
"matchLabels"
|
|
|
|
|
]
|
2026-08-10 00:42:37 -03:00
|
|
|
assert source["statefulset.kubernetes.io/pod-name"] == (
|
|
|
|
|
f"hermes-chat-tenant-{ordinal}"
|
|
|
|
|
)
|
2026-08-08 23:24:58 -03:00
|
|
|
|
|
|
|
|
|
2026-09-14 00:09:43 -05:00
|
|
|
def test_dashboard_pty_uses_alternate_screen_and_mouse_tracking():
|
|
|
|
|
agent = _documents(HERMES / "agent-deployment.yaml")[0]
|
|
|
|
|
main = next(
|
|
|
|
|
container
|
|
|
|
|
for container in agent["spec"]["template"]["spec"]["containers"]
|
|
|
|
|
if container["name"] == "hermes"
|
|
|
|
|
)
|
|
|
|
|
env = {item["name"]: str(item.get("value", "")) for item in main["env"]}
|
|
|
|
|
|
|
|
|
|
assert env["HERMES_DASHBOARD"] == "1"
|
|
|
|
|
assert env["HERMES_TUI_INLINE"] == "0"
|
|
|
|
|
assert env["HERMES_TUI_DISABLE_MOUSE"] == "0"
|
|
|
|
|
|
|
|
|
|
|
2026-08-08 23:24:58 -03:00
|
|
|
def test_gateway_image_honors_ui_model_and_caps_reasoning():
|
|
|
|
|
dockerfile = (ROOT / "dockerfiles" / "Dockerfile.hermes-agent").read_text()
|
2026-09-13 15:46:39 -05:00
|
|
|
base_arg = next(
|
|
|
|
|
line for line in dockerfile.splitlines() if line.startswith("ARG HERMES_AGENT_BASE=")
|
|
|
|
|
)
|
|
|
|
|
assert base_arg.startswith(
|
|
|
|
|
"ARG HERMES_AGENT_BASE=harbor-core.harbor.svc.cluster.local/mirror/hermes-agent@sha256:"
|
|
|
|
|
)
|
|
|
|
|
assert len(base_arg.rsplit("@sha256:", 1)[1]) == 64
|
|
|
|
|
assert "FROM ${HERMES_AGENT_BASE} AS sqlite-build" in dockerfile
|
|
|
|
|
assert "FROM sqlite-runtime" in dockerfile
|
2026-08-08 23:24:58 -03:00
|
|
|
assert "_resolve_request_route" in dockerfile
|
2026-08-11 20:22:26 -03:00
|
|
|
assert 'allowed_providers = {"atlas-switchyard"}' in dockerfile
|
2026-08-08 23:24:58 -03:00
|
|
|
assert 'reasoning_effort=body.get("reasoning_effort")' in dockerfile
|
|
|
|
|
assert 'reasoning_config = {"enabled": True, "effort": "xhigh"}' in dockerfile
|
|
|
|
|
assert "ddgs==9.14.4" in dockerfile
|
2026-08-08 23:59:05 -03:00
|
|
|
assert "specific not in _LEGACY_WEB_BACKENDS" in dockerfile
|
2026-08-10 04:31:41 -03:00
|
|
|
assert '"pre_internal_route"' in dockerfile
|
|
|
|
|
assert "pre_internal_route hook failed" in dockerfile
|
2026-08-10 05:16:30 -03:00
|
|
|
assert '"pre_subagent_route"' in dockerfile
|
|
|
|
|
assert "pre_subagent_route hook failed" in dockerfile
|
2026-08-15 21:48:30 -03:00
|
|
|
assert "agent._hermes_explicit_model_pick = bool" in dockerfile
|
|
|
|
|
assert "agent._hermes_oneshot = True" in dockerfile
|
|
|
|
|
assert 'getattr(parent_agent, "_hermes_oneshot", False)' in dockerfile
|
2026-08-13 00:12:02 -03:00
|
|
|
assert 'var != "CLAUDE_CODE_OAUTH_TOKEN"' in dockerfile
|
|
|
|
|
assert '"source": "managed_claude_code_subscription"' in dockerfile
|
|
|
|
|
assert '"name": "Claude Code subscription (Switchyard)"' in dockerfile
|
|
|
|
|
assert "Anthropic API / direct OAuth (not used by Switchyard)" in dockerfile
|
2026-08-13 02:49:41 -03:00
|
|
|
assert "scheduleTerminalPaint" in dockerfile
|
|
|
|
|
assert "sessionTree.childrenByParent" in dockerfile
|
2026-08-15 13:58:42 -03:00
|
|
|
assert "SessionActivityPanel" in dockerfile
|
|
|
|
|
assert "include_children=${includeChildren}" in dockerfile
|
|
|
|
|
assert "_dashboard_session_is_active" in dockerfile
|
2026-08-16 01:10:47 -03:00
|
|
|
assert 'searchParams.get("lineage_root") || resumeParam' in dockerfile
|
|
|
|
|
assert "followActiveLineage" in dockerfile
|
|
|
|
|
assert 'next.delete("lineage_root")' in dockerfile
|
2026-08-15 13:58:42 -03:00
|
|
|
activity = (
|
|
|
|
|
ROOT / "dockerfiles" / "hermes-session-activity-panel.tsx"
|
|
|
|
|
).read_text()
|
|
|
|
|
assert "Live worker activity" in activity
|
|
|
|
|
assert "api.getSessionMessages" in activity
|
|
|
|
|
assert "aria-live=\"polite\"" in activity
|
2026-08-15 17:58:47 -03:00
|
|
|
assert "role=\"log\"" in activity
|
|
|
|
|
assert "aria-busy={running && messages.length === 0}" in activity
|
|
|
|
|
assert 'detail.source !== "api_server"' not in activity
|
2026-08-15 13:58:42 -03:00
|
|
|
assert "Poll-backed transcript; terminal rendering is not required." in activity
|
2026-08-16 01:10:47 -03:00
|
|
|
assert "DEFAULT_VISIBLE_MESSAGES = 250" in activity
|
|
|
|
|
assert "current + DEFAULT_VISIBLE_MESSAGES" in activity
|
|
|
|
|
assert 'Show up to{" "}' in activity
|
|
|
|
|
assert "Math.min(" in activity
|
|
|
|
|
assert "visibleMessages.map" in activity
|
|
|
|
|
assert "response.total_messages" in activity
|
|
|
|
|
assert "visibleLimit)," in activity
|
|
|
|
|
assert "function displayText(value: unknown)" in activity
|
|
|
|
|
assert "open && (" in activity
|
2026-08-16 03:33:41 -03:00
|
|
|
assert 'document.getElementById("hermes-resume-bootstrap")?.remove()' in activity
|
|
|
|
|
assert 'id="hermes-resume-bootstrap"' in dockerfile
|
|
|
|
|
assert 'role="status"' in dockerfile
|
|
|
|
|
assert 'aria-busy="true"' in dockerfile
|
|
|
|
|
assert "Opening Hermes worker activity…" in dockerfile
|
|
|
|
|
assert 'params.has("resume")' in dockerfile
|
2026-08-13 02:49:41 -03:00
|
|
|
assert "Open Telegram setup" in dockerfile
|
2026-08-08 23:24:58 -03:00
|
|
|
|
|
|
|
|
|
2026-08-15 22:32:08 -03:00
|
|
|
def test_telegram_router_local_build_defaults_to_cluster_architecture():
|
|
|
|
|
dockerfile = (
|
|
|
|
|
ROOT / "dockerfiles" / "Dockerfile.hermes-chat-router"
|
|
|
|
|
).read_text(encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
assert "ARG TARGETOS=linux" in dockerfile
|
|
|
|
|
assert "ARG TARGETARCH=arm64" in dockerfile
|
|
|
|
|
assert "GOOS=${TARGETOS} GOARCH=${TARGETARCH}" in dockerfile
|
|
|
|
|
|
|
|
|
|
|
2026-08-09 01:04:27 -03:00
|
|
|
def test_chat_oauth_allows_stale_service_worker_retirement():
|
|
|
|
|
documents = _documents(HERMES / "oauth2-proxy.yaml")
|
|
|
|
|
deployment = next(
|
|
|
|
|
document
|
|
|
|
|
for document in documents
|
|
|
|
|
if document["kind"] == "Deployment"
|
|
|
|
|
and document["metadata"]["name"] == "oauth2-proxy-hermes-chat"
|
|
|
|
|
)
|
|
|
|
|
args = deployment["spec"]["template"]["spec"]["containers"][0]["args"]
|
|
|
|
|
|
2026-08-09 01:08:31 -03:00
|
|
|
assert "--skip-auth-route=GET=^/sw[.]js([?].*)?$" in args
|
2026-08-09 12:55:20 -03:00
|
|
|
assert "--custom-templates-dir=/etc/oauth2-proxy/templates" in args
|
|
|
|
|
|
2026-08-09 13:40:58 -03:00
|
|
|
template = (HERMES / "oauth2-proxy-templates" / "error.html").read_text()
|
2026-08-13 02:49:41 -03:00
|
|
|
assert 'http-equiv="refresh"' not in template
|
2026-08-09 13:33:36 -03:00
|
|
|
assert "Unable to find a valid CSRF token" in template
|
|
|
|
|
assert "expired or was already used" in template
|
2026-08-13 02:49:41 -03:00
|
|
|
assert "sessionStorage" in template
|
|
|
|
|
assert "Automatic recovery paused" in template
|
|
|
|
|
assert "/start?rd=/" in template
|
2026-08-09 13:40:58 -03:00
|
|
|
container = deployment["spec"]["template"]["spec"]["containers"][0]
|
|
|
|
|
assert "v7.15.3@sha256:10a1165743a192e" in container["image"]
|
|
|
|
|
assert "--cookie-csrf-per-request=true" in args
|
|
|
|
|
assert "--cookie-csrf-per-request-limit=8" in args
|
|
|
|
|
assert "--trusted-proxy-ip=10.42.0.0/16" in args
|
2026-08-10 00:42:37 -03:00
|
|
|
assert "--api-route=^/api/" in args
|
|
|
|
|
assert "--api-route=^/health$" in args
|
|
|
|
|
assert "--cookie-expire=168h" in args
|
2026-08-11 01:51:30 -03:00
|
|
|
assert "--cookie-refresh=19m" in args
|
|
|
|
|
assert "--session-store-type=redis" in args
|
|
|
|
|
assert any(
|
|
|
|
|
arg.startswith("--redis-connection-url=redis://hermes-oauth-sessions.")
|
|
|
|
|
for arg in args
|
|
|
|
|
)
|
2026-08-10 00:42:37 -03:00
|
|
|
|
2026-08-13 02:49:41 -03:00
|
|
|
agent = _documents(HERMES / "agent-deployment.yaml")[0]
|
|
|
|
|
agent_pod = agent["spec"]["template"]["spec"]
|
|
|
|
|
agent_oauth = next(
|
|
|
|
|
container for container in agent_pod["containers"]
|
|
|
|
|
if container["name"] == "oauth2-proxy"
|
|
|
|
|
)
|
|
|
|
|
assert "--custom-templates-dir=/etc/oauth2-proxy/templates" in agent_oauth["args"]
|
|
|
|
|
assert {
|
|
|
|
|
"name": "oauth-templates",
|
|
|
|
|
"mountPath": "/etc/oauth2-proxy/templates",
|
|
|
|
|
"readOnly": True,
|
|
|
|
|
} in agent_oauth["volumeMounts"]
|
|
|
|
|
agent_template_volume = next(
|
|
|
|
|
volume for volume in agent_pod["volumes"]
|
|
|
|
|
if volume["name"] == "oauth-templates"
|
|
|
|
|
)
|
|
|
|
|
assert agent_template_volume["configMap"]["name"] == (
|
|
|
|
|
"hermes-chat-oauth-templates"
|
|
|
|
|
)
|
|
|
|
|
|
2026-08-10 00:42:37 -03:00
|
|
|
|
|
|
|
|
def test_webui_recovers_auth_and_labels_session_scoped_controls():
|
|
|
|
|
dockerfile = (ROOT / "dockerfiles" / "Dockerfile.hermes-webui").read_text()
|
|
|
|
|
|
|
|
|
|
assert "res.status===401||res.status===403" in dockerfile
|
|
|
|
|
assert "window.location.assign('/oauth2/start?rd='" in dockerfile
|
|
|
|
|
assert "childrenExpanded?'▾ ':'▸ '" in dockerfile
|
2026-08-11 20:22:26 -03:00
|
|
|
assert "'atlas/auto/maximum': 'Automatic · Maximum'" in dockerfile
|
2026-08-16 15:59:09 -03:00
|
|
|
assert "hermes-webui-telegram-project-patch.py" in dockerfile
|
|
|
|
|
telegram_project_patch = (
|
|
|
|
|
ROOT / "dockerfiles" / "hermes-webui-telegram-project-patch.py"
|
|
|
|
|
).read_text()
|
|
|
|
|
assert "TELEGRAM_PROJECT_NAME = 'Telegram'" in telegram_project_patch
|
|
|
|
|
assert "session_key.startswith('telegram-topic-')" in telegram_project_patch
|
2026-08-11 20:22:26 -03:00
|
|
|
|
|
|
|
|
router = (ROOT / "dockerfiles" / "hermes-webui-router.js").read_text()
|
2026-08-18 06:55:51 -03:00
|
|
|
assert "'atlas/auto/fast':'Automatic · Fast'" in router
|
2026-08-11 20:22:26 -03:00
|
|
|
assert "'atlas/manual/codex/sol':'Codex · SOL'" in router
|
|
|
|
|
assert "'atlas/manual/claude/opus':'Claude · Opus'" in router
|
|
|
|
|
assert "watchModelOptions('modelSelect')" in router
|
|
|
|
|
assert "watchModelOptions('settingsModel')" in router
|
2026-08-10 00:42:37 -03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_chat_voice_uses_private_jetson_services_and_shared_auto_route():
|
|
|
|
|
statefulset = _documents(HERMES / "chat-statefulset.yaml")[0]
|
|
|
|
|
containers = statefulset["spec"]["template"]["spec"]["containers"]
|
|
|
|
|
webui = next(item for item in containers if item["name"] == "webui")
|
2026-08-24 04:45:15 -03:00
|
|
|
env = {item["name"]: item["value"] for item in webui["env"] if "value" in item}
|
2026-08-10 00:42:37 -03:00
|
|
|
|
|
|
|
|
assert env["HERMES_STT_URL"] == (
|
|
|
|
|
"http://hermes-stt.hermes.svc.cluster.local:9000/v1/audio/transcriptions"
|
|
|
|
|
)
|
2026-08-23 18:29:02 -03:00
|
|
|
assert env["HERMES_WEBUI_ATLAS_STT_STREAM_URL"] == (
|
|
|
|
|
"http://hermes-stt.hermes.svc.cluster.local:9000/v1/audio/transcriptions/stream"
|
|
|
|
|
)
|
2026-08-10 00:42:37 -03:00
|
|
|
assert env["HERMES_WEBUI_ATLAS_TTS_URL"] == (
|
|
|
|
|
"http://hermes-tts.hermes.svc.cluster.local:9001/v1/audio/speech"
|
|
|
|
|
)
|
2026-08-23 18:29:02 -03:00
|
|
|
assert env["HERMES_WEBUI_ATLAS_TTS_STREAM_URL"] == (
|
|
|
|
|
"http://hermes-tts.hermes.svc.cluster.local:9001/v1/audio/speech/stream"
|
|
|
|
|
)
|
2026-08-10 00:42:37 -03:00
|
|
|
assert "hermes_stt_client.py" in env["HERMES_LOCAL_STT_COMMAND"]
|
|
|
|
|
|
|
|
|
|
configmap = _documents(HERMES / "chat-configmap.yaml")[0]
|
|
|
|
|
config = yaml.safe_load(configmap["data"]["config.yaml"])
|
|
|
|
|
assert config["stt"] == {
|
|
|
|
|
"enabled": True,
|
|
|
|
|
"provider": "local_command",
|
2026-08-10 01:30:50 -03:00
|
|
|
"local": {"model": "small", "language": "auto"},
|
2026-08-10 00:42:37 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dockerfile = (ROOT / "dockerfiles" / "Dockerfile.hermes-webui").read_text()
|
|
|
|
|
assert "hermes-webui-atlas-patch.py" in dockerfile
|
|
|
|
|
assert "hermes-webui-atlas-voice.js" in dockerfile
|
|
|
|
|
voice_script = (ROOT / "dockerfiles" / "hermes-webui-atlas-voice.js").read_text()
|
|
|
|
|
assert "/api/transcribe/capability" in voice_script
|
|
|
|
|
assert "/api/transcribe" in voice_script
|
|
|
|
|
assert "/api/tts" in voice_script
|
|
|
|
|
assert "speakResponse(generation)" in voice_script
|
2026-08-23 18:29:02 -03:00
|
|
|
assert "window._atlasAdaptiveChunks" in voice_script
|
|
|
|
|
assert "preferredCut(firstSlice,40,52,60)" in voice_script
|
|
|
|
|
assert "preferredCut(remaining,100,120,140)" in voice_script
|
2026-08-23 16:50:48 -03:00
|
|
|
assert "hermes-atlas-voice-latency-v2" in voice_script
|
|
|
|
|
assert "hermes-voice-silence-ms','1100'" in voice_script
|
2026-08-23 18:29:02 -03:00
|
|
|
assert "const nextPrepared=next.then" in voice_script
|
|
|
|
|
assert "At most one later synthesis request exists" in voice_script
|
|
|
|
|
assert "restartSoon(turn.token,300)" in voice_script
|
2026-08-10 01:47:51 -03:00
|
|
|
assert "constraints.voiceIsolation=true" in voice_script
|
|
|
|
|
assert "highpass.frequency.value=140" in voice_script
|
|
|
|
|
assert "Math.max(0.04,noiseFloor*2.4+0.006)" in voice_script
|
2026-08-23 15:59:43 -03:00
|
|
|
assert "recorder.start();" in voice_script
|
|
|
|
|
assert "recorder.start(250)" not in voice_script
|
hermes(voice): interim-ack tail, natural fillers, unified audio + output picker
Three conversation-mode fixes in one pass:
- Interim-acknowledgement truncation: when an interim ack folds into the
hidden worklog segment mid-speech, the retained unspoken tail is now
flushed and spoken in full before the Thinking transition, and a
distinct follow-up message is chunked from its own start and queued
after the interim drains (no more 'stops after the first clause, rest
resurfaces with the next message').
- Natural thinking fillers: brief per-language interjections (Umm/Hmm/
One sec; Mmm/A ver; Хм/Секунду) on genuine >1.9s thinking gaps only,
non-repeating, answer-preempting, mute-aware.
- One unified audio sink for every spoken output (reply, cues, fillers,
WAV fallback) - fixes cues playing the loudspeaker while the reply
used a different output - plus a tidy corner output-device selector
(enumerateDevices + setSinkId, feature-detected, session-only) styled
like the language selector. Also realigns two STT-server decode-param
assertions to the dict form from the STT tuning commit. 284 voice tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BvMSXH8VH2tMWXanb8SJdf
2026-08-24 17:32:13 -03:00
|
|
|
assert "function flushRetainedTail(turn)" in voice_script # folded interim tail is spoken, never dropped
|
2026-08-10 01:47:51 -03:00
|
|
|
|
|
|
|
|
stt_server = (ROOT / "dockerfiles" / "hermes-jetson-stt-server.py").read_text()
|
|
|
|
|
assert "def _repetitive_token" in stt_server
|
hermes(voice): interim-ack tail, natural fillers, unified audio + output picker
Three conversation-mode fixes in one pass:
- Interim-acknowledgement truncation: when an interim ack folds into the
hidden worklog segment mid-speech, the retained unspoken tail is now
flushed and spoken in full before the Thinking transition, and a
distinct follow-up message is chunked from its own start and queued
after the interim drains (no more 'stops after the first clause, rest
resurfaces with the next message').
- Natural thinking fillers: brief per-language interjections (Umm/Hmm/
One sec; Mmm/A ver; Хм/Секунду) on genuine >1.9s thinking gaps only,
non-repeating, answer-preempting, mute-aware.
- One unified audio sink for every spoken output (reply, cues, fillers,
WAV fallback) - fixes cues playing the loudspeaker while the reply
used a different output - plus a tidy corner output-device selector
(enumerateDevices + setSinkId, feature-detected, session-only) styled
like the language selector. Also realigns two STT-server decode-param
assertions to the dict form from the STT tuning commit. 284 voice tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BvMSXH8VH2tMWXanb8SJdf
2026-08-24 17:32:13 -03:00
|
|
|
assert '"compression_ratio_threshold": 2.0' in stt_server
|
|
|
|
|
assert '"no_speech_threshold": 0.5' in stt_server
|
2026-08-10 01:47:51 -03:00
|
|
|
|
|
|
|
|
|
2026-08-23 18:29:02 -03:00
|
|
|
def test_worker_webui_rollouts_do_not_repeat_completed_state_migration():
|
|
|
|
|
deployment = _documents(HERMES / "deployment.yaml")[0]
|
|
|
|
|
pod_spec = deployment["spec"]["template"]["spec"]
|
|
|
|
|
init = next(
|
|
|
|
|
item
|
|
|
|
|
for item in pod_spec["initContainers"]
|
|
|
|
|
if item["name"] == "patch-api-server-sessions"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert pod_spec["securityContext"]["fsGroupChangePolicy"] == "OnRootMismatch"
|
|
|
|
|
command = "\n".join(init["command"])
|
|
|
|
|
assert "patch_api_server_sessions.py" in command
|
|
|
|
|
assert "migrate_api_session_lineage.py" not in command
|
|
|
|
|
|
|
|
|
|
|
2026-08-10 01:47:51 -03:00
|
|
|
def test_voice_transcript_filter_removes_fan_hallucinations(monkeypatch):
|
|
|
|
|
server_path = ROOT / "dockerfiles" / "hermes-jetson-stt-server.py"
|
|
|
|
|
spec = importlib.util.spec_from_file_location("hermes_jetson_stt_server", server_path)
|
|
|
|
|
assert spec and spec.loader
|
|
|
|
|
module = importlib.util.module_from_spec(spec)
|
|
|
|
|
monkeypatch.setitem(sys.modules, "cgi", SimpleNamespace())
|
|
|
|
|
monkeypatch.setitem(
|
|
|
|
|
sys.modules,
|
|
|
|
|
"torch",
|
|
|
|
|
SimpleNamespace(cuda=SimpleNamespace(is_available=lambda: False)),
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setitem(sys.modules, "whisper", SimpleNamespace())
|
|
|
|
|
spec.loader.exec_module(module)
|
|
|
|
|
|
|
|
|
|
result = {
|
|
|
|
|
"segments": [
|
|
|
|
|
{
|
|
|
|
|
"text": " ththththththththth Testing.",
|
|
|
|
|
"no_speech_prob": 0.12,
|
|
|
|
|
"avg_logprob": -0.2,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"text": " background hum",
|
|
|
|
|
"no_speech_prob": 0.82,
|
|
|
|
|
"avg_logprob": -0.9,
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
assert module._clean_transcript(result) == "Testing."
|
2026-08-10 00:42:37 -03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_voice_models_are_baked_and_runtime_has_no_public_egress():
|
|
|
|
|
stt_dockerfile = (ROOT / "dockerfiles" / "Dockerfile.hermes-jetson-stt").read_text()
|
|
|
|
|
tts_dockerfile = (ROOT / "dockerfiles" / "Dockerfile.hermes-jetson-tts").read_text()
|
|
|
|
|
assert "ADD --checksum=sha256:aff26ae4" in stt_dockerfile
|
2026-08-10 01:30:50 -03:00
|
|
|
assert "ADD --checksum=sha256:9ecf7799" in stt_dockerfile
|
2026-08-10 00:42:37 -03:00
|
|
|
assert "--chmod=0444" in stt_dockerfile
|
2026-08-10 01:00:20 -03:00
|
|
|
assert "chmod 0555 /opt/models /opt/models/whisper" in stt_dockerfile
|
2026-08-10 00:42:37 -03:00
|
|
|
assert "HERMES_STT_CACHE=/opt/models/whisper" in stt_dockerfile
|
|
|
|
|
assert "ADD --checksum=sha256:4cabf7c3" in tts_dockerfile
|
|
|
|
|
assert "ADD --checksum=sha256:db42b97d" in tts_dockerfile
|
2026-08-20 18:53:43 +00:00
|
|
|
assert "ADD --checksum=sha256:b3a6e47b57b8c7fbe6a0ce2518161a50f59a9cdd8a50835c02cb02bdd6206c18" in tts_dockerfile
|
|
|
|
|
assert "ADD --checksum=sha256:95a23eb4d42909d38df73bb9ac7f45f597dbfcde2d1bf9526fdeaf5466977d77" in tts_dockerfile
|
|
|
|
|
assert "ADD --checksum=sha256:8ff38212d23da300bbe3705c645e6e5b9475f0bfde01558eb17813e22acaaaaa" in tts_dockerfile
|
|
|
|
|
assert "ADD --checksum=sha256:c2ec28bb38e2b59e93b959b3e40348c1afebbd272f30fed5d41205d08e98a9d7" in tts_dockerfile
|
|
|
|
|
assert "ADD --checksum=sha256:3ef40a71ea63852cd8ab7e6fa7d2ecdcfa67a0b47c9c48e3f10e02ee02083ea0" in tts_dockerfile
|
|
|
|
|
assert "ADD --checksum=sha256:1afc81f703c0e4cb3b4d7c0dca096b8b54a98806807f0170cf5eb5557723c12d" in tts_dockerfile
|
|
|
|
|
assert tts_dockerfile.count("--chmod=0444") == 12
|
|
|
|
|
assert "/opt/models/piper/en_US-amy-medium.onnx" in tts_dockerfile
|
|
|
|
|
assert "/opt/models/piper/ru_RU-irina-medium.onnx" in tts_dockerfile
|
|
|
|
|
assert "/opt/models/piper/es_MX-claude-high.onnx" in tts_dockerfile
|
2026-08-10 01:00:20 -03:00
|
|
|
assert "chmod 0555 /opt/models /opt/models/piper" in tts_dockerfile
|
2026-08-10 00:42:37 -03:00
|
|
|
assert "HERMES_TTS_CACHE=/opt/models/piper" in tts_dockerfile
|
2026-08-20 18:53:43 +00:00
|
|
|
assert "HERMES_TTS_VOICE=en_US-amy-medium" in tts_dockerfile
|
2026-08-10 00:42:37 -03:00
|
|
|
tts_server = (ROOT / "dockerfiles" / "hermes-jetson-tts-server.py").read_text()
|
|
|
|
|
assert "download_voice" not in tts_server
|
|
|
|
|
assert "baked Piper voice is missing" in tts_server
|
2026-08-20 18:53:43 +00:00
|
|
|
assert "session_options.intra_op_num_threads = threads" in tts_server
|
|
|
|
|
assert 'LANGUAGE_VOICE_MAP = {' in tts_server
|
|
|
|
|
assert '"en": "en_US-amy-medium"' in tts_server
|
|
|
|
|
assert '"ru": "ru_RU-irina-medium"' in tts_server
|
|
|
|
|
assert '"es": "es_MX-claude-high"' in tts_server
|
2026-08-10 00:42:37 -03:00
|
|
|
|
|
|
|
|
policies = _documents(HERMES / "networkpolicy.yaml")
|
|
|
|
|
voice_policy = next(
|
|
|
|
|
item for item in policies if item["metadata"]["name"] == "hermes-private-voice"
|
|
|
|
|
)
|
|
|
|
|
assert voice_policy["spec"]["policyTypes"] == ["Ingress", "Egress"]
|
|
|
|
|
assert not any(
|
|
|
|
|
"ipBlock" in destination
|
|
|
|
|
for rule in voice_policy["spec"]["egress"]
|
|
|
|
|
for destination in rule.get("to", [])
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_voice_workloads_have_deliberate_xavier_placement():
|
|
|
|
|
documents = _documents(HERMES / "voice-deployment.yaml")
|
|
|
|
|
deployments = {
|
|
|
|
|
item["metadata"]["name"]: item
|
|
|
|
|
for item in documents
|
|
|
|
|
if item["kind"] == "Deployment"
|
|
|
|
|
}
|
|
|
|
|
stt = deployments["hermes-stt"]["spec"]["template"]["spec"]
|
|
|
|
|
tts = deployments["hermes-tts"]["spec"]["template"]["spec"]
|
|
|
|
|
|
|
|
|
|
assert "@sha256:" in stt["containers"][0]["image"]
|
|
|
|
|
assert "@sha256:" in tts["containers"][0]["image"]
|
|
|
|
|
assert stt["nodeSelector"] == {"kubernetes.io/hostname": "titan-21"}
|
2026-08-11 01:14:46 -03:00
|
|
|
assert tts["nodeSelector"] == {"kubernetes.io/hostname": "titan-21"}
|
2026-08-10 00:49:52 -03:00
|
|
|
assert stt["automountServiceAccountToken"] is False
|
|
|
|
|
assert tts["automountServiceAccountToken"] is False
|
|
|
|
|
assert stt["enableServiceLinks"] is False
|
|
|
|
|
assert tts["enableServiceLinks"] is False
|
2026-08-10 00:42:37 -03:00
|
|
|
assert stt["runtimeClassName"] == "nvidia"
|
2026-08-10 01:06:03 -03:00
|
|
|
assert stt["securityContext"]["supplementalGroups"] == [44]
|
2026-08-10 00:42:37 -03:00
|
|
|
stt_resources = stt["containers"][0]["resources"]
|
2026-08-10 01:47:51 -03:00
|
|
|
stt_env = {
|
|
|
|
|
item["name"]: item["value"] for item in stt["containers"][0]["env"]
|
|
|
|
|
}
|
|
|
|
|
assert stt_env["NVIDIA_DRIVER_CAPABILITIES"] == "compute,utility"
|
2026-08-10 00:42:37 -03:00
|
|
|
assert stt_resources["requests"]["nvidia.com/gpu.shared"] == 1
|
|
|
|
|
assert stt_resources["limits"]["nvidia.com/gpu.shared"] == 1
|
|
|
|
|
assert "nvidia.com/gpu.shared" not in tts["containers"][0]["resources"]["requests"]
|
2026-08-10 01:11:04 -03:00
|
|
|
tts_env = {
|
|
|
|
|
item["name"]: item["value"] for item in tts["containers"][0]["env"]
|
|
|
|
|
}
|
2026-08-23 15:54:56 -03:00
|
|
|
# Keep the deployment neutral so the old and new immutable images can
|
|
|
|
|
# cross one Flux rollout safely; each image owns its compatible default.
|
|
|
|
|
assert "HERMES_TTS_VOICE" not in tts_env
|
2026-08-23 15:59:43 -03:00
|
|
|
assert tts_env["HERMES_TTS_ONNX_THREADS"] == "4"
|
2026-08-10 01:11:04 -03:00
|
|
|
assert tts["containers"][0]["resources"]["limits"]["cpu"] == "4"
|
2026-08-10 00:42:37 -03:00
|
|
|
assert all("hostPath" not in volume for volume in stt["volumes"])
|
|
|
|
|
assert all("hostPath" not in volume for volume in tts["volumes"])
|