feat(ui): TASK 42 top bar redesign — one Record-or-Stream surface (2026-09-22)

The bar now renders ONE surface from the FIRST decision the creator makes —
Record or Stream — with each world being the whole bar. Creator directive:
"I wrote the fucking thing and I still can't figure-out how to do stuff",
"forget this one dog plan bullshit". OBS/streaming-tool reference for the
one-surface paradigm: https://obsproject.com (single mode toggle + context
actions) — cited per spin-guard habit; the go-live/test pattern follows
CEV (https://cev-desktop.aSean.xyz) precedent.

- Mode: single segmented REC|ON-AIR switch (SegmentToggle/SegmentLabel in
  Themes/Controls.xaml); radio-exclusive world selection, one tap to flip.
- Record world: switch + Start Recording, zero YouTube identity.
- Stream world: switch + Go Live + Test + account zone right (Sign In until
  connected, then avatar with Change Account/Logout context menu).
- Test is a child of Stream: procs only stream-armed AND signed-in.
- Running: one reality line "dot word elapsed" (REC/LIVE/TEST; green/red/
  gold) + End; worlds + switch retire.
- Gear moved up from bottom bar, ~3 wordmark letters past the brand, single
  click opens Settings/Bug/Feature/About menu (TASK 40 Unit B shipped early).
- Fix folded in: BeginTestStream now arms OnAirPillOn explicitly (unarmed
  pump booted with zero encoder outputs -> "At least one output is required"
  forced-stop cascade from TASK 41's pipeline).
- VM: world/reality props + RaiseTopBarModes() wired into StreamStatus,
  pill, IsRecording, IsTestStream, IsConnected setters.

Doc: ai.md top-bar model, ViewModels/index.md, Controls/index.md, TASKS.md,
HANDOFF.md, new TASKS/task-42-top-bar-redesign.md; task-40 note.

Tests: TopBarModeTests.cs (new Good Dog), PillRadioTests + TestStreamTests
gates updated. Build 0 warnings; full suite 314/315 — single abort is the
pre-existing env-dependent RealMouseDrag test (Path of Exile 2 running)
This commit is contained in:
2026-09-22 17:57:06 -07:00
parent bb5dcb4ba2
commit e69db4d26c
18 changed files with 538 additions and 236 deletions
+106
View File
@@ -0,0 +1,106 @@
using System;
using System.IO;
using Microsoft.Data.Sqlite;
using Xunit;
using ytLive.Models;
using ytLive.Services;
using ytLive.ViewModels;
namespace ytLive.Tests;
/// <summary>
/// Top bar redesign — the bar is ONE surface rendered by the FIRST decision
/// (Record or Stream). Each world is the whole bar: the Record world has no
/// YouTube identity at all, the Stream world owns the account zone (Sign In or
/// avatar) plus Test. The switch is a two-position gate, so "going back" is one
/// tap on the other segment. While running, the switch retires and a single
/// reality line (dot + word + elapsed) + End replace it.
/// </summary>
[Collection("RealApp")]
public sealed class TopBarModeTests
{
private readonly RealAppHost _app;
public TopBarModeTests(RealAppHost app) => _app = app;
[Fact]
public void TopBar_Renders_Only_The_Armed_World()
{
_app.Run(Run);
}
private void Run()
{
var tempDb = Path.Combine(Path.GetTempPath(), $"ytLlive-topbar-{Guid.NewGuid():N}.db");
MainViewModel.LayoutPathOverride = tempDb;
try
{
using (var schema = new LayoutStore(tempDb)) { }
SqliteConnection.ClearAllPools();
var vm = new MainViewModel();
// The dev machine may hold a real saved OAuth session (loads synchronously
// during the ctor), so the sign-in state is forced deterministically.
vm.IsConnected = false;
// Default (nothing armed) = the Record world: Go says "Start Recording",
// no account zone, no Test — YouTube doesn't exist in this world.
Assert.True(vm.ShowIdleWorld);
Assert.True(vm.ShowRecordAction);
Assert.False(vm.ShowStreamActions);
Assert.False(vm.ShowSignInButton);
Assert.False(vm.ShowAvatarButton);
Assert.False(vm.ShowTestButton);
Assert.Equal("Start Recording", vm.PrimaryStartButtonLabel);
// Arming ON-AIR switches the whole bar to the Stream world.
vm.OnAirPillOn = true;
Assert.False(vm.ShowRecordAction);
Assert.True(vm.ShowStreamActions);
// Signed out: the account zone shows Sign In; no avatar; Test is gated
// behind the sign-in (Test rides on Stream AND a connected account).
Assert.True(vm.ShowSignInButton);
Assert.False(vm.ShowAvatarButton);
Assert.False(vm.ShowTestButton);
Assert.Equal("Go Live", vm.PrimaryStartButtonLabel);
// Connected: avatar + Test join the Stream world; Sign In retires.
vm.IsConnected = true;
Assert.False(vm.ShowSignInButton);
Assert.True(vm.ShowAvatarButton);
Assert.True(vm.ShowTestButton);
// Going back is one tap: flipping REC returns the bar to the Record world.
vm.RecordPillOn = true;
Assert.False(vm.ShowStreamActions);
Assert.True(vm.ShowRecordAction);
Assert.False(vm.ShowAvatarButton);
Assert.False(vm.ShowTestButton);
Assert.Equal("Start Recording", vm.PrimaryStartButtonLabel);
// Running retires the switch and worlds; the reality line pins the mode.
vm.RecordPillOn = false;
vm.OnAirPillOn = true;
vm.IsRecording = true;
Assert.False(vm.ShowIdleWorld);
Assert.True(vm.RunningVisible);
Assert.Equal("REC", vm.RunningText);
Assert.Equal("#22c55e", vm.RunningDotBrush);
vm.IsRecording = false;
vm.StreamStatus = StreamStatus.Streaming;
Assert.Equal("LIVE", vm.RunningText);
Assert.Equal("#e94560", vm.RunningDotBrush);
vm.IsTestStream = true;
Assert.Equal("TEST", vm.RunningText);
Assert.Equal("#9c6f1c", vm.RunningDotBrush);
}
finally
{
MainViewModel.LayoutPathOverride = null;
SqliteConnection.ClearAllPools();
try { File.Delete(tempDb); } catch { /* best-effort cleanup */ }
}
}
}