fix(wpf): catch participant-left race in ToggleIsoAsync, toast instead of crash
Some checks failed
CI / build-and-test (push) Failing after 27s

The operator path: click Enable on a participant -> AsyncRelayCommand fires
ToggleIsoAsync -> IsoController.EnableIsoAsync(id) -> tracker lookup -> throws
InvalidOperationException 'Participant <guid> not currently visible on the
network' when the participant has departed between the click and the engine
resolving the id.

Previously this exception escaped AsyncRelayCommand.Execute via the unawaited
Task in ICommand.Execute, hit System.Threading.Tasks.Task.ThrowAsync, and
ended up in Dispatcher.UnhandledException — which the App.CrashHandlers path
treats as a fatal and fires the crash dialog. Fatal in the log captured
during this morning's session at 08:08:27.

Wrap the EnableIsoAsync / DisableIsoAsync calls in try/catch:
  - InvalidOperationException -> toast 'X just left the meeting'; leave
    IsEnabled at its current value (engine state of record)
  - Exception -> toast 'Couldn't toggle ISO for X: <message>'; same rationale
  - finally clause still flips IsProcessing back so the spinner clears

No new tests — the race is hard to trigger deterministically without
introducing a mocking seam on the controller. The behavior change is small
and the surface is the only call site for EnableIso/DisableIso from the
participant row.
This commit is contained in:
Zac Gaetano 2026-05-16 08:48:06 -04:00
parent 84861dafa5
commit 6c9bee7391

View file

@ -501,6 +501,27 @@ public sealed class ParticipantViewModel : ObservableObject
IsEnabled = true;
}
}
catch (InvalidOperationException)
{
// Race window: participant left the meeting between when the operator
// clicked Enable/Disable and when the engine resolved the ID. The
// controller throws InvalidOperationException with a "not currently
// visible on the network" message in this case. Surface it as a soft
// warning toast rather than letting it escape into the dispatcher's
// unhandled-exception channel (which fires a fatal crash dialog).
//
// Leave IsEnabled at its current value — the engine refused the state
// change, so the VM should reflect the actual engine state.
_toast?.Warn($"{DisplayName} just left the meeting");
}
catch (Exception ex)
{
// Defensive catch-all for any other engine-side failure (port bind
// race, pipeline factory throw, etc.). Same reasoning as above —
// an exception from an operator click should never tear down the
// dispatcher.
_toast?.Warn($"Couldn't toggle ISO for {DisplayName}: {ex.Message}");
}
finally
{
IsProcessing = false;