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.
|
||||
@@ -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
|
||||
|
||||
@@ -320,6 +320,7 @@ public class YouTubeStreamService
|
||||
snippet = new
|
||||
{
|
||||
liveChatId,
|
||||
type = "textMessageEvent",
|
||||
textMessageDetails = new { messageText }
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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`)
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user