Skip to content
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
d3fbcf9
move offline agents filtering to query root
nazabucciarelli Mar 10, 2026
04dc75a
fix user presence update from test users
nazabucciarelli Mar 11, 2026
81cf18e
fix linter error
nazabucciarelli Mar 11, 2026
391cce0
Merge branch 'develop' into fix/livechat-offline-agent-assignment
nazabucciarelli Mar 11, 2026
cae2152
rollback TEST_MODE conditional
nazabucciarelli Mar 11, 2026
0942d4a
add ws user helpers to fix connectionStatus on tests
nazabucciarelli Mar 12, 2026
9102e16
test on CI without closing ws
nazabucciarelli Mar 12, 2026
395c57b
add use of setUserAwayWS to routing test
nazabucciarelli Mar 12, 2026
6ebec42
enhance ws connection, add ws closures
nazabucciarelli Mar 13, 2026
8b41cd9
change ws port to 3000
nazabucciarelli Mar 13, 2026
6f3c33f
update idle status tests from routing
nazabucciarelli Mar 13, 2026
79d8eb9
fix playwright tests
nazabucciarelli Mar 16, 2026
46d52d0
add ws closure to playwright tests
nazabucciarelli Mar 16, 2026
6c3b010
improve ws cleanup on rooms test, make ws port depend on CI env variable
nazabucciarelli Mar 16, 2026
8137749
fix conflict with develop
nazabucciarelli Mar 17, 2026
7c2f150
remove unintended test lines
nazabucciarelli Mar 17, 2026
fcd37ad
Merge branch 'develop' into fix/livechat-offline-agent-assignment
nazabucciarelli Mar 17, 2026
02239ef
add changeset
nazabucciarelli Mar 17, 2026
ba26d7f
remove unneeded ws closures
nazabucciarelli Mar 17, 2026
e287b5b
handle errors on websockets result
nazabucciarelli Mar 17, 2026
1ae6230
remove accidental timeout
nazabucciarelli Mar 18, 2026
6134a5c
add DDP_LOGIN_PORT env variable for ws connection
nazabucciarelli Mar 19, 2026
f6e4a54
Merge branch 'develop' into fix/livechat-offline-agent-assignment
nazabucciarelli Mar 19, 2026
fca1d9a
Correct phrasing in changeset description
nazabucciarelli Mar 19, 2026
0b3395c
add forgotten CI new env variable
nazabucciarelli Mar 19, 2026
e74d5af
Merge branch 'fix/livechat-offline-agent-assignment' of github.com:Ro…
nazabucciarelli Mar 19, 2026
6d760f1
safe ws closure
nazabucciarelli Mar 19, 2026
7199cc4
enhance overall WS connection logic
nazabucciarelli Mar 20, 2026
d808a27
add onError handler for ws
nazabucciarelli Mar 20, 2026
6f6597e
Merge branch 'develop' of github.com:RocketChat/Rocket.Chat into fix/…
nazabucciarelli Apr 7, 2026
b19afad
modify changeset from patch to minor
nazabucciarelli Apr 8, 2026
06820bc
remove sendAction parameter in favor of stringifiedJsonPayload
nazabucciarelli Apr 8, 2026
c75d663
add comment to document auto-assignment bussines rule
nazabucciarelli Apr 8, 2026
402aad3
change websocket variables naming
nazabucciarelli Apr 8, 2026
f4c0f10
Update .changeset/purple-boxes-shout.md
KevLehman Apr 13, 2026
d03991e
Update .changeset/purple-boxes-shout.md
KevLehman Apr 13, 2026
928583f
remove ddpLogin from livechat test
nazabucciarelli Apr 14, 2026
8bfbc05
Merge branch 'fix/livechat-offline-agent-assignment' of github.com:Ro…
nazabucciarelli Apr 14, 2026
a4845d9
update e2e tests to not use ddpLogin
nazabucciarelli Apr 14, 2026
b047bc0
implement omnichannel room forward e2e tests
nazabucciarelli Apr 14, 2026
b35f583
remove converted api tests and ws logic from 00-rooms.ts
nazabucciarelli Apr 14, 2026
99144ed
add e2e test for livechat_enabled_when_agent_idle setting
nazabucciarelli Apr 14, 2026
17ffb08
remove converted tests from 24-routing.ts
nazabucciarelli Apr 14, 2026
8dae157
rollback ws logic
nazabucciarelli Apr 15, 2026
9303454
remove ci-test-e2e.yml added env variable
nazabucciarelli Apr 15, 2026
489e27f
remove .only
nazabucciarelli Apr 15, 2026
484e3c2
fix livechat test by adding agent browser context
nazabucciarelli Apr 15, 2026
5aec5db
improve tests by using element waiter instead of hardcoded sleep and …
nazabucciarelli Apr 15, 2026
851a449
move setting update to the end of needed test cases
nazabucciarelli Apr 15, 2026
e7898f2
use long ifs
nazabucciarelli Apr 15, 2026
1de82a7
use toBeOK instead of toBe for 200 status assertion
nazabucciarelli Apr 15, 2026
f332eaf
Merge branch 'develop' into fix/livechat-offline-agent-assignment
nazabucciarelli Apr 15, 2026
e17527e
add toBeOK on all api calls, organize the code better
nazabucciarelli Apr 16, 2026
59c0a05
Merge branch 'fix/livechat-offline-agent-assignment' of github.com:Ro…
nazabucciarelli Apr 16, 2026
d0fb398
move turn agent away logic to beforeEach instead of having it as a step
nazabucciarelli Apr 16, 2026
521487c
remove unneeded resolves, use createConversation
nazabucciarelli Apr 16, 2026
68f13c0
move createConversation logic to separate beforeEach
nazabucciarelli Apr 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/purple-boxes-shout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@rocket.chat/models': minor
'@rocket.chat/meteor': minor
---

Updates the behavior of the `Livechat_enabled_when_agent_idle` setting. When enabled, the routing query now excludes `offline` agents, ensuring no new conversations are assigned to them.
Comment thread
KevLehman marked this conversation as resolved.
Resolved
1 change: 1 addition & 0 deletions .github/workflows/ci-test-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ env:
TOOL_NODE_FLAGS: ${{ vars.TOOL_NODE_FLAGS }}
LOWERCASE_REPOSITORY: ${{ inputs.lowercase-repo }}
DOCKER_TAG: ${{ inputs.gh-docker-tag }}-amd64
DDP_LOGIN_PORT: 3000

jobs:
test:
Expand Down
4 changes: 3 additions & 1 deletion apps/meteor/tests/data/livechat/department.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,9 @@ export const createDepartmentWithAnAwayAgent = async ({
credentials: Credentials;
user: WithRequiredProperty<IUser, 'username'>;
};
ws: WebSocket;
}> => {
const { user, credentials } = await createAnAwayAgent();
const { user, credentials, ws } = await createAnAwayAgent();

const department = (await createDepartment({
allowReceiveForwardOffline,
Expand All @@ -192,6 +193,7 @@ export const createDepartmentWithAnAwayAgent = async ({
credentials,
user,
},
ws,
};
};

Expand Down
7 changes: 5 additions & 2 deletions apps/meteor/tests/data/livechat/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Random } from '@rocket.chat/random';

import { api, credentials, request } from '../api-data';
import { password } from '../user';
import { createUser, login, setUserAway, setUserStatus } from '../users.helper';
import { createUser, login, setUserAwayWS, ddpLogin, setUserStatus } from '../users.helper';
import { createAgent, makeAgentAvailable, makeAgentUnavailable } from './rooms';

export const createBotAgent = async (): Promise<{
Expand Down Expand Up @@ -93,20 +93,23 @@ export const createAnOfflineAgent = async (): Promise<{
export const createAnAwayAgent = async (): Promise<{
credentials: Credentials;
user: IUser & { username: string };
ws: WebSocket;
}> => {
const username = `user.test.${Date.now()}.away`;
const email = `${username}.offline@rocket.chat`;
const { body } = await request.post(api('users.create')).set(credentials).send({ email, name: username, username, password });
const agent = body.user;
const createdUserCredentials = await login(agent.username, password);
const ws = await ddpLogin(createdUserCredentials['X-Auth-Token']);
await createAgent(agent.username);
await makeAgentAvailable(createdUserCredentials);
await setUserStatus(createdUserCredentials, UserStatus.AWAY);
await setUserAway(createdUserCredentials);
await setUserAwayWS(ws);

return {
credentials: createdUserCredentials,
user: agent,
ws,
};
};

Expand Down
86 changes: 86 additions & 0 deletions apps/meteor/tests/data/users.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,92 @@ export const setUserAway = (overrideCredentials = credentials, config?: IRequest
});
};

const connectWS = (port: string): Promise<WebSocket> =>
new Promise((resolve, reject) => {
const ws = new WebSocket(`ws://localhost:${port}/websocket`);

ws.onopen = () => {
ws.addEventListener('message', (event: MessageEvent) => {
const data = JSON.parse(event.data);
if (data.msg === 'ping') {
ws.send(JSON.stringify({ msg: 'pong' }));
}
});
resolve(ws);
};
ws.onerror = () => reject(new Error(`WS connection failed on ${port}`));
});

const waitForDDP = (ws: WebSocket, id: string | 'handshake', stringifiedJsonPayload: string): Promise<any> => {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
cleanup();
ws.close();
reject(new Error(`Timeout waiting for DDP id: ${id}`));
}, 5000);

const cleanup = () => {
clearTimeout(timeout);
ws.removeEventListener('message', handler);
ws.removeEventListener('close', onClose);
ws.removeEventListener('error', onError);
};

const onClose = () => {
cleanup();
reject(new Error(`WS closed while waiting for id: ${id}`));
};

const onError = (error: any) => {
cleanup();
ws.close();
reject(error || new Error(`WS error during operation id: ${id}`));
};

const handler = (event: MessageEvent) => {
try {
const data = JSON.parse(event.data);
const isHandshake = id === 'handshake' && data.msg === 'connected';
const isResult = data.id === id && (data.msg === 'result' || data.msg === 'error');

if (isHandshake || isResult) {
cleanup();
if (data.error) {
ws.close();
return reject(data.error);
}
resolve(data);
}
} catch (e) {
// Ignore no JSON message
}
};

ws.addEventListener('message', handler);
ws.addEventListener('close', onClose);
ws.addEventListener('error', onError);
Comment thread
KevLehman marked this conversation as resolved.
Resolved

ws.send(stringifiedJsonPayload);
});
};

export const ddpLogin = async (resume: string): Promise<WebSocket> => {
const ws = await connectWS(process.env.DDP_LOGIN_PORT || '3000');
const loginId = `login-${Date.now()}`;

await waitForDDP(ws, 'handshake', JSON.stringify({ msg: 'connect', version: '1', support: ['1'] }));

await waitForDDP(ws, loginId, JSON.stringify({ msg: 'method', id: loginId, method: 'login', params: [{ resume }] }));

return ws;
};

export const setUserAwayWS = async (ws: WebSocket): Promise<void> => {
const id = `away-${Date.now()}`;

await waitForDDP(ws, id, JSON.stringify({ msg: 'method', method: 'UserPresence:away', params: [], id }));
};

export const setUserOnline = (overrideCredentials = credentials, config?: IRequestConfig) => {
const requestInstance = config?.request || request;
return requestInstance
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ddpLogin } from '../../data/users.helper';
import { createFakeVisitor } from '../../mocks/data';
import { IS_EE } from '../config/constants';
import { Users } from '../fixtures/userStates';
Expand Down Expand Up @@ -26,6 +27,7 @@ test.describe('OC - Tags Visibility', () => {
let tagB: Awaited<ReturnType<typeof createTag>>;
let globalTag: Awaited<ReturnType<typeof createTag>>;
let sharedTag: Awaited<ReturnType<typeof createTag>>;
let ws: WebSocket;

test.beforeAll('Create departments', async ({ api }) => {
expect((await setSettingValueById(api, 'Omnichannel_enable_department_removal', true)).status()).toBe(200);
Expand All @@ -35,6 +37,7 @@ test.describe('OC - Tags Visibility', () => {

test.beforeAll('Create agent', async ({ api }) => {
agent = await createAgent(api, 'user1');
ws = await ddpLogin(Users.user1.data.loginToken);
});

test.beforeAll('Add agents to departments', async ({ api }) => {
Expand Down Expand Up @@ -71,6 +74,7 @@ test.describe('OC - Tags Visibility', () => {
await agent.delete();
await departmentA.delete();
await departmentB.delete();
ws.close();
expect((await setSettingValueById(api, 'Omnichannel_enable_department_removal', false)).status()).toBe(200);
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { faker } from '@faker-js/faker';

import { ddpLogin } from '../../data/users.helper';
import { createFakeVisitor } from '../../mocks/data';
import { Users } from '../fixtures/userStates';
import { HomeOmnichannel } from '../page-objects';
Expand Down Expand Up @@ -30,9 +31,14 @@ test.describe.serial('OC - Custom fields usage, scope : room and visitor', () =>
let conversation: Awaited<ReturnType<typeof createConversation>>;
let roomCustomField: Awaited<ReturnType<typeof createCustomField>>;
let visitorCustomField: Awaited<ReturnType<typeof createCustomField>>;
let ws: WebSocket;

test.beforeAll('Set up agent, manager and custom fields', async ({ api }) => {
[agent, manager] = await Promise.all([createAgent(api, 'user1'), createManager(api, 'user1')]);
[agent, manager, ws] = await Promise.all([
createAgent(api, 'user1'),
createManager(api, 'user1'),
ddpLogin(Users.user1.data.loginToken),
Comment thread
nazabucciarelli marked this conversation as resolved.
Resolved
]);

[roomCustomField, visitorCustomField, conversation] = await Promise.all([
createCustomField(api, {
Expand Down Expand Up @@ -67,6 +73,7 @@ test.describe.serial('OC - Custom fields usage, scope : room and visitor', () =>

test.afterAll('Remove agent, manager, custom fields and conversation', async () => {
await Promise.all([agent.delete(), manager.delete(), roomCustomField.delete(), visitorCustomField.delete(), conversation.delete()]);
ws?.close();
});

test('Should be allowed to set room custom field for a conversation', async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Page } from 'playwright-core';

import { ddpLogin } from '../../data/users.helper';
import { createFakeVisitor } from '../../mocks/data';
import { createAuxContext } from '../fixtures/createAuxContext';
import { Users } from '../fixtures/userStates';
Expand Down Expand Up @@ -264,9 +265,11 @@ test.describe('OC - Livechat - Resume chat after closing', () => {
test.describe('OC - Livechat - Close chat using widget', () => {
let poLiveChat: OmnichannelLiveChat;
let agent: Awaited<ReturnType<typeof createAgent>>;
let ws: WebSocket;

test.beforeAll(async ({ api }) => {
agent = await createAgent(api, 'user1');
ws = await ddpLogin(Users.user1.data.loginToken);
});

test.beforeEach(async ({ page, api }) => {
Expand All @@ -277,6 +280,7 @@ test.describe('OC - Livechat - Close chat using widget', () => {

test.afterAll(async () => {
await agent.delete();
ws.close();
});

test('OC - Livechat - Close Chat', async () => {
Expand Down
28 changes: 22 additions & 6 deletions apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ describe('LIVECHAT - rooms', () => {
let visitor: ILivechatVisitor;
let room: IOmnichannelRoom;
let appId: string;
const sockets: WebSocket[] = [];

before((done) => getCredentials(done));

Expand Down Expand Up @@ -129,6 +130,11 @@ describe('LIVECHAT - rooms', () => {
.set(credentials)
.expect(200);
}
for (const ws of sockets) {
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
ws.close();
}
}
});

describe('livechat/room', () => {
Expand Down Expand Up @@ -1547,9 +1553,10 @@ describe('LIVECHAT - rooms', () => {
await updateSetting('Livechat_Routing_Method', 'Manual_Selection');
await updateSetting('Livechat_enabled_when_agent_idle', false);
const { department: initialDepartment } = await createDepartmentWithAnOnlineAgent();
const { department: forwardToOfflineDepartment } = await createDepartmentWithAnAwayAgent({
const { department: forwardToOfflineDepartment, ws: awayAgentWebSocket } = await createDepartmentWithAnAwayAgent({
allowReceiveForwardOffline: true,
});
sockets.push(awayAgentWebSocket);

const newVisitor = await createVisitor(initialDepartment._id);
const newRoom = await createLivechatRoom(newVisitor.token);
Expand Down Expand Up @@ -1675,7 +1682,10 @@ describe('LIVECHAT - rooms', () => {
async () => {
await updateSetting('Livechat_Routing_Method', 'Auto_Selection');
const { department: initialDepartment } = await createDepartmentWithAnOnlineAgent();
const { department: forwardToOfflineDepartment } = await createDepartmentWithAnAwayAgent({ allowReceiveForwardOffline: true });
const { department: forwardToOfflineDepartment, ws: awayAgentWebSocket } = await createDepartmentWithAnAwayAgent({
allowReceiveForwardOffline: true,
});
sockets.push(awayAgentWebSocket);

const newVisitor = await createVisitor(initialDepartment._id);
const newRoom = await createLivechatRoom(newVisitor.token);
Expand Down Expand Up @@ -1714,7 +1724,10 @@ describe('LIVECHAT - rooms', () => {
async () => {
await updateSetting('Livechat_Routing_Method', 'Auto_Selection');
const { department: initialDepartment } = await createDepartmentWithAnOnlineAgent();
const { department: forwardToOfflineDepartment } = await createDepartmentWithAnAwayAgent({ allowReceiveForwardOffline: false });
const { department: forwardToOfflineDepartment, ws: awayAgentWebSocket } = await createDepartmentWithAnAwayAgent({
allowReceiveForwardOffline: false,
});
sockets.push(awayAgentWebSocket);

const newVisitor = await createVisitor(initialDepartment._id);
const newRoom = await createLivechatRoom(newVisitor.token);
Expand Down Expand Up @@ -1791,10 +1804,14 @@ describe('LIVECHAT - rooms', () => {
await updateSetting('Livechat_Routing_Method', 'Auto_Selection');
await updateSetting('Livechat_enabled_when_agent_idle', true);
const { department: initialDepartment } = await createDepartmentWithAnOnlineAgent();
const { department: forwardToOfflineDepartment, agent } = await createDepartmentWithAnAwayAgent({
const {
department: forwardToOfflineDepartment,
agent,
ws: awayAgentWebSocket,
} = await createDepartmentWithAnAwayAgent({
allowReceiveForwardOffline: true,
});

sockets.push(awayAgentWebSocket);
const newVisitor = await createVisitor(initialDepartment._id);
const newRoom = await createLivechatRoom(newVisitor.token);

Expand All @@ -1810,7 +1827,6 @@ describe('LIVECHAT - rooms', () => {
});

const roomInfo = await getLivechatRoomInfo(newRoom._id);

expect(roomInfo.servedBy).to.have.property('_id', agent.user._id);
expect(roomInfo.departmentId).to.be.equal(forwardToOfflineDepartment._id);

Expand Down
Loading
Loading