fix(gateway): имена событий и resumable-флаг по спецификации §8.3

- ROLE_CREATE/ROLE_UPDATE/ROLE_DELETE вместо GUILD_ROLE_*;
- выдача/снятие роли приходит как MEMBER_UPDATE с полным списком role_ids:
  клиент пересчитывает доступные действия сразу после события;
- промах RESUME отдаёт INVALID_SESSION с {"resumable": true} и следом полный
  READY — сессия сохраняется, повторный вход не нужен;
- тесты обновлены под новый контракт.
This commit is contained in:
2026-09-19 21:57:46 +03:00
parent 463021161a
commit d6ca8e420f
4 changed files with 47 additions and 13 deletions
+15 -1
View File
@@ -357,9 +357,23 @@ func TestResumeReplaysMissedEvents(t *testing.T) {
t.Fatalf("resume replay = %q, want CHANNEL_CREATE", replayed.T)
}
// RESUME с неизвестной последовательностью приводит к полному READY.
// RESUME с неизвестной последовательностью: INVALID_SESSION с признаком
// resumable и следом полный READY (AGENT.md 8.3).
third, _ := f.dial(t)
send(t, third, gateway.OpResume, map[string]any{"token": token, "resume_seq": ready.S + 10000})
invalid := readEnvelope(t, third)
if invalid.Op != gateway.OpInvalidSess {
t.Fatalf("stale resume op = %d, want INVALID_SESSION", invalid.Op)
}
var invalidPayload struct {
Resumable bool `json:"resumable"`
}
if err := json.Unmarshal(invalid.D, &invalidPayload); err != nil {
t.Fatalf("decode INVALID_SESSION: %v", err)
}
if !invalidPayload.Resumable {
t.Fatal("resume failure must be marked resumable: the client keeps the session")
}
full := readEnvelope(t, third)
if full.T != "READY" {
t.Fatalf("stale resume = %q, want full READY", full.T)
+12 -2
View File
@@ -217,7 +217,14 @@ func (s *Service) readLoop(ctx context.Context, session *clientSession) {
// sendInvalidSession сообщает об ошибке идентификации и дожидается отправки
// кадра: соединение закрывается сразу после этого (AGENT.md 8.3).
func (s *Service) sendInvalidSession(session *clientSession, reason string) {
payload, err := frame(OpInvalidSess, map[string]any{"reason": reason})
s.sendInvalidSessionResumable(session, reason, false)
}
// sendInvalidSessionResumable отдаёт INVALID_SESSION с признаком resumable:
// при true клиент продолжает работу (сервер пришлёт полный READY), при false —
// уходит на экран входа.
func (s *Service) sendInvalidSessionResumable(session *clientSession, reason string, resumable bool) {
payload, err := frame(OpInvalidSess, map[string]any{"reason": reason, "resumable": resumable})
if err != nil {
return
}
@@ -276,7 +283,10 @@ func (s *Service) handleIdentify(ctx context.Context, session *clientSession, pa
s.register(session, authSession.TokenHash)
return nil
}
// Буфер не содержит нужную последовательность: полный READY (AGENT.md 8.3).
// Буфер не содержит нужную последовательность: по AGENT.md 8.3 клиенту
// сообщаем INVALID_SESSION с признаком resumable и сразу отдаём полный
// READY, чтобы состояние восстановилось без повторного входа.
s.sendInvalidSessionResumable(session, "resume failed", true)
}
snapshot, err := s.readiness.Build(ctx, user)