Files
hermes-webui/tests/test_session_attention_sound.py
T
ai-ag2026 00e6d953c2 feat: add distinct attention request sound
Adds a distinct two-tone attention sound (880->660Hz) for approval and
clarify prompts so they are not confused with the existing completion sound,
plus sidebar attention badges + colored rails driven by `attention` metadata
on /api/sessions. Includes lock-safety note: the in-lock
publish_session_list_changed() calls in clarify.py are safe because publish()
only takes the leaf _SESSION_EVENTS_LOCK and never re-acquires clarify._lock
(verified by Opus advisor review).

Co-authored-by: ai-ag2026 <261867348+ai-ag2026@users.noreply.github.com>
2026-05-30 19:40:40 +00:00

82 lines
3.7 KiB
Python

"""Regression tests for attention sounds on session attention state.
Approval/clarify prompts can surface through the sidebar session metadata rather
than the active live SSE stream. The sidebar badge path must play the distinct
attention sound when a session newly needs user input, without blasting sounds
for already-existing badges on initial load.
"""
from pathlib import Path
REPO = Path(__file__).parent.parent
SESSIONS_JS = (REPO / "static" / "sessions.js").read_text(encoding="utf-8")
MESSAGES_JS = (REPO / "static" / "messages.js").read_text(encoding="utf-8")
def _body_from_brace(src: str, brace: int, label: str) -> str:
assert brace >= 0, f"body opening brace not found for: {label}"
depth = 1
i = brace + 1
while i < len(src) and depth:
ch = src[i]
if ch == "{":
depth += 1
elif ch == "}":
depth -= 1
i += 1
assert depth == 0, f"body did not close for: {label}"
return src[brace + 1 : i - 1]
def _function_body(src: str, name: str) -> str:
marker = f"function {name}("
start = src.find(marker)
assert start >= 0, f"function not found: {name}"
signature_end = src.find("){", start)
assert signature_end >= 0, f"function body not found: {name}"
return _body_from_brace(src, signature_end + 1, name)
def test_sidebar_attention_state_plays_distinct_sound_on_new_attention_only():
sync_body = _function_body(SESSIONS_JS, "_syncSessionAttentionSoundState")
apply_body = _function_body(SESSIONS_JS, "_applySessionListPayload")
assert "let _sessionAttentionSoundPrimed = false;" in SESSIONS_JS
assert "const _sessionAttentionSoundState = new Map();" in SESSIONS_JS
assert "_syncSessionAttentionSoundState(_allSessions);" in apply_body
assert "if(!_sessionAttentionSoundPrimed)" in sync_body
assert "_sessionAttentionSoundPrimed=true;" in sync_body
assert "playKey=typeof _attentionSoundKey==='function'?_attentionSoundKey(s.session_id,kind,count):`${s.session_id}:${sig}`;" in sync_body
assert "if(playKey&&typeof playAttentionSound==='function') playAttentionSound(playKey);" in sync_body
assert "playNotificationSound" not in sync_body
def test_attention_signature_tracks_kind_and_count_for_badge_changes():
signature_body = _function_body(SESSIONS_JS, "_sessionAttentionSoundSignature")
assert "attention.kind" in signature_body
assert "Number.isFinite(count)" in signature_body
assert "count<=0" in signature_body
assert "approval" in signature_body
assert "clarify" in signature_body
assert "return `${kind}:${Math.max(1,count||1)}`;" in signature_body
def test_attention_sound_is_softer_short_reverse_of_completion_sound():
attention_body = _function_body(MESSAGES_JS, "playAttentionSound")
completion_body = _function_body(MESSAGES_JS, "playNotificationSound")
assert "osc.type='sine'" in attention_body
assert "window._lastAttentionSoundAt" in attention_body
assert "nowMs-window._lastAttentionSoundAt<900" in attention_body
assert "window._attentionSoundSeenKeys" in attention_body
assert "seen.has(dedupeKey)" in attention_body
assert "seen.set(dedupeKey,nowMs)" in attention_body
assert "300000" in attention_body
assert "osc.frequency.setValueAtTime(880,ctx.currentTime);" in attention_body
assert "osc.frequency.setValueAtTime(660,ctx.currentTime+0.075);" in attention_body
assert "gain.gain.setValueAtTime(0.24,ctx.currentTime);" in attention_body
assert "osc.stop(ctx.currentTime+0.24);" in attention_body
assert "osc.frequency.setValueAtTime(660,ctx.currentTime);" in completion_body
assert "osc.frequency.setValueAtTime(880,ctx.currentTime+0.1);" in completion_body
assert "osc.stop(ctx.currentTime+0.3);" in completion_body