feat(top bar): EDIT decides live vs test on the primary button — the TEST button is gone

Creator ruling (2026-10-30):
- EDIT off + ON-AIR armed → the primary button reads "Go Live" and runs the livestream.
- EDIT on + ON-AIR armed → the same button reads "Test Stream" and runs the test broadcast.
- REC armed (or nothing armed) → "Start Recording", whatever EDIT says; there is no test
  recording, and an unarmed start records locally rather than doing nothing.

So the fifth button was redundant: `ShowTestButton` no longer feeds any control. The decision is a
pure function (`Models/SessionStartDecision`) so the whole matrix is testable without a window;
`PrimaryStartCommand` follows the same kind, so label and action cannot disagree.

Guard: SessionStartDecisionTests pins the four cases, including the no-dead-ends fallback that my
first version got wrong (unarmed returned Live instead of Record — the test caught it).

Note for the v1 gate: the "hide TEST at v1" item is now satisfied by construction — the test path
is a mode of the primary button, not a separate control.
This commit is contained in:
2026-10-30 11:14:33 -07:00
parent 9b08c3af4f
commit fbeb04385b
5 changed files with 103 additions and 12 deletions
+6 -2
View File
@@ -85,8 +85,12 @@ public partial class MainViewModel
get => _editingActive;
set
{
if (SetProperty(ref _editingActive, value))
OnPropertyChanged(nameof(IsEditMode));
if (!SetProperty(ref _editingActive, value)) return;
OnPropertyChanged(nameof(IsEditMode));
// EDIT decides whether the ON-AIR start is live or a test (creator ruling 2026-10-30).
OnPropertyChanged(nameof(PrimaryStartKind));
OnPropertyChanged(nameof(PrimaryStartButtonLabel));
OnPropertyChanged(nameof(PrimaryStartCommand));
}
}
+24 -2
View File
@@ -72,6 +72,9 @@ public partial class MainViewModel : ViewModelBase
OnPropertyChanged(nameof(OnAirBrush));
OnPropertyChanged(nameof(IsLivePrivate));
OnPropertyChanged(nameof(ShowPrimaryStartButton));
OnPropertyChanged(nameof(PrimaryStartKind));
OnPropertyChanged(nameof(PrimaryStartButtonLabel));
OnPropertyChanged(nameof(PrimaryStartCommand));
OnPropertyChanged(nameof(ShowEndStreamButton));
OnPropertyChanged(nameof(EndButtonText));
OnPropertyChanged(nameof(ShowTestButton));
@@ -109,6 +112,9 @@ public partial class MainViewModel : ViewModelBase
if (!SetProperty(ref _recordPillOn, value)) return;
if (value && _onAirPillOn) OnAirPillOn = false; // record-OR-live — arming REC drops ON-AIR
OnPropertyChanged(nameof(ShowPrimaryStartButton));
OnPropertyChanged(nameof(PrimaryStartKind));
OnPropertyChanged(nameof(PrimaryStartButtonLabel));
OnPropertyChanged(nameof(PrimaryStartCommand));
OnPropertyChanged(nameof(CanStartSession));
RaiseTopBarModes();
}
@@ -125,6 +131,9 @@ public partial class MainViewModel : ViewModelBase
if (!SetProperty(ref _onAirPillOn, value)) return;
if (value && _recordPillOn) RecordPillOn = false; // record-OR-live — arming ON-AIR drops REC
OnPropertyChanged(nameof(ShowPrimaryStartButton));
OnPropertyChanged(nameof(PrimaryStartKind));
OnPropertyChanged(nameof(PrimaryStartButtonLabel));
OnPropertyChanged(nameof(PrimaryStartCommand));
OnPropertyChanged(nameof(CanStartSession));
RaiseTopBarModes();
}
@@ -146,6 +155,9 @@ public partial class MainViewModel : ViewModelBase
OnPropertyChanged(nameof(IsEditMode));
OnPropertyChanged(nameof(ShowEndStreamButton));
OnPropertyChanged(nameof(ShowPrimaryStartButton));
OnPropertyChanged(nameof(PrimaryStartKind));
OnPropertyChanged(nameof(PrimaryStartButtonLabel));
OnPropertyChanged(nameof(PrimaryStartCommand));
OnPropertyChanged(nameof(EndButtonText));
OnPropertyChanged(nameof(ShowTestButton));
RaiseTopBarModes();
@@ -169,8 +181,18 @@ public partial class MainViewModel : ViewModelBase
/// routes to the sign-in inside StartSession (Start — the account step shows up
/// beside it as the Sign In button); a signed-out go-live stays the dead-end the
/// creator ruling 2026-09-20-02 forbids.</summary>
public string PrimaryStartButtonLabel =>
OnAirPillOn ? "Go Live" : "Start Recording";
/// <summary>Creator ruling 2026-10-30: EDIT decides whether the ON-AIR start is the real
/// livestream or the test one, which is why the separate TEST button is gone.</summary>
public SessionStartKind PrimaryStartKind =>
SessionStartDecision.For(RecordPillOn, OnAirPillOn, EditingActive);
public string PrimaryStartButtonLabel => SessionStartDecision.LabelFor(PrimaryStartKind);
/// <summary>The button's action follows its label: Test runs the same go-live pipeline with
/// `IsTestStream` set, Go Live runs it for real.</summary>
public ICommand PrimaryStartCommand => PrimaryStartKind == SessionStartKind.Test
? StartTestCommand
: StartStreamCommand;
/// <summary>The End button shows while recording OR live.</summary>
public bool ShowEndStreamButton => IsLive || IsRecording;