Files
hermes-agent/apps/desktop/electron/event-dedupe.test.ts
T
Teknium 50ea107d9d fix(desktop): read an auto-TTS reply aloud once when the HUD is open
With `voice.auto_tts` on and HUD mode active, a reply could be spoken
twice: the HUD renderer and the hidden app window both claim
`speak:<messageId>` through `hermes:ambient:claim`, and main collapsed
the two claims with the same 1 s deduper it uses for the turn-end beep.
The app window under the HUD is throttled by Chromium, so its transcript
subscription fires well past 1 s, finds the key pruned, and is told it
owns the cue. Fixes #99717.

The unit was wrong, not the window: a spoken reply is minutes of audio
keyed by a durable message id. `createAmbientClaimArbiter` keeps the
tick-sized window for `sound:*` and holds `speak:*` claims for a
10-minute TTL. No HUD special-casing: the same race exists between any
two windows showing the same chat, and the arbiter fixes them all.

Diagnosis credit: #99810 (@liuhao1024) and #100289 (@shivamjg101),
whose TTL framing this follows.
2026-09-10 09:38:07 -07:00

55 lines
2.0 KiB
TypeScript

import assert from 'node:assert/strict'
import { test } from 'vitest'
import { createAmbientClaimArbiter, createEventDeduper } from './event-dedupe'
test('collapses the same key inside the window (two windows, one event)', () => {
const isDup = createEventDeduper(1000)
assert.equal(isDup('input:s1', 0), false, 'first window claims')
assert.equal(isDup('input:s1', 5), true, 'second window is deduped')
})
test('distinct keys are independent', () => {
const isDup = createEventDeduper(1000)
assert.equal(isDup('input:s1', 0), false)
assert.equal(isDup('approval:s1', 0), false, 'different kind')
assert.equal(isDup('input:s2', 0), false, 'different session')
})
test('re-fires once the window elapses', () => {
const isDup = createEventDeduper(1000)
assert.equal(isDup('turnDone:s1', 0), false)
assert.equal(isDup('turnDone:s1', 999), true, 'still within window')
assert.equal(isDup('turnDone:s1', 1000), false, 'window elapsed → fires again')
})
test('prunes stale keys so the map cannot grow unbounded', () => {
const isDup = createEventDeduper(1000)
for (let i = 0; i < 100; i += 1) {
// Each far-apart key is pruned before the next, so none linger as duplicates.
assert.equal(isDup(`turnDone:s${i}`, i * 2000), false)
}
})
// #99717: the hidden app window under an open HUD claims the same reply late.
test('a spoken reply stays claimed long after the tick window, a beep does not', () => {
const owns = createAmbientClaimArbiter(1000)
assert.equal(owns('speak:m1', 0), true, 'HUD renderer claims the reply')
assert.equal(owns('speak:m1', 5_000), false, 'app window claiming 5 s later stays quiet')
assert.equal(owns('sound:turnDone:s1', 0), true)
assert.equal(owns('sound:turnDone:s1', 5_000), true, 'beep keys still re-fire after the tick window')
})
test('a spoken reply can be claimed again once the speech TTL elapses', () => {
const owns = createAmbientClaimArbiter(1000, 60_000)
assert.equal(owns('speak:m1', 0), true)
assert.equal(owns('speak:m1', 60_000), true)
})