fix(stream): TEST-tab chat — resolve liveChatId from snippet, poll until the broadcast is live

The open creator report ('I still cannot post a chat message in the TEST tab',
feedback 'Chat polling couldn't start — Mock Chat Input is disabled') was a single
cause: the liveChatId never resolved. Two YouTube API facts:
1. The id lives in snippet.liveChatId — contentDetails has no such property
   (TASK 44 read part=contentDetails: could never resolve).
   https://developers.google.com/youtube/v3/live/docs/liveBroadcasts
2. It only exists once the broadcast is LIVE — the official sample lists
   broadcastStatus=active, and our fetch ran before the frame pump pushed RTMP
   (enableAutoStart flips ready→live). A ready-state list legitimately returns
   no id.
   https://github.com/youtube/api-samples/blob/master/java/src/main/java/com/google/api/services/samples/youtube/cmdline/live/GetLiveChatId.java

Fix: GetBroadcastLiveChatIdAsync reads part=snippet and polls with a bounded
retry (10x/2s); PrepareAndStartLiveAsync starts the frame pump FIRST, then
resolves the id. Chat stays non-fatal. Docs ai.md/TASKS.md/HANDOFF.md +
MyMistakes recipe updated same commit. TASK 44.

Good Dog: ONE integration test (GetBroadcastLiveChatIdAsync_Polls_Snippet_...
) drives a broadcast that gains its id mid-retry and asserts every request used
part=snippet. Gate: clean build 0 warnings, vstest 318/318.
This commit is contained in:
2026-09-25 08:06:02 -07:00
parent d4aa588da0
commit 94f015044a
8 changed files with 219 additions and 83 deletions
+58
View File
@@ -0,0 +1,58 @@
# TASK 44 — TEST-tab chat fix: resolve the liveChatId (snippet + live-timing)
> Catalog: [`TASKS.md`](../TASKS.md). Status: ✅ **SHIPPED 2026-09-25** — the open
> creator report from HANDOFF ("I still cannot post a chat message in the TEST tab")
> diagnosed and fixed as its own scoped unit.
## Provenance
- **2026-09-24, HANDOFF** filed the creator report as open: "I still cannot post a chat
message in the TEST tab." The TEST drawer's Mock Chat Input posts a real
`liveChat/messages.insert`; the drawer only enables it when a liveChatId is resolved.
- **2026-09-25 diagnosis:** the reported feedback line "Chat polling couldn't start —
Mock Chat Input is disabled" is `TestSessionViewModel.BeginTest` running with a **null
liveChatId**. That id comes from `YouTubeStreamService.GetBroadcastLiveChatIdAsync`,
and two API facts (from the official liveBroadcasts reference + the
`GetLiveChatId.java` sample) pin why it never resolved:
1. **It lives under `snippet.liveChatId`** — `contentDetails` has no such property at
all. The code requested `part=contentDetails` and read `contentDetails.liveChatId`,
a path that can NEVER resolve.
2. **YouTube only populates it once the broadcast is `live`** (the official sample
lists `broadcastStatus=active`). The fetch ran right after broadcast insert
(lifecycleStatus `ready`) AND before `_framePump.StartAsync()` pushed the RTMP
stream that `enableAutoStart` needs to flip the broadcast to live — so even the
correct property would have been empty at fetch time.
## What shipped
- `Services/YouTubeStreamService.cs`:
- `GetBroadcastLiveChatIdAsync(broadcastId, maxAttempts = 10, delayMs = 2000)` now
requests **`part=snippet`** and reads **`snippet.liveChatId`**, and **polls with a
bounded retry** until the id appears or the attempts exhaust (never throws). The
default retry (~20s) gives the encoder time to connect and the broadcast to go live.
- New private `FetchLiveChatIdAsync` holds the single read; the public method owns
the retry loop.
- `ViewModels/MainViewModel.Streaming.Operations.cs`:
- `PrepareAndStartLiveAsync` **starts the frame pump FIRST**, then resolves the
liveChatId — the reorder is required for the id to ever exist. Chat remains
non-fatal: failure logs and the stream continues.
- TEST drawer wiring preserved: `TestSession.BeginTest(broadcastId, liveChatId)`
still opens the dock once the id resolves → Mock Chat Input becomes sendable.
- Docs: `ai.md` (Test Stream section), `TASKS.md` (row 44), `HANDOFF.md` rewritten.
## Validation (Good Dog: ONE integration test)
`ytLive.Tests/YouTubeStreamServiceTests.cs`:
`GetBroadcastLiveChatIdAsync_Polls_Snippet_Until_Id_Appears` — a fake broadcast that
starts WITHOUT a liveChatId (not yet live) and gains it on a later poll; asserts the
retry eventually resolves `Cg0LIVECHAT42`, that it polled ≥3 times, and that every
request asked for **snippet** (never contentDetails) — the exact seam that was broken.
**Gate: clean build 0 warnings; full vstest 318/318 passed.**
## Reference
- https://developers.google.com/youtube/v3/live/docs/liveBroadcasts (snippet.liveChatId;
contentDetails has no liveChatId)
- https://github.com/youtube/api-samples/blob/master/java/src/main/java/com/google/api/services/samples/youtube/cmdline/live/GetLiveChatId.java
(sample lists broadcastStatus=active — id only present on live broadcasts)