@@ -451,3 +451,64 @@ TEST_CASE("input pick: an unknown driver on either side is left to the engine",
451451 noInputDriver.engineDriver = " DirectSound" ;
452452 REQUIRE (audioInputPickPlan (noInputDriver).send );
453453}
454+
455+ // ── 4. Telling the user ────────────────────────────────────────────────────
456+ //
457+ // On 2026-10-06 the MOTU was absent at boot: the restore asked for it, the
458+ // engine refused, the pref was cleared to "follow system" (rightly, so a dead
459+ // device is not replayed every boot) — and nothing on screen said so. The next
460+ // boot followed the system default to a streamer in another room, again
461+ // silently. The decision now says what the user must hear.
462+
463+ TEST_CASE (" notice: a failed request names the output that isn't available" , " [audio][notice]" )
464+ {
465+ SonicPi::AudioSwitchRequest request;
466+ request.output = " motu-xaero" ;
467+ SonicPi::AudioSwitchOutcome outcome;
468+ outcome.success = false ;
469+ outcome.error = " No such device: motu-xaero" ;
470+ const auto d = SonicPi::audioPrefsDecision (request, outcome);
471+ CHECK (d.notice == SonicPi::AudioNotice::OutputUnavailable);
472+ CHECK (d.noticeDevice == " motu-xaero" );
473+ }
474+
475+ TEST_CASE (" notice: following the system default names the device it resolved to" , " [audio][notice]" )
476+ {
477+ SonicPi::AudioSwitchRequest request;
478+ request.output = SonicPi::kAudioSystemOutput ;
479+ SonicPi::AudioSwitchOutcome outcome;
480+ outcome.success = true ;
481+ outcome.actualOutput = " DMP-A6(Kitchen)" ;
482+ const auto d = SonicPi::audioPrefsDecision (request, outcome);
483+ CHECK (d.notice == SonicPi::AudioNotice::FollowingSystemDefault);
484+ CHECK (d.noticeDevice == " DMP-A6(Kitchen)" );
485+
486+ SonicPi::AudioSwitchRequest byRow;
487+ byRow.output = " Some Default Row" ;
488+ byRow.outputFollowsDefault = true ;
489+ const auto r = SonicPi::audioPrefsDecision (byRow, outcome);
490+ CHECK (r.notice == SonicPi::AudioNotice::FollowingSystemDefault);
491+ }
492+
493+ TEST_CASE (" notice: a device the user named and got raises none, nor does an engine-initiated swap" , " [audio][notice]" )
494+ {
495+ SonicPi::AudioSwitchRequest request;
496+ request.output = " motu-xaero" ;
497+ SonicPi::AudioSwitchOutcome outcome;
498+ outcome.success = true ;
499+ outcome.actualOutput = " motu-xaero" ;
500+ CHECK (SonicPi::audioPrefsDecision (request, outcome).notice == SonicPi::AudioNotice::None);
501+ SonicPi::AudioSwitchRequest engineInitiated; // nothing asked: the engine's own reopen
502+ CHECK (SonicPi::audioPrefsDecision (engineInitiated, outcome).notice == SonicPi::AudioNotice::None);
503+ }
504+
505+ TEST_CASE (" notice: the engine falling back on its own, mid-session, is a lost device" , " [audio][notice]" )
506+ {
507+ // The engine's own swap rolled back (a device unplugged, a reopen refused):
508+ // the next device-list push names where the sound went, and the user hears it.
509+ CHECK (SonicPi::audioStateLostDevice (" running" , " swap-failed-rollback" ));
510+ CHECK_FALSE (SonicPi::audioStateLostDevice (" running" , " rate-change" )); // a swap the user asked for
511+ CHECK_FALSE (SonicPi::audioStateLostDevice (" running" , " boot" ));
512+ CHECK_FALSE (SonicPi::audioStateLostDevice (" running" , " snapshot" ));
513+ CHECK_FALSE (SonicPi::audioStateLostDevice (" restarting" , " swap-failed-rollback" )); // not until it is running again
514+ }
0 commit comments