4e0915a36e
Click-to-pin an open window as the full-bleed Live backdrop, via the existing
window:<hwnd> capture path (same render layer as game/desktop). Session-scoped:
window:/picker: keys heal to auto on reload (HWND-recycle hazard); pin wins
while the window enumerates, then auto-fallbacks (game → desktop → static); a
dead window's capture session heals to auto, no dead ends.
Fix: ScreenCaptureSourceFactory.Resolve hex-parsed "window:0x<Hwnd>" WITHOUT
stripping the 0x prefix — NumberStyles.HexNumber rejects it, so every pin
resolved null ("No capture target for this key") and the heal silently rolled
back. Same flaw in IsAliveWindowPin (pin-wins guard was dead). Both fixed.
Commit also carries an out-of-scope prerequisite: PillRadioTests.cs:105 had a
committed stray token ("...PrimaryStartButtonLabel soil;", CS1003) blocking the
whole test-project compile — token removed.
Plus the previous session's uncommitted pricing docs (one-time $29/$49) that
share TASKS.md/ai.md/HANDOFF.md.
114 lines
4.5 KiB
C#
114 lines
4.5 KiB
C#
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 */ }
|
|
}
|
|
}
|
|
|
|
/// <summary>Good Dog (one integration test, creator ruling 2026-09-20): the ON-AIR
|
|
/// pill is armable signed-out — intent may light while the account is offline (a
|
|
/// greyed pill was the dead end the guard shipped to forbid). A front-branch on
|
|
/// the ON-AIR setter that dropped the pill headlessly is what the pill is FOR. The
|
|
/// Start button face reads "Sign In" while the armed signed-out pill is lit, and
|
|
/// flips back to "Start" the moment a session connects — the pill keeps its light
|
|
/// either way. Signed-out ON-AIR Start routes to the sign-in; a signed-out go-live
|
|
/// is the dead end the ruling forbids.</summary>
|
|
[Fact]
|
|
public void SignedOut_OnAir_Pill_Stays_Lit_And_Start_Face_Is_SignIn()
|
|
{
|
|
_app.Run(RunSignedOutOnAir);
|
|
}
|
|
|
|
private void RunSignedOutOnAir()
|
|
{
|
|
var tempDb = Path.Combine(Path.GetTempPath(), $"ytLlive-pillradio-signin-{Guid.NewGuid():N}.db");
|
|
MainViewModel.LayoutPathOverride = tempDb;
|
|
try
|
|
{
|
|
using (var schema = new LayoutStore(tempDb)) { }
|
|
SqliteConnection.ClearAllPools();
|
|
|
|
var vm = new MainViewModel();
|
|
|
|
// No sign-in: the ON-AIR pill MUST still be able to light — intent before
|
|
// reality, armable signed-out (creator ruling 2026-09-20).
|
|
vm.OnAirPillOn = true;
|
|
Assert.True(vm.OnAirPillOn, "ON-AIR pill must be armable signed-out (greyed pill = dead end)");
|
|
Assert.True(vm.CanStartSession);
|
|
|
|
// While armed signed-out the Start button wears the sign-in face, so a
|
|
// signed-out start is never the dead-end go-live the ruling forbids.
|
|
Assert.Equal("Sign In", vm.PrimaryStartButtonLabel);
|
|
Assert.False(vm.ShowPrimaryStartButton && vm.CanStartSession && false,
|
|
"sanity: face follows the pill, not the account race");
|
|
|
|
// A session connects: the pill KEEPS its light and the face flips to Start.
|
|
vm.IsConnected = true;
|
|
Assert.True(vm.OnAirPillOn, "the armed signed-out pill must survive the sign-in");
|
|
Assert.Equal("Start", vm.PrimaryStartButtonLabel);
|
|
}
|
|
finally
|
|
{
|
|
MainViewModel.LayoutPathOverride = null;
|
|
SqliteConnection.ClearAllPools();
|
|
try { File.Delete(tempDb); } catch { /* best-effort cleanup */ }
|
|
}
|
|
}
|
|
} |