feat(topbar): the action follows the armed output; Test is an explicit ask again

Unit C (creator ruling 2026-11-12).

- Local MP4: one button, "Start Recording".
- YouTube: "Private Test Stream" beside "Review & Go Live", the latter with the primary face because it
  is the default ask.
- Running: "End Live Stream" (or "End Test" for a test run, "Stop Recording" for a local one).

The behavioural change underneath: SessionStartDecision.For no longer takes `editing`. Since 2026-10-30
EDIT silently chose test-vs-real, which meant the same button did two different things depending on a
toggle three inches away. With Test as its own button that inference has no job, so For() now reads only
the armed output and ON-AIR always means the real livestream. That supersedes the 2026-10-30 ruling;
SessionStartDecisionTests is rewritten to pin the new matrix rather than the old one.

A signed-out press still starts the sign-in flow instead of sitting dead (no-dead-ends), unchanged from
before — now stated in one place, the two new commands, instead of inside a route table.

680 pass, 0 warnings. Scope justification: Assets/starting-background.jpg is outside this unit — the
creator's replacement art, still awaiting their visual check.
This commit is contained in:
2026-11-12 15:44:15 -08:00
parent a5a10fb0b0
commit 4bc41c488c
8 changed files with 82 additions and 26 deletions
+10 -15
View File
@@ -3,27 +3,22 @@ using ytLive.Models;
namespace ytLive.Tests;
/// <summary>Creator ruling (2026-10-30): the top bar's primary button is context-sensitive and the
/// separate TEST button is gone.
/// <summary>Which session an armed output starts, and where a press routes.
///
/// <para>EDIT off + ON-AIR → the livestream message. EDIT on + ON-AIR → the test-livestream
/// message. REC always records, because there is no test recording. The matrix is pure, so it is
/// cheaper to pin it here than to drive a window.</para></summary>
/// <para>Superseded 2026-11-12: the ON-AIR start no longer infers Test from EDIT. The output stage
/// offers "Private Test Stream" and "Review & Go Live" as separate buttons, so ON-AIR always means the
/// real livestream here. REC always records, because there is no test recording.</para></summary>
public class SessionStartDecisionTests
{
[Fact]
public void Edit_decides_whether_the_on_air_start_is_live_or_a_test()
public void The_armed_output_decides_the_session_kind_and_test_is_never_inferred()
{
// ON-AIR armed: EDIT decides.
Assert.Equal(SessionStartKind.Live, SessionStartDecision.For(recordArmed: false, onAirArmed: true, editing: false));
// ON-AIR armed: the real livestream, never a test.
Assert.Equal(SessionStartKind.Live, SessionStartDecision.For(recordArmed: false, onAirArmed: true));
Assert.Equal("Go Live", SessionStartDecision.LabelFor(SessionStartKind.Live));
Assert.Equal(SessionStartKind.Test, SessionStartDecision.For(recordArmed: false, onAirArmed: true, editing: true));
Assert.Equal("Test Stream", SessionStartDecision.LabelFor(SessionStartKind.Test));
// REC armed: always a recording, whatever EDIT says (no test recording exists).
Assert.Equal(SessionStartKind.Record, SessionStartDecision.For(true, false, false));
Assert.Equal(SessionStartKind.Record, SessionStartDecision.For(true, false, true));
// REC armed: always a recording (no test recording exists).
Assert.Equal(SessionStartKind.Record, SessionStartDecision.For(true, false));
Assert.Equal("Start Recording", SessionStartDecision.LabelFor(SessionStartKind.Record));
// Signed out: an ON-AIR press (live OR test) routes to the sign-in flow, never a dead
@@ -35,6 +30,6 @@ public class SessionStartDecisionTests
Assert.Equal(SessionStartDecision.Route.Record, SessionStartDecision.RouteFor(SessionStartKind.Record, connected: false));
// Unarmed start records locally (the no-dead-ends rule).
Assert.Equal(SessionStartKind.Record, SessionStartDecision.For(false, false, false));
Assert.Equal(SessionStartKind.Record, SessionStartDecision.For(false, false));
}
}
+12
View File
@@ -39,6 +39,18 @@ public class TopBarWorldTests
Assert.False(vm.RecordPillOn);
vm.RecordPillOn = true;
Assert.False(vm.OnAirPillOn);
// The action follows the output: one button for MP4, two for YouTube (test + real).
Assert.True(vm.ShowStartRecordingAction);
Assert.False(vm.ShowYouTubeStartActions);
vm.OnAirPillOn = true;
Assert.False(vm.ShowStartRecordingAction);
Assert.True(vm.ShowYouTubeStartActions);
// Neither action exists while building scenes.
vm.EnterBuildWorldCommand.Execute(null);
Assert.False(vm.ShowStartRecordingAction);
Assert.False(vm.ShowYouTubeStartActions);
}
finally
{
+5
View File
@@ -49,6 +49,11 @@ public class UiShellTests
Assert.Contains("IsChecked=\"{Binding OnAirPillOn}\"", topBar);
Assert.Contains("IsChecked=\"{Binding RecordPillOn}\"", topBar);
// 2d. The action follows the armed output, and Test is an explicit ask again.
Assert.Contains("Content=\"Start Recording\"", topBar);
Assert.Contains("Content=\"Private Test Stream\"", topBar);
Assert.Contains("Content=\"Review &amp; Go Live\"", topBar);
// 3. Active scene = orange; live stays red.
Assert.Contains("IsStaged}\" Value=\"True\">\n <Setter Property=\"BorderBrush\" Value=\"{StaticResource Brush.Attention}\"/>", strip);