fix(stream): chat insert 400 — insert body must declare snippet.type
The liveChatId fix (TASK 44) worked on the next Test Stream, but every Mock
Chat Input send returned 'YouTube rejected the message (error 400)'. The
runtime log's body: 400 MISSING_REQUIRED_FIELD, domain
youtube.api.v3.LiveChatMessageInsertResponse.Error.
The liveChat/messages.insert snippet requires type ('textMessageEvent' or
'pollEvent') alongside liveChatId and textMessageDetails.messageText; the
TASK 41 body omitted it, and the Good Dog test asserted only liveChatId +
messageText were present — false-green while real YouTube rejected every
send. Fix body + assert type in the same test so the field can never drop
silently again.
Reference: https://developers.google.com/youtube/v3/live/docs/liveChatMessages/insert
Good Dog: ONE integration test (strengthened TestStream_DockTooling...).
Gate: clean build 0 warnings; full vstest 317/318 (the 1 failure is the
known environmental RealMouseDrag flake — passes 3/3 in isolation).
This commit is contained in:
+37
-32
@@ -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.
|
||||
Reference in New Issue
Block a user