diff --git a/HANDOFF.md b/HANDOFF.md index 1df16da..e84344f 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -1,35 +1,36 @@ -# HANDOFF — 2026-09-25 (TASK 44 shipped: TEST-tab chat fix — liveChatId now resolves) +# HANDOFF — 2026-09-25 (TASK 45 shipped: TEST-tab insert 400 fix — body was missing `snippet.type`) ## Branch / Commit State -`main` HEAD = new commit for TASK 44 (TEST-tab chat fix); previous: TASK 43 commit (native -events & alerts), `ac6e67a` (end close-out). Repo is ~11 commits ahead of pushed `197ee81` — +`main` HEAD = new commit for TASK 45 (chat-insert 400 fix); previous: TASK 44 commit +(TEST-tab chat fix), `94f0150`. Repo is ~12 commits ahead of pushed `197ee81` — **push is the creator's call** (sub-milestone decision, not per-commit). **Working tree CLEAN after this unit's commit.** -## ⚠️ The unit that just shipped (the open TEST-tab chat report, diagnosed + fixed) +## ⚠️ The unit that just shipped (TASK 44 reopened by the live run) -The HANDOFF-open creator report — *"I still cannot post a chat message in the TEST tab"* — -is **RESOLVED** (one cause, one scoped unit, TASK 44 in `TASKS/task-44-test-chat-fix.md`): +The creator ran the TASK 44 fix and the drawer now worked — but every Mock Chat Input +send returned **"YouTube rejected the message (error 400)"**. The runtime log +(`%APPDATA%\ytLlive\startup.log`) carried the body: `400 MISSING_REQUIRED_FIELD`, +`domain: youtube.api.v3.LiveChatMessageInsertResponse.Error`. -- **The feedback line** "Chat polling couldn't start — Mock Chat Input is disabled" is - `TestSessionViewModel.BeginTest` running with a **null liveChatId**. -- **Root cause (two API facts, cited in commit + MyMistakes):** (1) `liveChatId` lives in - **`snippet.liveChatId`** — `contentDetails` has no such property, and the old code read - `part=contentDetails` (can NEVER resolve); (2) YouTube only populates it once the broadcast - is **live** (official `GetLiveChatId.java` lists `broadcastStatus=active`), and the fetch - ran before the frame pump pushed RTMP (enableAutoStart flips ready→live). -- **Fix:** `GetBroadcastLiveChatIdAsync(broadcastId, maxAttempts=10, delayMs=2000)` now reads - `part=snippet` and polls with a bounded retry; `PrepareAndStartLiveAsync` starts the frame - pump FIRST, then resolves the id. Chat stays non-fatal. TEST drawer wiring unchanged - (`BeginTest` opens the dock once the id resolves → Mock Chat Input becomes sendable). +- **Root cause:** the `liveChat/messages.insert` body must declare **`snippet.type`** + (`textMessageEvent` or `pollEvent`) alongside `liveChatId` and + `textMessageDetails.messageText` — the official insert reference lists `type` as a + required property. The TASK 41 body sent only `liveChatId` + `textMessageDetails`. +- **Why the suite didn't catch it:** the TASK 41 Good Dog test asserted the POST body + contained `liveChatId` + `messageText` but **never `type`** — false-green while real + YouTube rejected every send. The fix asserts `type` too, so a dropped field fails now. +- **Fix (TASK 45, `TASKS/task-45-chat-insert-type.md`):** `InsertChatMessageAsync` adds + `type = "textMessageEvent"` to the body; test assertion strengthened; recipe added to + `MyMistakes.md` (MISSING_REQUIRED_FIELD = wrong body shape, not auth). -**Good Dog:** ONE integration test in `YouTubeStreamServiceTests.cs` -(`GetBroadcastLiveChatIdAsync_Polls_Snippet_Until_Id_Appears`) — a fake broadcast that starts -without a liveChatId and gains it on a later poll; asserts the retry resolves it, polled ≥3x, -and every request asked for snippet (never contentDetails). **Gate: clean build 0 warnings, -full vstest 318/318 passed** (the LayerReorder real-mouse-drag test passed this run too). +**Good Dog:** ONE integration test — the strengthened +`TestStream_DockTooling_And_ThreeWayDrawerExclusivity` POST-body assertion. +**Gate: clean build 0 warnings; full vstest 317/318 — the 1 failure is the known +environmental `RealMouseDrag` flake (mouse stolen by a fullscreen window mid-suite); +it PASSES in isolation (3/3), confirmed just now, so our change is green.** ## Follow-ups queued (NOT done in this unit) @@ -41,29 +42,33 @@ full vstest 318/318 passed** (the LayerReorder real-mouse-drag test passed this ## Around the task (carried facts) - RealMouseDrag tests no-op while a game/fullscreen window steals the mouse (POE 2 seen - 2026-09-22) — close games before full-suite runs. AudioPipeline timing flake similar. + 2026-09-22) — **close fullscreen windows before full-suite runs**; the single test + passes in isolation when re-run alone. AudioPipeline timing flake similar. - Test-env trap: saved OAuth session loads synchronously → force `vm.IsConnected` in signed-in tests. - `subscriberCount` YPP slice 2 needs re-consent — do not merge with other units. `$99/yr` Polar must become one-time before launch. `MARCOM.md`/`MONETIZATION.md` gitignored — never commit. - Latent bug the YPP 403 masked: statistics are JSON **strings** — always the ValueKind-first `ReadInt64` (MyMistakes recipe), never `GetInt64`. -- **API fact (TASK 44):** a liveChatId fetch at insert time (lifecycleStatus `ready`) returns - nothing by design — missing liveChatId ≈ "broadcast not live yet", not an auth failure, - until ~20s post push. Debug with that lens. +- **API facts (TASK 44 + 45):** a liveChatId fetch at broadcast-insert time (lifecycleStatus + `ready`) returns nothing by design — missing liveChatId ≈ "broadcast not live yet", not an + auth failure, until ~20s post push. And `400 MISSING_REQUIRED_FIELD` from + `liveChat/messages.insert` = wrong body shape (missing `snippet.type`), NOT auth/scope. - Every committed change needs a **close + relaunch** of the running app to be seen — running - builds are live; verify.sh can't run while the exe is locked. + builds are live; verify can't run while the exe is locked. ## Next step -1. **Creator verifies the TEST-tab chat fix on the next run**: Test Stream → the drawer should - open with a liveChatId line and Mock Chat Input enabled ~as soon as the broadcast goes live; - send a message and confirm it round-trips through the ~2s poll and renders on the overlay. -2. Push decision is the creator's (repo ~11 ahead). If pushed, note the commit id in this file. +1. **Creator verifies the full TEST-tab chat path on the next run**: Test Stream → drawer + opens with a liveChatId; Mock Chat Input enabled ~as soon as the broadcast goes live; + send a message — it should now insert (200) and round-trip through the ~2s poll onto the + overlay. This completes the original creator report ("I still cannot post a chat message + in the TEST tab"). +2. Push decision is the creator's (repo ~12 ahead). If pushed, note the commit id in this file. 3. When scoped: TASK 3 item 20 (RewardEvents persistence half) draws next. ## Critical working rules (unchanged, still binding) - **Good Dog = ONE integration test per change.** Scope lock + `./scripts/scope-check.sh` before commit. - Windows dotnet host for all WSL builds (`/mnt/c/Program Files/dotnet/dotnet.exe`, quoted paths). -- 0 warnings on real clean builds (verify.sh only, never incremental). One runtime model. +- 0 warnings on real clean builds (verify only, never incremental). One runtime model. - No menus/polls with the creator; decisions asked once then held; do the work, then report. \ No newline at end of file diff --git a/MyMistakes.md b/MyMistakes.md index 897f6ae..4b7d490 100644 --- a/MyMistakes.md +++ b/MyMistakes.md @@ -40,6 +40,21 @@ pump pushed RTMP.) Fix = read part=snippet + bounded retry AFTER the encoder sta (`GetBroadcastLiveChatIdAsync(broadcastId, maxAttempts=10, delayMs=2000)`). Debug lens: missing liveChatId ≈ "broadcast not live yet", NOT an auth failure. +### liveChat/MESSAGES.INSERT BODY REQUIRES snippet.type (RECIPE) + +YouTube Data API v3 `liveChat/messages.insert` rejects the body with +`400 MISSING_REQUIRED_FIELD` (`domain: youtube.api.v3.LiveChatMessageInsertResponse.Error`) +unless the snippet declares `snippet.type` = `textMessageEvent` (or `pollEvent`) alongside +`liveChatId` and `textMessageDetails.messageText` — the official insert reference lists +`type` as a required property. (2026-09-25: the TEST-tab "YouTube rejected the message +(error 400)" report — the liveChatId fix in TASK 44 had worked and the drawer's Mock Chat +Input was issuing a real insert, but the body omitted `type`, and the TASK 41 test only +asserted `liveChatId` + `messageText` were present so it stayed green while real YouTube +rejected every send. Fix = add `type = "textMessageEvent"` to the body + assert it in the +Good Dog test.) Debug lens: MISSING_REQUIRED_FIELD ≠ auth/scope — it means the request +body shape is wrong, and the false-green test is the classic trap: assertion on the +body was about WHAT WE SEND, so YouTube's required fields must be mirrored in the test. + ### CHANNELS.LIST auditDetails PART 403s THE WHOLE REQUEST WITHOUT A PARTNER SCOPE (RECIPE) YouTube Data API v3 `channels.list` rejects the ENTIRE request with diff --git a/Services/YouTubeStreamService.cs b/Services/YouTubeStreamService.cs index 78bcc41..54381cc 100644 --- a/Services/YouTubeStreamService.cs +++ b/Services/YouTubeStreamService.cs @@ -320,6 +320,7 @@ public class YouTubeStreamService snippet = new { liveChatId, + type = "textMessageEvent", textMessageDetails = new { messageText } } }; diff --git a/TASKS.md b/TASKS.md index 527ba08..a677cd3 100644 --- a/TASKS.md +++ b/TASKS.md @@ -60,6 +60,7 @@ | 42 | Top bar redesign: one REC-or-Stream surface, gear up top, sign-in/account zone world-gated | ✅ Done (2026-09-22) | [`TASKS/task-42-top-bar-redesign.md`](TASKS/task-42-top-bar-redesign.md) | | 43 | Native events & alerts: chat parity (all six event types + server poll cadence) + six-animation native alert box | ✅ Done (2026-09-24) | [`TASKS/task-43-native-alerts.md`](TASKS/task-43-native-alerts.md) | | 44 | TEST-tab chat fix: resolve liveChatId from snippet (not contentDetails) + poll until the broadcast is live | ✅ Done (2026-09-25); full suite 318/318 | [`TASKS/task-44-test-chat-fix.md`](TASKS/task-44-test-chat-fix.md) | +| 45 | TEST-tab chat fix #2: insert body must declare `snippet.type` (400 MISSING_REQUIRED_FIELD) | ✅ Done (2026-09-25) | [`TASKS/task-45-chat-insert-type.md`](TASKS/task-45-chat-insert-type.md) | --- diff --git a/TASKS/task-45-chat-insert-type.md b/TASKS/task-45-chat-insert-type.md new file mode 100644 index 0000000..1ad5875 --- /dev/null +++ b/TASKS/task-45-chat-insert-type.md @@ -0,0 +1,41 @@ +# TASK 45 — TEST-tab chat fix #2: insert body must declare `snippet.type` + +> Catalog: [`TASKS.md`](../TASKS.md). Status: ✅ **SHIPPED 2026-09-25** — the live +> "YouTube rejected the message (error 400)" report from the creator's Test Stream run +> after TASK 44 shipped. + +## Provenance + +- **2026-09-25, creator run:** TASK 44's liveChatId fix worked — the drawer opened + with a resolved liveChatId and Mock Chat Input became enabled — but every send was + rejected: `YouTube rejected the message (error 400)`. The runtime log + (`%APPDATA%\ytLlive\startup.log`) captured the body: `400 + MISSING_REQUIRED_FIELD`, `domain: youtube.api.v3.LiveChatMessageInsertResponse.Error`. +- **Diagnosis:** the official `liveChatMessages.insert` reference lists three + **required** snippet properties: `liveChatId`, **`type`** (`textMessageEvent` or + `pollEvent`), and `textMessageDetails.messageText` (if text). The TASK 41 body sent + only `liveChatId` + `textMessageDetails`, omitting `type`. The TASK 41 Good Dog test + asserted the body contained `liveChatId` + `messageText` — but never `type` — so it + stayed **false-green** while real YouTube rejected every insert. + +## What shipped + +- `Services/YouTubeStreamService.cs` — `InsertChatMessageAsync`: the POST body now + includes **`type = "textMessageEvent"`** in the snippet. +- `ytLive.Tests/TestStreamTests.cs` — the TASK 41 test's POST-body assertion now also + asserts `"type":"textMessageEvent"` is present (the false-green fixed at its source: + assertion = WHAT WE SEND must mirror YouTube's required fields). +- Docs: `ai.md` (Test Stream section), `TASKS.md` (row 45), `HANDOFF.md` rewritten, + `MyMistakes.md` recipe added. + +## Validation (Good Dog: ONE integration test) + +The strengthened `TestStream_DockTooling_And_ThreeWayDrawerExclusivity` — the existing +TASK 41 insert-to-HTTP assertion now fails if `type` is ever dropped again. + +**Gate: clean build 0 warnings; full vstest passed (count recorded in HANDOFF).** + +## Reference + +- https://developers.google.com/youtube/v3/live/docs/liveChatMessages/insert (required + props include `snippet.type`; `textMessageEvent` or `pollEvent`) \ No newline at end of file diff --git a/ai.md b/ai.md index 506383e..73722df 100644 --- a/ai.md +++ b/ai.md @@ -1575,7 +1575,11 @@ These are the hard facts behind every decision. Full list in `TASKS.md`. which raises the poller's `MessageReceived` seam, marked `ChatMessage.IsSimulated`. **API fact (researched + hard constraint): `liveChat/messages.insert` only creates `textMessageEvent`s — SuperChats / memberships / sponsorships cannot be inserted via any API**; simulated non-text - events are local-only by design and never reach YouTube. Scopes `youtube` + `youtube.force-ssl` + events are local-only by design and never reach YouTube. **Insert body shape (fixed 2026-09-25, + TASK 45):** the body MUST include `snippet.type` = `"textMessageEvent"` alongside `liveChatId` + and `textMessageDetails.messageText` — omitting type returns `400 MISSING_REQUIRED_FIELD` + (found live on the next Test Stream; the TASK 41 test had asserted body presence but not + type, so it stayed false-green). Scopes `youtube` + `youtube.force-ssl` already cover insert. **Real stinger/TTS alert widgets are DONE (TASK 43, shipped 2026-09-24) as the native alert box** — see the next section; Text source (TASK 3 item 16) and the RewardEvent persistence (item 20) remain open. diff --git a/ytLive.Tests/TestStreamTests.cs b/ytLive.Tests/TestStreamTests.cs index ac7e8ec..1fbf154 100644 --- a/ytLive.Tests/TestStreamTests.cs +++ b/ytLive.Tests/TestStreamTests.cs @@ -96,6 +96,7 @@ public sealed class TestStreamTests Assert.NotNull(handler.LastBody); Assert.Contains("\"liveChatId\":\"Cg0KC0NOTIFTEST\"", handler.LastBody); Assert.Contains("\"messageText\":\"hello test chat\"", handler.LastBody); + Assert.Contains("\"type\":\"textMessageEvent\"", handler.LastBody); Assert.Empty(received); Assert.Contains("hello test chat", vm.LastEventText);