fix(stream): health poll crashed on every start — parse the real healthStatus object shape

startup.log 2026-09-22 (via the user's run of the new build): every session
start logged 'Stream health poll failed: The requested operation requires an
element of type String, but the target element has type Object'.

Root cause: GetStreamHealthAsync did status.healthStatus.GetString(), but the
real API nests it as status.healthStatus = {status, lastUpdateTimeSeconds,
configurationIssues[]} — an OBJECT, per the liveStreams reference
https://developers.google.com/youtube/v3/live/docs/liveStreams (checked the
docs before fixing, per derivative-work rule). Our flat-string fixture was the
wrong assumption since TASK 9; the report-by-exception health banner had been
silently dead.

Fix: read the nested healthStatus.status + nested configurationIssues[], tolerating
the legacy flat-string shape so nothing else breaks. Tests flipped to the real
object shape (the Good Dog for this change). Full suite 315/315, build 0 warnings.

Also recorded in MyMistakes.md (API-shape recipe) and ai.md/Services index. End-of-session
transition(complete) 403 invalidTransition (enableAutoStop fallback) observed on all three
2026-09-22 stops — separate racy-design issue, carried, not in this change.
This commit is contained in:
2026-09-22 19:25:02 -07:00
parent e69db4d26c
commit 18ab553b0e
6 changed files with 94 additions and 50 deletions
+14 -8
View File
@@ -159,17 +159,22 @@ public class YouTubeStreamServiceTests
Assert.Empty(handler.Requests); // never reached the API
}
// TASK 5 item 3: liveStreams.status — healthStatus + configurationIssues[]
// parsed into the report-by-exception surface.
// TASK 5 item 3: liveStreams.status — status.healthStatus = {status,
// configurationIssues[]} (the REAL API shape — nested object, not a string;
// the flat-string shape was our own wrong fixture) parsed into the
// report-by-exception surface. This fixture is what a live test/real stream
// actually returns — it used to crash the poll with
// "requires an element of type 'String'" (2026-09-22 log, two sessions).
[Fact]
public async Task GetStreamHealthAsync_Parses_Status_And_Issue_Severities()
{
var handler = new RecordingHandler
{
ResponseBody = """
{"items":[{"status":{"healthStatus":"bad","configurationIssues":[
{"items":[{"status":{"healthStatus":{"status":"bad","lastUpdateTimeSeconds":1748000000,
"configurationIssues":[
{"type":"bitrateHigh","severity":"error"},
{"type":"reconnect","severity":"warning"}]}}]}
{"type":"reconnect","severity":"warning"}]}}}]}
"""
};
var service = new YouTubeStreamService(CreateAuthed(), new HttpClient(handler));
@@ -188,7 +193,7 @@ public class YouTubeStreamServiceTests
[Fact]
public async Task GetStreamHealthAsync_Good_Health_Yields_No_Issues()
{
var handler = new RecordingHandler { ResponseBody = """{"items":[{"status":{"healthStatus":"good"}}]}""" };
var handler = new RecordingHandler { ResponseBody = """{"items":[{"status":{"healthStatus":{"status":"good"}}}]}""" };
var service = new YouTubeStreamService(CreateAuthed(), new HttpClient(handler));
var health = await service.GetStreamHealthAsync("S456");
@@ -218,7 +223,7 @@ public class YouTubeStreamServiceTests
{
var handler = new RecordingHandler
{
ResponseBody = """{"items":[{"status":{"healthStatus":"good"}}]}"""
ResponseBody = """{"items":[{"status":{"healthStatus":{"status":"good"}}}]}"""
};
var service = new YouTubeStreamService(CreateAuthed(), new HttpClient(handler));
@@ -228,8 +233,9 @@ public class YouTubeStreamServiceTests
Assert.Null(StreamHealthReporter.BannerFor(healthy!.ConfigurationIssues).Text);
handler.ResponseBody = """
{"items":[{"status":{"healthStatus":"bad","configurationIssues":[
{"type":"bitrateHigh","severity":"error"}]}}]}
{"items":[{"status":{"healthStatus":{"status":"bad",
"configurationIssues":[
{"type":"bitrateHigh","severity":"error"}]}}}]}
""";
var degraded = await service.GetStreamHealthAsync("S456");
Assert.Equal("bad", degraded?.HealthStatus);