feat(topbar): REC/ON-AIR pills are radio-exclusive (record-OR-live)
The record-OR-live ruling (2026-09-01) was documented in ai.md but the 'pills = radios' UI constraint never landed — both pills could be armed and StartSession/BuildEncoderOptions would dual-encode to the YouTube ingest AND a local file at once, streaming a concurrent disk write off the same ingested Kbps (cripples the stream on mid-range chassis; 'the VOD is already the copy'). The pill setters now clear one another (silent mutual clear); the dual-block ffmpeg machinery stays generic and unreachable from the UI by design.
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Xunit;
|
||||
using ytLive.Services;
|
||||
using ytLive.ViewModels;
|
||||
|
||||
namespace ytLive.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Record-OR-live (creator ruling 2026-09-01): the REC and ON-AIR pills are
|
||||
/// radio-exclusive — arming one clears the other, so a session can never dual-encode
|
||||
/// to both the YouTube ingest and a local file at once (a concurrent disk write
|
||||
/// streamed from the same sustained Kbps cripples the stream on mid-range chassis).
|
||||
/// The dual-block encoder machinery in FfmpegArgs stays generic; the constraint
|
||||
/// lives at the pills.
|
||||
/// </summary>
|
||||
[Collection("RealApp")]
|
||||
public sealed class PillRadioTests
|
||||
{
|
||||
private readonly RealAppHost _app;
|
||||
|
||||
public PillRadioTests(RealAppHost app) => _app = app;
|
||||
|
||||
[Fact]
|
||||
public void Arming_One_Output_Pill_Clears_The_Other()
|
||||
{
|
||||
_app.Run(Run);
|
||||
}
|
||||
|
||||
private void Run()
|
||||
{
|
||||
var tempDb = Path.Combine(Path.GetTempPath(), $"ytLlive-pillradio-{Guid.NewGuid():N}.db");
|
||||
MainViewModel.LayoutPathOverride = tempDb;
|
||||
try
|
||||
{
|
||||
using (var schema = new LayoutStore(tempDb)) { }
|
||||
SqliteConnection.ClearAllPools();
|
||||
|
||||
var vm = new MainViewModel();
|
||||
vm.IsConnected = true; // the ON-AIR setter requires a sign-in
|
||||
|
||||
// Arm REC first, then ON-AIR → REC must clear.
|
||||
vm.RecordPillOn = true;
|
||||
vm.OnAirPillOn = true;
|
||||
Assert.False(vm.RecordPillOn, "arming ON-AIR must clear the REC pill");
|
||||
Assert.True(vm.OnAirPillOn);
|
||||
|
||||
// Arm REC while ON-AIR is lit → ON-AIR must clear.
|
||||
vm.RecordPillOn = true;
|
||||
Assert.False(vm.OnAirPillOn, "arming REC must clear the ON-AIR pill");
|
||||
Assert.True(vm.RecordPillOn);
|
||||
|
||||
// A single armed pill still makes the Start button actionable.
|
||||
Assert.True(vm.CanStartSession);
|
||||
}
|
||||
finally
|
||||
{
|
||||
MainViewModel.LayoutPathOverride = null;
|
||||
SqliteConnection.ClearAllPools();
|
||||
try { File.Delete(tempDb); } catch { /* best-effort cleanup */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user