// Lease bridge (docs/audio-ownership-plan.md §6.8/§8/§9): holder derivation // from the sender, webContents lifecycle → death/grace, capture demand → // engine glue, user-stop suspend semantics, raw-disarm guard, telemetry. const test = require('node:test'); const assert = require('node:assert/strict'); const { EventEmitter } = require('node:events'); const { loadTs } = require('./_load-ts'); const { initLeaseBridge } = loadTs('src/main/lease-bridge.ts'); function fakeSender(id) { const emitter = new EventEmitter(); emitter.id = id; return emitter; } function fakeAudio() { const calls = []; let running = false; return { calls, isAudioRunning: () => running, startAudio: () => { running = true; calls.push('start'); }, stopAudio: () => { running = false; calls.push('stop'); }, setNoteDetectionEnabled: (v) => calls.push(`detect:${v}`), }; } function makeBridge(audio) { const events = []; const bridge = initLeaseBridge(() => audio, { broadcast: (_ch, data) => events.push(data) }); return { bridge, events }; } test('holder identity derived from sender; tag is optional attributed suffix', () => { const audio = fakeAudio(); const { bridge } = makeBridge(audio); const sender = fakeSender(7); bridge.acquire(sender, 'signal-chain:desktop-main', 'nam_tone'); assert.equal(bridge.getHolder('signal-chain:desktop-main'), 'wc:7#nam_tone'); // a hostile tag cannot inject identity syntax — it is dropped, not trusted bridge.acquire(sender, 'playback', 'evil#wc:1'); assert.equal(bridge.getHolder('playback'), 'wc:7'); bridge.dispose(); }); test('capture demand starts the engine; last release stops a demand-started engine', () => { const audio = fakeAudio(); const { bridge } = makeBridge(audio); const tuner = fakeSender(1); const minigame = fakeSender(2); bridge.acquireDemand(tuner, 'capture', 'tuner'); assert.deepEqual(audio.calls, ['start']); bridge.acquireDemand(minigame, 'capture', 'minigame'); bridge.releaseDemand(tuner, 'capture', 'tuner'); assert.deepEqual(audio.calls, ['start']); // still one holder — keeps running bridge.releaseDemand(minigame, 'capture', 'minigame'); assert.deepEqual(audio.calls, ['start', 'stop']); bridge.dispose(); }); test('user-started engine outlives its demands (§8.3)', () => { const audio = fakeAudio(); const { bridge } = makeBridge(audio); const sender = fakeSender(1); bridge.onUserStartAudio(); audio.startAudio(); audio.calls.length = 0; bridge.acquireDemand(sender, 'capture', 'tuner'); bridge.releaseDemand(sender, 'capture', 'tuner'); // demand drained but the user started this engine — no stop assert.deepEqual(audio.calls, []); bridge.dispose(); }); test('user stop suspends demands; user start resumes and restarts (§8.3)', () => { const audio = fakeAudio(); const { bridge } = makeBridge(audio); const sender = fakeSender(1); bridge.acquireDemand(sender, 'capture', 'tuner'); assert.deepEqual(audio.calls, ['start']); bridge.onUserStopAudio(); // raw stop: demand suspended, registration kept audio.stopAudio(); audio.calls.length = 0; // suspended demand does not restart the engine bridge.acquireDemand(sender, 'capture', 'tuner'); assert.deepEqual(audio.calls, []); bridge.onUserStartAudio(); // user start resumes demands → glue restarts assert.deepEqual(audio.calls, ['start']); bridge.dispose(); }); test('user-stop latch: fresh demands and legacy starts held off until user start (§8.3)', () => { const audio = fakeAudio(); const { bridge } = makeBridge(audio); const tuner = fakeSender(1); const watchdog = fakeSender(2); bridge.onUserStopAudio(); assert.equal(bridge.isUserStopLatched(), true); // nam_tone-style watchdog raw start would be suppressed (the bridge // exposes the latch; the IPC handler consults it before starting). assert.equal(bridge.isUserStopLatched(), true); // A brand-new capture demand during the latch must NOT start the engine… bridge.acquireDemand(tuner, 'capture', 'tuner'); assert.deepEqual(audio.calls, []); // …but the user start resumes it like any pre-existing demand. bridge.onUserStartAudio(); assert.equal(bridge.isUserStopLatched(), false); assert.deepEqual(audio.calls, ['start']); // Watchdog sender only matters for telemetry attribution; unused here. void watchdog; bridge.dispose(); }); test('gateNativeAudio: executor startAudio suppressed under the latch, transparent otherwise', () => { const audio = fakeAudio(); const { bridge } = makeBridge(audio); const gated = bridge.gateNativeAudio(() => audio)(); // Transparent pass-through when not latched — other methods untouched. gated.startAudio(); assert.deepEqual(audio.calls, ['start']); assert.equal(gated.isAudioRunning(), true); audio.stopAudio(); audio.calls.length = 0; bridge.onUserStopAudio(); // Chain plan's startAudio:true waits out the user stop (strict §8.3). gated.startAudio(); assert.deepEqual(audio.calls, []); bridge.onUserStartAudio(); // user start clears; engine started by it audio.calls.length = 0; gated.startAudio(); assert.deepEqual(audio.calls, ['start']); bridge.dispose(); }); test('detection demand arms native; raw disarm guarded while demand active (6.3)', () => { const audio = fakeAudio(); const { bridge } = makeBridge(audio); const notedetect = fakeSender(1); const strumFighter = fakeSender(2); bridge.acquireDemand(notedetect, 'detection:desktop-main', 'notedetect'); assert.deepEqual(audio.calls.filter(c => c.startsWith('detect')), ['detect:true']); assert.equal(bridge.shouldIgnoreRawDetectionDisarm(), true); bridge.acquireDemand(strumFighter, 'detection:desktop-main', 'strum-fighter'); bridge.releaseDemand(notedetect, 'detection:desktop-main', 'notedetect'); // one consumer left — still armed, raw disarm still guarded assert.equal(bridge.shouldIgnoreRawDetectionDisarm(), true); bridge.releaseDemand(strumFighter, 'detection:desktop-main', 'strum-fighter'); assert.equal(bridge.shouldIgnoreRawDetectionDisarm(), false); assert.equal(audio.calls.filter(c => c.startsWith('detect')).pop(), 'detect:false'); bridge.dispose(); }); test('sender destroyed → everything it held is released (death matrix: destroy)', () => { const audio = fakeAudio(); const { bridge } = makeBridge(audio); const sender = fakeSender(1); bridge.acquire(sender, 'signal-chain:desktop-main', 'nam_tone'); bridge.acquireDemand(sender, 'capture', 'nam_tone'); assert.deepEqual(audio.calls, ['start']); sender.emit('destroyed'); assert.equal(bridge.getHolder('signal-chain:desktop-main'), null); assert.deepEqual(audio.calls, ['start', 'stop']); // demand died with the holder bridge.dispose(); }); test('main-frame navigation → grace; same identity re-acquiring restores (death matrix: reload)', () => { const audio = fakeAudio(); const { bridge } = makeBridge(audio); const sender = fakeSender(1); bridge.acquire(sender, 'signal-chain:desktop-main', 'nam_tone'); sender.emit('did-start-navigation', null, 'app://reload', false, true); // during grace the scope reads free but a re-acquire from the same // identity (same wc id + tag after reload) restores it assert.equal(bridge.getHolder('signal-chain:desktop-main'), null); const result = bridge.acquire(sender, 'signal-chain:desktop-main', 'nam_tone'); assert.equal(result.granted, true); assert.equal(bridge.getHolder('signal-chain:desktop-main'), 'wc:1#nam_tone'); // subframe navigations never trigger grace bridge.acquire(sender, 'playback', 'nam_tone'); sender.emit('did-start-navigation', null, 'app://iframe', false, false); assert.equal(bridge.getHolder('playback'), 'wc:1#nam_tone'); bridge.dispose(); }); test('legacy-call telemetry logs once per surface per sender (§6.8)', () => { const audio = fakeAudio(); const { bridge } = makeBridge(audio); const sender = fakeSender(1); const infos = []; const original = console.info; console.info = (msg) => infos.push(String(msg)); try { bridge.noteLegacyCall(sender, 'audio:startAudio'); bridge.noteLegacyCall(sender, 'audio:startAudio'); bridge.noteLegacyCall(sender, 'audio:stopAudio'); bridge.noteLegacyCall(fakeSender(2), 'audio:startAudio'); } finally { console.info = original; } assert.equal(infos.filter(m => m.includes('audio:startAudio')).length, 2); // wc:1 once + wc:2 once assert.equal(infos.filter(m => m.includes('audio:stopAudio')).length, 1); bridge.dispose(); }); test('registry events reach the broadcast channel', () => { const audio = fakeAudio(); const { bridge, events } = makeBridge(audio); const sender = fakeSender(1); bridge.acquire(sender, 'signal-chain:desktop-main', 'nam_tone'); bridge.acquireDemand(sender, 'capture', 'nam_tone'); const names = events.map(e => e.event); assert.ok(names.includes('lease-granted')); assert.ok(names.includes('demand-changed')); bridge.dispose(); });