fix(install): публичный адрес LiveKit вычисляется после TLS_ENABLED
Установщик падал на 'TLS_ENABLED: unbound variable': расчёт LIVEKIT_PUBLIC_URL стоял выше определения TLS_ENABLED. Значение перенесено после блока TLS, dry-run проходит.
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { render, screen, waitFor, within } from '@testing-library/react';
|
||||
|
||||
import type { Message } from '@/api/messages';
|
||||
import { MessageList } from '@/components/chat/MessageList';
|
||||
import type { AuthorDirectory } from '@/components/chat/useAuthorDirectory';
|
||||
import { parseSystemMessage, systemMessageUserId } from '@/lib/systemMessage';
|
||||
import { makeMessage, makeUser, json, installFetch, installGatewaySocket, messagesRoutes, renderApp, resetStores } from './helpers';
|
||||
|
||||
/**
|
||||
* Системные сообщения (AGENT.md §7.6): сервер пишет их с `type: "system"`,
|
||||
* а в `content` — JSON о событии. Лента показывает человекочитаемую строку
|
||||
* с именем участника, а неизвестное событие и мусорный JSON — как есть.
|
||||
*/
|
||||
|
||||
const authors: AuthorDirectory = {
|
||||
resolve: (userId) => ({
|
||||
id: userId ?? '',
|
||||
name: userId === 'user-2' ? 'Боб' : 'неизвестный',
|
||||
avatarFileId: undefined,
|
||||
colorHex: null,
|
||||
isInstanceAdmin: false,
|
||||
isMember: userId === 'user-2',
|
||||
}),
|
||||
resolveMention: (userId) => ({ name: userId === 'user-2' ? 'Боб' : userId, isMe: false }),
|
||||
resolveMemberName: (userId) => (userId === 'user-2' ? 'Боб' : null),
|
||||
loaded: true,
|
||||
};
|
||||
|
||||
function systemMessage(content: string, id = 'm-1'): Message {
|
||||
return makeMessage({
|
||||
id,
|
||||
channel_id: 'c-1',
|
||||
content,
|
||||
type: 'system',
|
||||
});
|
||||
}
|
||||
|
||||
function renderList(messages: Message[]) {
|
||||
return render(
|
||||
<MessageList
|
||||
channelId="c-1"
|
||||
channelName="общий"
|
||||
messages={messages}
|
||||
status="ready"
|
||||
error={null}
|
||||
hasMore={false}
|
||||
loadingMore={false}
|
||||
unreadAnchorId={null}
|
||||
highlightId={null}
|
||||
authors={authors}
|
||||
currentUserId="user-1"
|
||||
canManageMessages
|
||||
canSend
|
||||
onReload={() => undefined}
|
||||
onLoadMore={() => undefined}
|
||||
onReply={() => undefined}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
resetStores();
|
||||
});
|
||||
|
||||
describe('системные сообщения: разбор payload', () => {
|
||||
it('читает события участника и комнаты', () => {
|
||||
expect(parseSystemMessage('{"event":"member_join","user_id":"123"}')).toEqual({
|
||||
kind: 'member_join',
|
||||
userId: '123',
|
||||
});
|
||||
expect(parseSystemMessage('{"event":"member_kick","user_id":"123"}')).toEqual({
|
||||
kind: 'member_kick',
|
||||
userId: '123',
|
||||
});
|
||||
expect(parseSystemMessage('{"event":"channel_created","name":"общий","type":"text"}')).toEqual({
|
||||
kind: 'channel_created',
|
||||
name: 'общий',
|
||||
channelType: 'text',
|
||||
});
|
||||
expect(systemMessageUserId('{"event":"member_kick","user_id":"123"}')).toBe('123');
|
||||
expect(systemMessageUserId('{"event":"channel_created","name":"общий"}')).toBeNull();
|
||||
});
|
||||
|
||||
it('не падает на мусоре и неизвестном событии', () => {
|
||||
expect(parseSystemMessage('Аля вошла')).toBeNull();
|
||||
expect(parseSystemMessage('{"event":"guild_boost"}')).toBeNull();
|
||||
expect(parseSystemMessage('{"event":"member_join"}')).toBeNull();
|
||||
expect(parseSystemMessage('"строка"')).toBeNull();
|
||||
expect(parseSystemMessage('null')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('системные сообщения: лента', () => {
|
||||
it('member_join — «{имя} присоединился к серверу»', () => {
|
||||
renderList([systemMessage('{"event":"member_join","user_id":"user-2"}')]);
|
||||
|
||||
const body = screen.getByTestId('message-m-1');
|
||||
expect(body).toHaveTextContent('Боб присоединился к серверу');
|
||||
// Сырой JSON в ленте не показываем.
|
||||
expect(body.textContent).not.toContain('member_join');
|
||||
});
|
||||
|
||||
it('member_kick — «{имя} покинул сервер (исключён)»', () => {
|
||||
renderList([systemMessage('{"event":"member_kick","user_id":"user-2"}')]);
|
||||
|
||||
expect(screen.getByTestId('message-m-1')).toHaveTextContent(
|
||||
'Боб покинул сервер (исключён)',
|
||||
);
|
||||
});
|
||||
|
||||
it('channel_created — комната и голосовая комната', () => {
|
||||
renderList([
|
||||
systemMessage('{"event":"channel_created","name":"общий","type":"text"}', 'm-1'),
|
||||
systemMessage('{"event":"channel_created","name":"Голосовая","type":"voice"}', 'm-2'),
|
||||
]);
|
||||
|
||||
expect(screen.getByTestId('message-m-1')).toHaveTextContent('Комната #общий создана');
|
||||
expect(screen.getByTestId('message-m-2')).toHaveTextContent(
|
||||
'Голосовая комната Голосовая создана',
|
||||
);
|
||||
});
|
||||
|
||||
it('неизвестное событие и невалидный JSON показываются как есть', () => {
|
||||
renderList([
|
||||
systemMessage('{"event":"guild_boost","level":2}', 'm-1'),
|
||||
systemMessage('Аля вошла', 'm-2'),
|
||||
]);
|
||||
|
||||
expect(screen.getByTestId('message-m-1')).toHaveTextContent('{"event":"guild_boost","level":2}');
|
||||
expect(screen.getByTestId('message-m-2')).toHaveTextContent('Аля вошла');
|
||||
});
|
||||
|
||||
it('неизвестный участник подписан «участник»', () => {
|
||||
renderList([systemMessage('{"event":"member_join","user_id":"user-9"}')]);
|
||||
|
||||
expect(screen.getByTestId('message-m-1')).toHaveTextContent('участник присоединился к серверу');
|
||||
});
|
||||
|
||||
it('остаётся центрированной строкой без аватара и действий', () => {
|
||||
renderList([systemMessage('{"event":"member_join","user_id":"user-2"}')]);
|
||||
|
||||
const body = screen.getByTestId('message-m-1');
|
||||
expect(body).toHaveClass('items-center', 'text-center');
|
||||
expect(body).toHaveAttribute('data-message-type', 'system');
|
||||
expect(within(body).queryByRole('button')).toBeNull();
|
||||
expect(screen.getByTestId('message-group').querySelector('.inline-flex.shrink-0')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('системные сообщения: имя через справочник авторов', () => {
|
||||
it('добирает имя участника по GET /users/{id}', async () => {
|
||||
const user = makeUser();
|
||||
const snapshot = {
|
||||
user: {
|
||||
id: 'user-1',
|
||||
username: 'alice',
|
||||
display_name: 'Alice',
|
||||
is_instance_admin: false,
|
||||
badges: [],
|
||||
},
|
||||
guilds: [
|
||||
{
|
||||
id: 'g-1',
|
||||
name: 'Сервер',
|
||||
owner_id: 'user-1',
|
||||
is_main: true,
|
||||
channels: [{ id: 'c-1', name: 'общий', type: 'text', position: 0 }],
|
||||
roles: [],
|
||||
member_ids: ['user-1'],
|
||||
my_role_ids: [],
|
||||
my_permissions: ['SEND_MESSAGES'],
|
||||
emojis: [],
|
||||
},
|
||||
],
|
||||
dm_channels: [],
|
||||
read_states: [],
|
||||
session_id: 'session-1',
|
||||
heartbeat_interval_ms: 45_000,
|
||||
};
|
||||
|
||||
installFetch([
|
||||
{ match: '/api/v1/users/@me/guilds', response: () => json({ guilds: [] }) },
|
||||
{ match: '/api/v1/users/@me', response: () => json({ user }) },
|
||||
{ match: '/api/v1/instance', response: () => json({ instance: { name: 'glchat-test' } }) },
|
||||
// Участника нет в списке сервера — имя приходит из GET /users/{id}.
|
||||
{ match: '/api/v1/guilds/g-1/members', response: () => json({ members: [] }) },
|
||||
{ match: '/api/v1/guilds/g-1/roles', response: () => json({ roles: [] }) },
|
||||
{
|
||||
match: '/api/v1/users/user-9',
|
||||
response: () =>
|
||||
json({
|
||||
user: {
|
||||
id: 'user-9',
|
||||
username: 'carol',
|
||||
display_name: 'Кэрол',
|
||||
is_instance_admin: false,
|
||||
badges: [],
|
||||
},
|
||||
}),
|
||||
},
|
||||
{ match: '/api/v1/meta', response: () => json({ max_upload_size: 26_214_400 }) },
|
||||
...messagesRoutes('c-1', [
|
||||
makeMessage({
|
||||
id: 'm-1',
|
||||
channel_id: 'c-1',
|
||||
type: 'system',
|
||||
content: '{"event":"member_join","user_id":"user-9"}',
|
||||
}),
|
||||
]),
|
||||
]);
|
||||
|
||||
const gateway = installGatewaySocket(snapshot);
|
||||
renderApp('/app/g-1/c-1');
|
||||
await screen.findByTestId('guild-rail');
|
||||
await gateway.greet();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('message-m-1')).toHaveTextContent('Кэрол присоединился к серверу');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user