import test from 'node:test'; import assert from 'node:assert/strict'; import { EventEmitter } from 'node:events'; import { VoiceLoop, fastCommand } from '../daemon/voice-loop.js'; import { WakeEngine } from '../daemon/wake-engine.js'; import { VadSegmenter } from '../daemon/vad.js'; import { PipeWireCapture, pcmRms } from '../daemon/audio-pipewire.js'; import { SentenceBuffer, isMeaningfulTranscript } from '../daemon/transcript.js'; test('Phase 4 transcript filtering and sentence buffering are deterministic', () => { assert.equal(isMeaningfulTranscript('[BLANK_AUDIO]'), false); assert.equal(isMeaningfulTranscript('hi'), false); assert.equal(isMeaningfulTranscript('what time is it'), true); const out = []; const buffer = new SentenceBuffer({ onSentence: (s) => out.push(s) }); buffer.push('First sentence. Second'); buffer.push(' sentence!'); buffer.flush(); assert.deepEqual(out, ['First sentence.', 'Second sentence!']); }); test('Phase 4 wake engine emits only configured local detections', () => { const wake = new WakeEngine({ phrases: ['hey jarvis'], detect: () => ({ phrase: 'hey jarvis' }) }); const heard = []; wake.on('wake', (phrase) => heard.push(phrase)); wake.push(Buffer.from([0])); assert.deepEqual(heard, ['hey jarvis']); wake.pause(); wake.push(Buffer.from([0])); assert.equal(heard.length, 1); }); test('Phase 4 VAD emits a bounded utterance after silence', () => { const vad = new VadSegmenter({ frameMs: 100, params: { threshold: 0.6, minSpeechDurationMs: 100, minSilenceDurationMs: 200 } }); const utterances = []; vad.on('utterance', (audio) => utterances.push(audio)); const loud = Buffer.alloc(3200); for (let i = 0; i < loud.length; i += 2) loud.writeInt16LE(20_000, i); const quiet = Buffer.alloc(3200); vad.push(loud); vad.push(quiet); vad.push(quiet); assert.equal(utterances.length, 1); assert.ok(utterances[0].length > 0); assert.ok(pcmRms(loud) > 0); }); test('Phase 4 fast commands are handled before the harness', () => { assert.equal(fastCommand('Hands Off'), 'cancel'); assert.equal(fastCommand('take the wheel'), 'computer'); assert.equal(fastCommand('ordinary question'), null); }); test('Phase 4 feedback gate drops capture while speaking and during cooldown', () => { const capture = new EventEmitter(); capture.start = () => {}; capture.stop = () => {}; const wake = new WakeEngine({ detect: () => ({ phrase: 'hey jarvis' }) }); const vad = new VadSegmenter({ frameMs: 20 }); let pushed = 0; const original = vad.push.bind(vad); vad.push = (x) => { pushed++; original(x); }; const daemon = new EventEmitter(); daemon.state = 'LISTENING'; const loop = new VoiceLoop({ daemon, capture, wake, vad, now: () => 1000 }); loop.running = true; loop.isSpeaking = true; loop.pushAudio(Buffer.alloc(3200)); assert.equal(pushed, 0); loop.isSpeaking = false; loop.cooldownUntil = 1200; loop.pushAudio(Buffer.alloc(3200)); assert.equal(pushed, 0); }); test('Phase 4 PipeWire capture uses a named 16 kHz mono node', () => { let args; const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); child.kill = () => {}; const capture = new PipeWireCapture({ spawnImpl: (_cmd, received) => { args = received; return child; } }); capture.start(); assert.deepEqual(args, ['--record', '--raw', '--format', 's16', '--rate', '16000', '--channels', '1', '--name', 'Jarvis']); capture.stop(); });