diff --git a/.changeset/fix-slash-command-multiline-truncation.md b/.changeset/fix-slash-command-multiline-truncation.md new file mode 100644 index 0000000000000..e5f47ad0b5805 --- /dev/null +++ b/.changeset/fix-slash-command-multiline-truncation.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes slash command params being silently truncated when the message contains a newline (Shift+Enter). The client-side regex in `processSlashCommand.ts` now uses the dotall (`s`) flag so multi-line params are passed to the command handler in full, matching the existing behaviour of the server-side parser. diff --git a/apps/meteor/client/lib/chats/flows/processSlashCommand.spec.ts b/apps/meteor/client/lib/chats/flows/processSlashCommand.spec.ts new file mode 100644 index 0000000000000..250ca4692c46a --- /dev/null +++ b/apps/meteor/client/lib/chats/flows/processSlashCommand.spec.ts @@ -0,0 +1,96 @@ +import { processSlashCommand } from './processSlashCommand'; +import { createFakeMessage } from '../../../../tests/mocks/data'; +import type { ChatAPI } from '../ChatAPI'; +import { slashCommands } from '../../../../app/utils/client'; +import { sdk } from '../../../../app/utils/client/lib/SDKClient'; + +jest.mock('../../../../app/authorization/client', () => ({ + hasAtLeastOnePermission: jest.fn(() => true), +})); + +jest.mock('../../../../app/utils/client', () => ({ + slashCommands: { + commands: {}, + }, +})); + +jest.mock('../../../../app/utils/client/lib/SDKClient', () => ({ + sdk: { + rest: { + post: jest.fn().mockResolvedValue({ result: {} }), + }, + }, +})); + +jest.mock('../../../../app/utils/lib/i18n', () => ({ + t: jest.fn((key) => key), +})); + +jest.mock('../../settings', () => ({ + settings: { + peek: jest.fn(() => true), + }, +})); + +const mockChat = { + uid: 'user-123', + composer: { + clear: jest.fn(), + }, + ActionManager: { + generateTriggerId: jest.fn(() => 'trigger-123'), + notifyBusy: jest.fn(), + notifyIdle: jest.fn(), + }, + data: { + pushEphemeralMessage: jest.fn(), + }, +} as unknown as ChatAPI; + +const message = createFakeMessage({ + _id: 'message-id', + rid: 'room-id', +}); + +describe('processSlashCommand', () => { + beforeEach(() => { + jest.clearAllMocks(); + slashCommands.commands['test'] = { + command: 'test', + clientOnly: false, + } as any; + }); + + it('should parse single-line parameters correctly', async () => { + await processSlashCommand(mockChat, { ...message, msg: '/test param1 param2' }); + + expect(sdk.rest.post).toHaveBeenCalledWith('/v1/commands.run', { + command: 'test', + params: ' param1 param2', + roomId: message.rid, + triggerId: 'trigger-123', + }); + }); + + it('should parse parameters spanning multiple lines correctly (dotall support)', async () => { + await processSlashCommand(mockChat, { ...message, msg: '/test param1\nparam2' }); + + expect(sdk.rest.post).toHaveBeenCalledWith('/v1/commands.run', { + command: 'test', + params: ' param1\nparam2', + roomId: message.rid, + triggerId: 'trigger-123', + }); + }); + + it('should parse quoted parameters spanning multiple lines correctly', async () => { + await processSlashCommand(mockChat, { ...message, msg: '/test "quoted\nparam"' }); + + expect(sdk.rest.post).toHaveBeenCalledWith('/v1/commands.run', { + command: 'test', + params: ' "quoted\nparam"', + roomId: message.rid, + triggerId: 'trigger-123', + }); + }); +}); diff --git a/apps/meteor/client/lib/chats/flows/processSlashCommand.ts b/apps/meteor/client/lib/chats/flows/processSlashCommand.ts index bfd6a87ba3736..331604213c4ee 100644 --- a/apps/meteor/client/lib/chats/flows/processSlashCommand.ts +++ b/apps/meteor/client/lib/chats/flows/processSlashCommand.ts @@ -10,7 +10,7 @@ import { settings } from '../../settings'; import type { ChatAPI } from '../ChatAPI'; const parse = (msg: string): { command: string; params: string } | { command: SlashCommand; params: string } | undefined => { - const match = msg.match(/^\/([^\s]+)(.*)/); + const match = msg.match(/^\/([^\s]+)(.*)/s); if (!match) { return undefined;