feat(guild): leave + remove with leader-leaves rule

- LeaveAsync: regular/subleader removes self; leader blocked if members remain;
  sole-member leader auto-routes to BreakupAsync.
- RemoveAsync: leader-only kick (SubLeader has no remove authority per
  GuildManager.cs decompile); self-remove and cross-guild target rejected.
- GuildController: wired /guild/leave (BaseRequest) and /guild/remove
  (GuildRemoveRequest.remove_viewer_id field).
- SpyGuildChatService: extended Emissions with EmissionsWithBody tuple to
  allow body-content assertions in leave/remove tests.
- 8 new integration tests; all 1493 pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-06-27 14:16:47 -04:00
parent 901c3d7568
commit 00fb28681b
4 changed files with 391 additions and 6 deletions

View File

@@ -299,10 +299,20 @@ public sealed class GuildController : SVSimController
}
[HttpPost("leave")]
public Task<ActionResult<EmptyResponse>> Leave([FromBody] BaseRequest _, CancellationToken ct) => Stub();
public async Task<ActionResult<EmptyResponse>> Leave([FromBody] BaseRequest _, CancellationToken ct)
{
if (!TryGetViewerId(out var viewerId)) return Unauthorized();
var r = await _guild.LeaveAsync(viewerId, ct);
return r.IsOk ? new EmptyResponse() : MapErrorToWire(r);
}
[HttpPost("remove")]
public Task<ActionResult<EmptyResponse>> Remove([FromBody] GuildRemoveRequest req, CancellationToken ct) => Stub();
public async Task<ActionResult<EmptyResponse>> Remove([FromBody] GuildRemoveRequest req, CancellationToken ct)
{
if (!TryGetViewerId(out var viewerId)) return Unauthorized();
var r = await _guild.RemoveAsync(viewerId, req.RemoveViewerId, ct);
return r.IsOk ? new EmptyResponse() : MapErrorToWire(r);
}
[HttpPost("change_role")]
public Task<ActionResult<GuildChangeRoleResponse>> ChangeRole([FromBody] GuildChangeRoleRequest req, CancellationToken ct)