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:
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user