# TASK 41 — Test Stream mode: private test broadcast + TEST drawer **Status:** ✅ Done (2026-09-22) — one scope-checked work unit, one Good Dog integration test, 314/314 suite green, 0 warnings. ## Creator ask (2026-09-22) "YouTube Test Stream & Chat Integration": a **Test/Preview** button beside Start that runs a private broadcast via the API, monitors stream/ingest, activates the chat endpoint (liveChatId), and provides a **chat simulator**: mock chat input (`liveChatMessages.insert`) + a toggle to inject mock chat, with alert/event triggers for subscriber, membership, and Super Chat. ## Locked decisions (user answers, 2026-09-22) - **Chat-overlay events only** — simulated events validate the existing chat overlay styling (text / member `memberColor` / SuperChat `superChatColor` rows). Dedicated stinger/TTS alert widgets are NOT part of this task — they stay queued under **TASK 3 items 16/17/20**. - **TEST drawer in the right rail** — third white tab below YPP, three-way one-open-at-a-time exclusivity, auto-opens when a test broadcast boots. - **Hybrid simulation** — text = REAL `liveChat/messages.insert` (round-trips the real 2s poll and renders through the live overlay path); member/SuperChat/subscriber = **local injection** at the poller's `MessageReceived` seam (marked `IsSimulated`). YouTube's insert API only creates `textMessageEvent`s — non-text events are local-only by design. ## Facts the build sits on (verified in code, 2026-09-22 research pass) - **The app is already private-only go-live** — `YouTubeStreamService.CreateBroadcast` hardcodes `privacyStatus="private"` + `enableAutoStart/Stop` + `low latency`, no monitor stream (`enableMonitorStream=false`), and the Go Live dialog is locked to Private. "Test" is therefore a **session variant over the real pipeline**, not a new broadcast mode. - The **liveChatId flow already exists**: `GetBroadcastLiveChatIdAsync` → `_youtubeChat.Start(liveChatId)` in `PrepareAndStartLiveAsync` — the "chat endpoint activation" the ask describes. - OAuth scope already includes `youtube` + `youtube.force-ssl` (`YouTubeAuthService.cs:18`) — enough for `liveChat/messages.insert`; no re-consent. - Chat = **polling** (`YouTubeChatService`, 2s): no webhook exists or is planned. - No alert system exists anywhere in the codebase (grep verified) — hence the scope decision above. - Existing mock-chat *preview* (`ChatOverlayLayer.StartMockChatPreview`) is static placeholders, not live-injectable — separate from this task. ## What shipped (TASK 41, 2026-09-22) - **`Controls/TopBar.xaml`** — **Test** button (secondary style) left of Start; visible only when `ShowTestButton` (offline ∧ not recording ∧ signed-in); **TEST** amber badge while `IsTestStream` (next to the elapsed clock). - **`Controls/PreviewPane.xaml`** — **TEST** tab (gold text, below YPP on the rail) + TEST drawer: session caption / liveChatId line, **Mock Chat Input** (TextBox + Send), **SIMULATED EVENTS** (Subscriber / New Member / Super Chat buttons), live feedback line, footer "End Test" + the "private, same pipeline as a real live" note; drawer exclusivity wired three-way. - **`Services/TestSessionViewModel.cs`** (NEW) — the TEST drawer VM: `BeginTest(broadcastId, liveChatId)` (auto-opens the dock once the liveChatId resolves), `SendTestMessageAsync` → real insert, `Simulate{Subscriber,Member,SuperChat}Command` → `InjectSimulatedMessage`, `EndTestCommand` → hands to `MainViewModel.StopStream`, `EndTestSilently` (idempotent, called on any stop/ rollback). Fully testable — takes `YouTubeStreamService` + `YouTubeChatService` via ctor. - **`Services/YouTubeStreamService.InsertChatMessageAsync(liveChatId, text)`** — `POST liveChat/messages?part=snippet`; returns string error or null; never throws. - **`Services/YouTubeChatService.InjectSimulatedMessage(ChatMessage)`** — raises `MessageReceived`, the exact seam the real poll uses, so simulations flow through the whole overlay pipeline; marked `ChatMessage.IsSimulated`. - **`ViewModels/MainViewModel.*`** — `IsTestStream` (internal setter, test seam like `IsRecording`); `BeginTestStream()` → the shared `StartStreamingSession(alsoRecord:false)` extracted from `BeginGoLive` (no dialog — a Test needs no confirmation, every broadcast is already private; no `BroadcastForm.CaptureGoLive` "last live" stamp; no recording). `StopStream` clears the face and forgets the drawer session. `EndButtonText` = "End Test" while in a test. Gold top bar + glow (`#9c6f1c`) identify a rehearsal vs the live red. - **Drawer exclusivity** (this task): opening TEST collapses Stream Settings + YPP and vice versa (wired in `MainViewModel.Ypp.cs`). ## Good Dog — ONE integration test `ytLive.Tests/TestStreamTests.cs` — one `[Fact]`, two sections: 1. **Coordinator (no network, `RecordingHandler`-faked HTTP):** idle → `BeginTest` opens the dock + enables chat; Mock Chat Input emits the insert (POST body asserted for the liveChatId + messageText); Subscriber/New Member/Super Chat simulations arrive through `MessageReceived` marked `IsSimulated` with the right shapes (member level, `$5.00` Super Chat, plain subscriber row); End hands back to the app; `EndTestSilently` clears. 2. **Real window (temp DB):** TEST tab/drawer exist, three-way drawer exclusivity, Test button gate follows sign-in deterministically. ## Follow-ups (NOT in this task) - Real stinger/TTS alert widgets → TASK 3 items 16/17/20. - Test-mode health-panel depth (per-broadcast status inside the drawer) if ever wanted.