Files
hermes-webui/tests/test_6673_notification_renotify.py

95 lines
3.3 KiB
Python

"""Regression coverage for same-session notification toasts (#6673).
Desktop notifications via the Notification API only pop a toast banner the
first time for a given tag: on Windows the notification persists in the
Action Center, and subsequent same-session turn completions produce no
visible toast. The fix keeps the session-scoped `tag` (so stale
notifications are still replaced in the notification center) but sets
`renotify: true` so every turn completion re-pops a toast banner.
"""
import json
import shutil
import subprocess
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
MESSAGES_SRC = (ROOT / "static" / "messages.js").read_text(encoding="utf-8")
NODE = shutil.which("node")
pytestmark = pytest.mark.skipif(NODE is None, reason="node not on PATH")
_DRIVER_SRC = r"""
const fs = require('fs');
const src = fs.readFileSync(process.argv[2], 'utf8');
function extract(source, name) {
const start = source.indexOf('function ' + name + '(');
if (start === -1) throw new Error('function not found: ' + name);
const bodyStart = source.indexOf('){', start) + 1;
let depth = 0;
for (let i = bodyStart; i < source.length; i++) {
if (source[i] === '{') depth++;
else if (source[i] === '}') {
depth--;
if (depth === 0) return source.slice(start, i + 1);
}
}
throw new Error('function body did not close: ' + name);
}
// Stub the globals `_notificationOptions` touches so the extracted function
// runs standalone: S (session state), location, and _sessionUrlForSid.
let S = { session: { session_id: 'sess-6673' } };
global.S = S;
global.location = { origin: 'http://localhost:8080', href: 'http://localhost:8080/' };
global._sessionUrlForSid = (sid) => '/#/s/' + sid;
eval(extract(src, '_notificationOptions'));
const withSid = _notificationOptions('Turn done', { sid: 'sess-6673' });
// Fallback path: no options.sid AND no session in scope -> generic tag.
S = null;
const withoutSid = _notificationOptions('Turn done', {});
console.log(JSON.stringify({
withSid: { renotify: withSid.renotify, tag: withSid.tag, body: withSid.body },
withoutSid: { renotify: withoutSid.renotify, tag: withoutSid.tag },
}));
"""
def _run_driver(tmp_path) -> dict:
driver = tmp_path / "driver_6673.js"
driver.write_text(_DRIVER_SRC, encoding="utf-8")
result = subprocess.run(
[NODE, str(driver), str(ROOT / "static" / "messages.js")],
capture_output=True,
text=True,
timeout=30,
)
assert result.returncode == 0, f"node failed: {result.stderr}"
return json.loads(result.stdout)
def test_same_session_notification_renotifies(tmp_path):
"""#6673: a notification for a session-scoped tag must re-pop a toast on
every turn completion instead of being swallowed after the first pop."""
out = _run_driver(tmp_path)
assert out["withSid"]["renotify"] is True
assert out["withSid"]["body"] == "Turn done"
def test_session_scoped_tag_is_preserved(tmp_path):
"""The session-scoped tag stays so stale notifications are still replaced
in the notification center (no duplicate clutter)."""
out = _run_driver(tmp_path)
assert out["withSid"]["tag"] == "hermes-sess-6673"
def test_fallback_tag_without_session(tmp_path):
out = _run_driver(tmp_path)
assert out["withoutSid"]["tag"] == "hermes-webui"
assert out["withoutSid"]["renotify"] is True