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
+39 -1
View File
@@ -294,4 +294,42 @@ public class YouTubeStreamServiceTests
Assert.Contains("403", closeOutError);
Assert.StartsWith("POST ", handler.Requests[^1]);
}
}
/// <summary>The ONE integration test for the 2026-09-25 TEST-tab chat fix:
/// "Chat polling couldn't start — Mock Chat Input is disabled" because the
/// liveChatId never resolved. Two facts pin it: (1) the id lives in
/// <c>snippet.liveChatId</c> — part=contentDetails could NEVER return it, and
/// (2) YouTube only populates it once the broadcast is live, so a single fetch
/// right after insert (lifecycleStatus ready) comes back empty. The fix reads
/// part=snippet AND polls with a bounded retry — this test drives a fake
/// broadcast that starts WITHOUT the id (broadcast not yet live) and gains it on
/// a later poll, proving the retry resolves it and that every request asked for
/// snippet (never contentDetails).</summary>
[Fact]
public async Task GetBroadcastLiveChatIdAsync_Polls_Snippet_Until_Id_Appears()
{
var polls = 0;
var handler = new RecordingHandler
{
ResponseSelector = request =>
{
polls++;
// First polls: broadcast exists but not yet live → no liveChatId.
return polls < 3
? """{"items":[{"id":"BC-42","snippet":{"title":"t"}}]}"""
: """{"items":[{"id":"BC-42","snippet":{"title":"t","liveChatId":"Cg0LIVECHAT42"}}]}""";
}
};
var service = new YouTubeStreamService(CreateAuthed(), new HttpClient(handler));
var chatId = await service.GetBroadcastLiveChatIdAsync("BC-42", maxAttempts: 5, delayMs: 25);
Assert.Equal("Cg0LIVECHAT42", chatId);
Assert.True(polls >= 3, $"expected the retry to poll until the id appears (polled {polls}x)");
Assert.All(handler.Requests, r =>
{
Assert.Contains("part=snippet", r);
Assert.DoesNotContain("part=contentDetails", r);
});
}
}