mirror of
https://github.com/R0m1k3/EveFlow.git
synced 2026-10-12 01:39:13 +02:00
Over the runs transport Hermes never reports the session it attached to a
run in the SSE stream, only in GET /v1/runs/{id}. EveFlow only read it in the
polling fallback, so the locally generated id was sent again and again and
each message opened a fresh Hermes session, losing the conversation context.
The client now adopts the run's session id after every run.
Long answers are now spoken as a digest: the first sentences (configurable,
4 by default) plus the closing question, followed by a short notice that the
full text is on screen. The default instructions ask Hermes to open long
replies with the essentials and to rely on the ongoing conversation.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ln4KL1feHtnV4nee4sZFhM
60 lines
2.5 KiB
TypeScript
60 lines
2.5 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { DEFAULT_SETTINGS, useSettings } from '../src/state/settings';
|
|
|
|
const enqueue = vi.fn((text: string) => /[\p{L}\p{N}]/u.test(text.replace(/```[\s\S]*?```/g, '')));
|
|
const speak = vi.fn();
|
|
const stop = vi.fn();
|
|
vi.mock('../src/services/voice/tts', () => ({
|
|
TtsEngine: class {
|
|
enqueue = enqueue;
|
|
speak = speak;
|
|
stop = stop;
|
|
onState() { return () => undefined; }
|
|
updateConfig() { /* noop */ }
|
|
get isActive() { return false; }
|
|
}
|
|
}));
|
|
|
|
const { speech, DIGEST_NOTICE } = await import('../src/services/voice/speech');
|
|
|
|
function stream(text: string, size = 7): void {
|
|
for (let i = 0; i < text.length; i += size) speech.pushStream(text.slice(i, i + size));
|
|
}
|
|
const spoken = () => enqueue.mock.calls.map((c) => c[0]);
|
|
|
|
describe('spoken digest of streamed replies', () => {
|
|
beforeEach(() => {
|
|
enqueue.mockClear();
|
|
speak.mockClear();
|
|
useSettings.setState({ settings: { ...DEFAULT_SETTINGS, speech: { ...DEFAULT_SETTINGS.speech, summarizeReplies: true, replySentences: 2 } } });
|
|
});
|
|
afterEach(() => speech.stop());
|
|
|
|
it('stops after the configured sentences, then adds the closing question and a notice', () => {
|
|
const text = 'Première phrase du rapport. Deuxième phrase utile. Troisième phrase de détail. Quatrième phrase encore. Tu veux la suite ?';
|
|
stream(text);
|
|
speech.endStream(text);
|
|
expect(spoken()).toEqual(['Première phrase du rapport.', 'Deuxième phrase utile.', 'Tu veux la suite ?', DIGEST_NOTICE]);
|
|
});
|
|
|
|
it('speaks short replies in full without any notice', () => {
|
|
const text = 'Une phrase courte. Une seconde phrase';
|
|
stream(text);
|
|
speech.endStream(text);
|
|
expect(spoken()).toEqual(['Une phrase courte.', 'Une seconde phrase']);
|
|
});
|
|
|
|
it('speaks everything when the digest is disabled', () => {
|
|
useSettings.setState({ settings: { ...DEFAULT_SETTINGS, speech: { ...DEFAULT_SETTINGS.speech, summarizeReplies: false } } });
|
|
const text = 'Première phrase du rapport. Deuxième phrase utile. Troisième phrase de détail. Quatrième phrase encore.';
|
|
stream(text);
|
|
speech.endStream(text);
|
|
expect(spoken()).toHaveLength(4);
|
|
});
|
|
|
|
it('digests a reply delivered in one piece', () => {
|
|
speech.sayReply('Première phrase du rapport. Deuxième phrase utile. Troisième phrase de détail. On continue ?');
|
|
expect(speak).toHaveBeenCalledWith(`Première phrase du rapport. Deuxième phrase utile. On continue ? ${DIGEST_NOTICE}`, expect.anything());
|
|
});
|
|
});
|