TASK 5 items 1-3: reusable stream, health banner, docs refresh

- Reusable stream: GetOrCreateReusableStreamAsync, cached via LayoutStore, bound at broadcast insert
- Health monitoring: GetStreamHealthAsync polls liveStreams.status, report-by-exception banner
- Go-live order: ensure stream → create broadcast → then start pump (URL must exist first)
- Full broadcast form scoped (TASK 5 items 6-7) in TASKS.md + ai.md
- TASK 23 trimmed to technical scope, TASK 26 added (marcom/launch kit)
- Confidential files: MONETIZATION.md + MARCOM.md (gitignored, not committed)
- .gitignore updated for confidential business files
- HANDOFF.md updated with session state and next steps
This commit is contained in:
2026-08-16 17:48:37 -07:00
parent 92f1471ab4
commit 0fac1e6a5a
16 changed files with 810 additions and 140 deletions
@@ -14,6 +14,35 @@ namespace ytLive.Tests;
/// </summary>
public class LayoutStorePersistenceTests
{
// The reusable stream cache (TASK 5): the RTMP URL must survive restarts so
// the frame pump has it the moment go-live starts, without a YouTube round-trip.
[Fact]
public void ReusableStream_Cache_RoundTrips()
{
var path = Path.Combine(Path.GetTempPath(), $"ytLlive-layout-{Guid.NewGuid():N}.db");
try
{
using (var store = new LayoutStore(path))
{
store.SaveReusableStream(new ReusableStream("S456", "rtmp://a.rtmp.youtube.com/live2", "KEY123"));
var loaded = store.LoadReusableStream();
Assert.NotNull(loaded);
Assert.Equal("S456", loaded.Id);
Assert.Equal("rtmp://a.rtmp.youtube.com/live2", loaded.IngestionAddress);
Assert.Equal("KEY123", loaded.StreamName);
Assert.Equal("rtmp://a.rtmp.youtube.com/live2/KEY123", loaded.RtmpUrl);
}
using (var reopened = new LayoutStore(path))
Assert.Equal("KEY123", reopened.LoadReusableStream()?.StreamName);
}
finally
{
SqliteConnection.ClearAllPools();
try { File.Delete(path); } catch { /* best-effort cleanup */ }
}
}
[Fact]
public void Deleted_Webcam_Config_Does_Not_Return_After_Save_And_Reload()
{