fix(audio): gate the executor's chain-plan startAudio behind the user-stop latch

Second field finding: a user-stopped engine came back when a song load
ran a rig_builder chain plan with startAudio:true - the executor calls
native startAudio directly in the main process, bypassing the IPC latch
check entirely (plan 6.1's second writer).

Decision (strict 8.3): only the device screen's user start resumes.
lease-bridge gains gateNativeAudio(), a proxy over the native accessor
that suppresses startAudio while latched (log-once telemetry line);
audio-bridge hands the executor the gated accessor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
OmikronApex
2026-07-15 01:37:34 +02:00
co-authored by Claude Fable 5
parent bb0f79e0be
commit a142b0f3de
3 changed files with 57 additions and 1 deletions
+25
View File
@@ -127,6 +127,31 @@ test('user-stop latch: fresh demands and legacy starts held off until user start
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);