89fee6ca4a
Take two (2026-09-01) three confirmed defects, all from creator feedback + the
new resolver logging:
1. NO WEBCAM IN OUTPUT: WebcamSceneConfig.WebcamId carries the identity GUID;
CameraManager keys sessions by DEVICE id — GetLatestFrame(guid) returned null
forever, the compositor silently dropped the layer while the preview (bitmap
path) looked fine. The resolver logging added in 8dcaee0 caught it red-handed.
DeviceKeyForWebcam maps identity->device through the _webcam singleton (identity
mismatch / unknown ids pass through). Test: WebcamOutputKeyTests.
2. START BUTTON VANISHED AFTER STOP: my pills-clear ruling met the old
ShowPrimaryStartButton gate (needed IsConnected or a lit REC pill) — signed-out,
pills-off = blank top bar, no session reachable. Start is now ALWAYS the idle
face; unarmed Start records (local needs no account — no dead-end no-op); the
Sign In button folds into Start's context menu ('Sign in to YouTube', shown while
disconnected) with a dedicated SignInCommand. SessionTeardownTests extended:
stopped session must leave a reachable Start.
3. TOP BAR ORDER (creator spec): [sign light] REC [pill] [sign light] ON-AIR [pill]
— each reality lamp now sits in front of its own intent switch (was: two pills,
then two orphaned dots). Sign labels keep the shared StatusSignText style.
Tests: WebcamOutputKey, SessionTeardown, FramePump, WebcamMenuGate, GlobalHotkey,
BroadcastPullOut — 14/14 across the six classes, clean build 0 warnings.
72 lines
2.8 KiB
C#
72 lines
2.8 KiB
C#
using System;
|
|
using System.IO;
|
|
using Microsoft.Data.Sqlite;
|
|
using Xunit;
|
|
using ytLive.Services;
|
|
using ytLive.ViewModels;
|
|
|
|
namespace ytLive.Tests;
|
|
|
|
/// <summary>
|
|
/// Session-teardown integration (2026-09-01, first native launch): a frame-pump death
|
|
/// must roll the WHOLE session back — record-only sessions used to stay IsRecording=true
|
|
/// with a dead encoder (no live session to flip StreamStatus), and StopStream never
|
|
/// cleared the intent pills. "Stop ends everything" (creator ruling): pills slide off
|
|
/// with the session; re-arming is one click. Real-App + temp-DB pattern.
|
|
/// </summary>
|
|
[Collection("RealApp")]
|
|
public sealed class SessionTeardownTests
|
|
{
|
|
private readonly RealAppHost _app;
|
|
|
|
public SessionTeardownTests(RealAppHost app) => _app = app;
|
|
|
|
[Fact]
|
|
public void Pump_Failure_Rolls_Back_Recording_And_Clears_Pills()
|
|
{
|
|
_app.Run(Run);
|
|
}
|
|
|
|
private void Run()
|
|
{
|
|
var tempDb = Path.Combine(Path.GetTempPath(), $"ytLlive-teardown-{Guid.NewGuid():N}.db");
|
|
MainViewModel.LayoutPathOverride = tempDb;
|
|
var window = default(MainWindow);
|
|
try
|
|
{
|
|
using (var schema = new LayoutStore(tempDb)) { }
|
|
SqliteConnection.ClearAllPools();
|
|
|
|
window = new MainWindow();
|
|
var vm = (MainViewModel)window.DataContext;
|
|
window.Show();
|
|
window.UpdateLayout();
|
|
|
|
// The exact 2026-09-01 zombie: record-only, intent lit, reality true,
|
|
// encoder already dead. Sign-in absent — the guard must also prove no
|
|
// close-out API call is attempted for a record-only session (StopStream
|
|
// completes without throwing offline).
|
|
vm.RecordPillOn = true;
|
|
vm.IsRecording = true;
|
|
|
|
vm.OnFramePumpFailed(this, "test: ffmpeg exited");
|
|
|
|
Assert.False(vm.IsRecording, "pump death must roll IsRecording back — no zombie session");
|
|
Assert.False(vm.RecordPillOn, "Stop ends everything — the REC pill slides off with the session");
|
|
Assert.False(vm.OnAirPillOn, "both pills clear");
|
|
Assert.False(vm.ShowEndStreamButton, "no End button once nothing is running");
|
|
Assert.Equal(ytLive.Models.StreamStatus.Offline, vm.StreamStatus);
|
|
// No dead ends (2026-09-01): after a stop the top bar must still offer a
|
|
// reachable Start — pills cleared + signed out used to blank the button.
|
|
Assert.True(vm.ShowPrimaryStartButton, "a stopped session must leave a Start button");
|
|
}
|
|
finally
|
|
{
|
|
if (window != null) window.Close();
|
|
MainViewModel.LayoutPathOverride = null;
|
|
SqliteConnection.ClearAllPools();
|
|
try { File.Delete(tempDb); } catch { /* best-effort */ }
|
|
}
|
|
}
|
|
}
|