T34: priorityInbox (M.8) — paid_boost clips get inserted immediately
after whatever's currently playing (danceSchedule[0]), pushing every
later entry's timing back, rather than waiting for the fill-loop's
normal "top up to 30s ahead" trigger. That distinction mattered:
draining priorityInbox only from inside the fill loop (first attempt)
technically satisfied "never loses to a vote-ranked pick" but could
leave a ready paid_boost clip waiting a long time if the schedule
already had ~30s queued — dance-mode3.test.js's race test caught this
(timed out) before the eager-insert fix. ensureDanceSchedule's existing
"generating -> done" reconciliation loop diverts a job into
priorityInbox instead of danceQueue's "ready" state when
job.priority === "paid_boost", spliced out of danceQueue after the
loop (not mid-iteration).
T35: POST /api/dance/upload-character (需登录+emailVerified, same rule
as T27). Face photo -> T2 upload+EXIF strip -> T32 moderateFaceBlacklist
-> T25-style describeCharacterImage (new pluggable fn, shares
VISION_API_KEY) -> characters.create(source:"user_paid", isDefault:
false, totalSegments/remainingSegments computed from pricing_config's
new spend_character_per_segment×segmentsPerUnit) -> creditLedger.spend
(relatedAssetId=character.id, rolled back on failure) ->
submitGeneration(..., priority:"paid_boost") -> lands in priorityInbox
(cache hit) or danceQueue pending T34's reconciliation (fresh
generation). Garment is optional (need求文档 3.3 第 4 点): given
garmentId is validated active-or-400, otherwise a random active
garment is picked — a fully empty wardrobe is a 400 before any spend.
dance-mode3.test.js covers both: auth/balance/no-active-garments gates,
the happy path (cost scales with requested segments, character record
shape, clip carries priority/requestedByUserId), and T34's actual
guarantee — a paid_boost character is scheduled at or ahead of an
already-ready free pick for the same garment, distinguished by
characterId since both target the same garment on purpose.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo