GuildFriendListTask.Parse() reads base.ResponseData[data][i] directly —
data must be a bare JSON array, not {friends:[...]}. Delete the
GuildFriendListResponse wrapper DTO; promote GuildInviteCandidateDto to its
own file. Controller now returns List<GuildInviteCandidateDto> so the
middleware wraps it as data:[…]. Update wire-shape and integration tests to
assert array-at-root.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ChangeRoleAsync: Leader-only role management with SubLeader cap check.
Atomic leader transfer loads both member rows + Guild.LeaderViewerId in
one _db.SaveChangesAsync call (no split saves). Emits ChangeLeader (6)
or ChangeSubLeader (7) chat events. Same-role is no-op / returns Ok.
- /guild/change_role controller: calls ChangeRoleAsync, returns full updated
member list (GuildChangeRoleResponse.Members[]).
- /guild/friend_list controller: calls IFriendService.GetFriendsAsync, then
annotates each friend with is_join_guild from Viewer.GuildId column.
- IGuildRepository.UpdateLeaderViewerIdAsync: added for completeness even
though the atomic transfer path bypasses it via direct EF mutation.
- Tests: 8 GuildServiceChangeRoleTests (role changes, cap, atomic transfer,
no-op, permission); 3 GuildChangeRoleFriendListFlowTests (HTTP roundtrip,
is_join_guild); 2 GuildWireShape tests (literal JSON shape); 1508 pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>