`GetTrainerServiceInfo`'s second return was nil whenever the spell had no
rank. The client reads that field out of the spell record's string block,
where an absent rank is the empty string at offset 0 rather than a null, so
it is always a string — and the stock window depends on that: it formats the
subtext unguarded at `Blizzard_TrainerUI.lua:275`, twenty lines before its
own `not serviceSubText` fallback.
So a nil raised inside `ClassTrainer_SelectFirstLearnableSkill`, which
`ClassTrainerFrame_Show` calls on the line above `ClassTrainerFrame_Update`
(l.48-49). The update never ran, so the list came up with no rows at all,
and `SelectTrainerService` (l.277) never ran, so the Train button had
nothing to act on. A trainer window that shows nothing and trains nothing,
from one nil.
Every rank-less service reaches that line: every profession recipe, and
class abilities like Parry.
The name beside it stays optional, because the stock window says so itself —
it tests `serviceName` before using it as a header's (l.209) and falls back
to `UNKNOWN` for a service's (l.287).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn