feat(server): статус комнаты в снапшоте READY

Шапка комнаты показывает статус (описание) рядом с названием, но в
снапшоте этого поля не было: клиент получал описание только REST-запросом
списка комнат, поэтому статус появлялся с задержкой и не приходил в
событиях комнат (AGENT.md 7.5, 11.6).

- `ReadyChannel.Description` + заполнение в `channelPayload` — статус
  приходит и в READY, и в `CHANNEL_CREATE`/`CHANNEL_UPDATE` (те же
  payload'ы, что у REST);
- Go-тест `TestReadyCarriesChannelStatus`: описание и медленный режим
  доезжают до снапшота.

Клиентская часть (парсер и шапка) идёт следующим коммитом.
This commit is contained in:
2026-09-26 11:06:05 +03:00
parent 19d2501ab0
commit 61cad0fdfc
4 changed files with 84 additions and 8 deletions
+69
View File
@@ -0,0 +1,69 @@
package server
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
// TestReadyCarriesChannelStatus проверяет, что снапшот несёт статус комнаты —
// описание и медленный режим: шапка показывает их рядом с названием, включая
// чужой сервер у инстанс-админа (AGENT.md 7.5, 11.6).
func TestReadyCarriesChannelStatus(t *testing.T) {
srv, _ := newTestServer(t)
httpServer := httptest.NewServer(srv.Handler())
t.Cleanup(httpServer.Close)
ownerCookie := registerAndLogin(t, srv, "status_owner", "status-owner@example.com")
created := doJSON(t, srv, http.MethodPost, "/api/v1/guilds", `{"name":"Статусы"}`, ownerCookie)
guild := decodeResponse[struct {
Guild struct {
ID string `json:"id"`
} `json:"guild"`
}](t, created)
channels := doJSON(t, srv, http.MethodGet, "/api/v1/guilds/"+guild.Guild.ID+"/channels", "", ownerCookie)
channel := decodeResponse[struct {
Channels []struct {
ID string `json:"id"`
} `json:"channels"`
}](t, channels)
if len(channel.Channels) == 0 {
t.Fatal("у нового сервера нет комнат")
}
patched := doJSON(t, srv, http.MethodPatch,
"/api/v1/guilds/"+guild.Guild.ID+"/channels/"+channel.Channels[0].ID,
`{"description":"Статус комнаты","slowmode_seconds":5}`, ownerCookie)
if patched.Code != http.StatusOK {
t.Fatalf("описание комнаты = %d, body = %s", patched.Code, patched.Body.String())
}
client := dialGateway(t, httpServer, ownerCookie)
var snapshot struct {
Guilds []struct {
ID string `json:"id"`
Channels []struct {
Description string `json:"description"`
Slowmode int `json:"slowmode_seconds"`
} `json:"channels"`
} `json:"guilds"`
}
if err := json.Unmarshal(client.ready.D, &snapshot); err != nil {
t.Fatalf("decode READY: %v", err)
}
described := false
for _, item := range snapshot.Guilds {
if item.ID != guild.Guild.ID {
continue
}
for _, ch := range item.Channels {
if ch.Description == "Статус комнаты" && ch.Slowmode == 5 {
described = true
}
}
}
if !described {
t.Fatalf("READY не несёт статус комнаты: %+v", snapshot.Guilds)
}
}
+3
View File
@@ -17,6 +17,8 @@ import (
type realtimeClient struct {
t *testing.T
conn *websocket.Conn
// ready — тело кадра READY: тесты снапшота читают из него списки серверов.
ready realtimeFrame
}
// dialGateway подключается к /gateway и проходит IDENTIFY пустым токеном:
@@ -47,6 +49,7 @@ func dialGateway(t *testing.T, server *httptest.Server, cookie *http.Cookie) *re
if ready.T != "READY" {
t.Fatalf("frame = %q, want READY", ready.T)
}
client.ready = ready
return client
}