diff --git a/app/controllers/api/v1/accounts/agent_bots_controller.rb b/app/controllers/api/v1/accounts/agent_bots_controller.rb index 43bce17bc..1422beea1 100644 --- a/app/controllers/api/v1/accounts/agent_bots_controller.rb +++ b/app/controllers/api/v1/accounts/agent_bots_controller.rb @@ -37,7 +37,7 @@ class Api::V1::Accounts::AgentBotsController < Api::V1::Accounts::BaseController end def permitted_params - params.permit(:name, :description, :outgoing_url, :avatar, :avatar_url, :bot_type, bot_config: [:csml_content]) + params.permit(:name, :description, :outgoing_url, :avatar, :avatar_url, :bot_type, bot_config: {}) end def process_avatar_from_url diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 0f915ab8a..d91d85b0d 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -36,7 +36,7 @@ class DashboardController < ActionController::Base 'LOGOUT_REDIRECT_LINK', 'DISABLE_USER_PROFILE_UPDATE', 'DEPLOYMENT_ENV', - 'CSML_EDITOR_HOST', 'INSTALLATION_PRICING_PLAN' + 'INSTALLATION_PRICING_PLAN' ).merge(app_config) end diff --git a/app/javascript/dashboard/api/agentBots.js b/app/javascript/dashboard/api/agentBots.js index 4de6fcee0..6e59f38d3 100644 --- a/app/javascript/dashboard/api/agentBots.js +++ b/app/javascript/dashboard/api/agentBots.js @@ -1,9 +1,26 @@ +/* global axios */ import ApiClient from './ApiClient'; class AgentBotsAPI extends ApiClient { constructor() { super('agent_bots', { accountScoped: true }); } + + create(data) { + return axios.post(this.url, data, { + headers: { 'Content-Type': 'multipart/form-data' }, + }); + } + + update(id, data) { + return axios.patch(`${this.url}/${id}`, data, { + headers: { 'Content-Type': 'multipart/form-data' }, + }); + } + + deleteAgentBotAvatar(botId) { + return axios.delete(`${this.url}/${botId}/avatar`); + } } export default new AgentBotsAPI(); diff --git a/app/javascript/dashboard/components-next/dialog/Dialog.vue b/app/javascript/dashboard/components-next/dialog/Dialog.vue index 42325b0c5..6800b1254 100644 --- a/app/javascript/dashboard/components-next/dialog/Dialog.vue +++ b/app/javascript/dashboard/components-next/dialog/Dialog.vue @@ -125,7 +125,10 @@ defineExpose({ open, close }); - + ({ meta: { permissions: ['administrator'], }, - globalConfigFlag: 'csmlEditorHost', toState: frontendURL(`accounts/${accountId}/settings/agent-bots`), toStateName: 'agent_bots', featureFlag: FEATURE_FLAGS.AGENT_BOTS, diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue b/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue index 6ca2ef8f8..6bee69a41 100644 --- a/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue +++ b/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue @@ -49,13 +49,6 @@ export default { return !!this.menuItem.children; }, isMenuItemVisible() { - if (this.menuItem.globalConfigFlag) { - // this checks for the `csmlEditorHost` flag in the global config - // if this is present, we toggle the CSML editor menu item - // TODO: This is very specific, and can be handled better, fix it - return !!this.globalConfig[this.menuItem.globalConfigFlag]; - } - let isFeatureEnabled = true; if (this.menuItem.featureFlag) { isFeatureEnabled = this.isFeatureEnabledonAccount( diff --git a/app/javascript/dashboard/i18n/locale/en/agentBots.json b/app/javascript/dashboard/i18n/locale/en/agentBots.json index 41b8fcb1b..128cd1564 100644 --- a/app/javascript/dashboard/i18n/locale/en/agentBots.json +++ b/app/javascript/dashboard/i18n/locale/en/agentBots.json @@ -2,23 +2,13 @@ "AGENT_BOTS": { "HEADER": "Bots", "LOADING_EDITOR": "Loading editor...", - "DESCRIPTION": "Agent Bots are like the most fabulous members of your team. They can handle the small stuff, so you can focus on the stuff that matters. Give them a try.You can manage your bots from this page or create new ones using the 'Configure new bot' button.", + "DESCRIPTION": "Agent Bots are like the most fabulous members of your team. They can handle the small stuff, so you can focus on the stuff that matters. Give them a try. You can manage your bots from this page or create new ones using the 'Add Bot' button.", "LEARN_MORE": "Learn about agent bots", - "CSML_BOT_EDITOR": { - "NAME": { - "LABEL": "Bot name", - "PLACEHOLDER": "Name your bot.", - "ERROR": "Bot name is required." - }, - "DESCRIPTION": { - "LABEL": "Bot description", - "PLACEHOLDER": "What does this bot do?" - }, - "BOT_CONFIG": { - "ERROR": "Please enter your CSML bot configuration above.", - "API_ERROR": "Your CSML configuration is invalid. Please fix it and try again." - }, - "SUBMIT": "Validate and save" + "GLOBAL_BOT": "System bot", + "GLOBAL_BOT_BADGE": "System", + "AVATAR": { + "SUCCESS_DELETE": "Bot avatar deleted successfully", + "ERROR_DELETE": "Error deleting bot avatar, please try again" }, "BOT_CONFIGURATION": { "TITLE": "Select an agent bot", @@ -32,7 +22,7 @@ "SELECT_PLACEHOLDER": "Select bot" }, "ADD": { - "TITLE": "Configure new bot", + "TITLE": "Add Bot", "CANCEL_BUTTON_TEXT": "Cancel", "API": { "SUCCESS_MESSAGE": "Bot added successfully.", @@ -40,16 +30,22 @@ } }, "LIST": { - "404": "No bots found. You can create a bot by clicking the 'Configure new bot' button ↗", + "404": "No bots found. You can create a bot by clicking the 'Add Bot' button.", "LOADING": "Fetching bots...", - "TYPE": "Bot type" + "TABLE_HEADER": { + "DETAILS": "Bot Details", + "URL": "Webhook URL" + } }, "DELETE": { "BUTTON_TEXT": "Delete", "TITLE": "Delete bot", - "SUBMIT": "Delete", - "CANCEL_BUTTON_TEXT": "Cancel", - "DESCRIPTION": "Are you sure you want to delete this bot? This action is irreversible.", + "CONFIRM": { + "TITLE": "Confirm Deletion", + "MESSAGE": "Are you sure you want to delete {name}?", + "YES": "Yes, Delete", + "NO": "No, Keep" + }, "API": { "SUCCESS_MESSAGE": "Bot deleted successfully.", "ERROR_MESSAGE": "Could not delete bot. Please try again." @@ -57,17 +53,44 @@ }, "EDIT": { "BUTTON_TEXT": "Edit", - "LOADING": "Fetching bots...", "TITLE": "Edit bot", - "CANCEL_BUTTON_TEXT": "Cancel", "API": { "SUCCESS_MESSAGE": "Bot updated successfully.", "ERROR_MESSAGE": "Could not update bot. Please try again." } }, + "FORM": { + "AVATAR": { + "LABEL": "Bot avatar" + }, + "NAME": { + "LABEL": "Bot name", + "PLACEHOLDER": "Enter bot name", + "REQUIRED": "Bot name is required" + }, + "DESCRIPTION": { + "LABEL": "Description", + "PLACEHOLDER": "What does this bot do?" + }, + "WEBHOOK_URL": { + "LABEL": "Webhook URL", + "PLACEHOLDER": "https://example.com/webhook", + "REQUIRED": "Webhook URL is required" + }, + "ERRORS": { + "NAME": "Bot name is required", + "URL": "Webhook URL is required", + "VALID_URL": "Please enter a valid URL starting with http:// or https://" + }, + "CANCEL": "Cancel", + "CREATE": "Create Bot", + "UPDATE": "Update Bot" + }, + "WEBHOOK": { + "DESCRIPTION": "Configure a webhook bot to integrate with your custom services. The bot will receive and process events from conversations and can respond to them." + }, "TYPES": { - "WEBHOOK": "Webhook bot", - "CSML": "CSML bot" + "WEBHOOK": "Webhook bot" } } } diff --git a/app/javascript/dashboard/routes/dashboard/settings/agentBots/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/agentBots/Index.vue index 591267c31..0d42d15cf 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/agentBots/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/agentBots/Index.vue @@ -1,102 +1,191 @@ - - - - - - - - + + + + {{ thHeader }} + + + + + + + + + + {{ bot.name }} + + {{ $t('AGENT_BOTS.GLOBAL_BOT_BADGE') }} + + + + {{ bot.description }} + + + + + + {{ bot.outgoing_url || bot.bot_config?.webhook_url }} + + + + + + + + + + + + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/agentBots/agentBot.routes.js b/app/javascript/dashboard/routes/dashboard/settings/agentBots/agentBot.routes.js index d83335d7e..d82d2e32e 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/agentBots/agentBot.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/agentBots/agentBot.routes.js @@ -1,9 +1,6 @@ import { FEATURE_FLAGS } from '../../../../featureFlags'; import Bot from './Index.vue'; -import CsmlEditBot from './csml/Edit.vue'; -import CsmlNewBot from './csml/New.vue'; import { frontendURL } from '../../../../helper/URLHelper'; -import SettingsContent from '../Wrapper.vue'; import SettingsWrapper from '../SettingsWrapper.vue'; export default { @@ -26,36 +23,5 @@ export default { }, ], }, - { - path: frontendURL('accounts/:accountId/settings/agent-bots'), - component: SettingsContent, - props: () => { - return { - headerTitle: 'AGENT_BOTS.HEADER', - icon: 'bot', - showBackButton: true, - }; - }, - children: [ - { - path: 'csml/new', - name: 'agent_bots_csml_new', - component: CsmlNewBot, - meta: { - featureFlag: FEATURE_FLAGS.AGENT_BOTS, - permissions: ['administrator'], - }, - }, - { - path: 'csml/:botId', - name: 'agent_bots_csml_edit', - component: CsmlEditBot, - meta: { - featureFlag: FEATURE_FLAGS.AGENT_BOTS, - permissions: ['administrator'], - }, - }, - ], - }, ], }; diff --git a/app/javascript/dashboard/routes/dashboard/settings/agentBots/components/AgentBotModal.vue b/app/javascript/dashboard/routes/dashboard/settings/agentBots/components/AgentBotModal.vue new file mode 100644 index 000000000..b64b1ef58 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/agentBots/components/AgentBotModal.vue @@ -0,0 +1,251 @@ + + + + + + + + {{ $t('AGENT_BOTS.FORM.AVATAR.LABEL') }} + + + + + + + + + + + + + + + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/agentBots/components/AgentBotRow.vue b/app/javascript/dashboard/routes/dashboard/settings/agentBots/components/AgentBotRow.vue deleted file mode 100644 index 365e4948f..000000000 --- a/app/javascript/dashboard/routes/dashboard/settings/agentBots/components/AgentBotRow.vue +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - {{ agentBot.name }} - () - - - - - - - - - - - - - diff --git a/app/javascript/dashboard/routes/dashboard/settings/agentBots/components/AgentBotType.vue b/app/javascript/dashboard/routes/dashboard/settings/agentBots/components/AgentBotType.vue deleted file mode 100644 index b207e8637..000000000 --- a/app/javascript/dashboard/routes/dashboard/settings/agentBots/components/AgentBotType.vue +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - {{ botTypeConfig[botType].label }} - - diff --git a/app/javascript/dashboard/routes/dashboard/settings/agentBots/components/CSMLBotEditor.vue b/app/javascript/dashboard/routes/dashboard/settings/agentBots/components/CSMLBotEditor.vue deleted file mode 100644 index 9c962136d..000000000 --- a/app/javascript/dashboard/routes/dashboard/settings/agentBots/components/CSMLBotEditor.vue +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - - - - - {{ $t('AGENT_BOTS.CSML_BOT_EDITOR.BOT_CONFIG.ERROR') }} - - - - - - - - {{ $t('AGENT_BOTS.CSML_BOT_EDITOR.NAME.LABEL') }} - - - {{ $t('AGENT_BOTS.CSML_BOT_EDITOR.NAME.ERROR') }} - - - - {{ $t('AGENT_BOTS.CSML_BOT_EDITOR.DESCRIPTION.LABEL') }} - - - - - - - - - diff --git a/app/javascript/dashboard/routes/dashboard/settings/agentBots/components/CSMLMonacoEditor.vue b/app/javascript/dashboard/routes/dashboard/settings/agentBots/components/CSMLMonacoEditor.vue deleted file mode 100644 index 0f80e0e31..000000000 --- a/app/javascript/dashboard/routes/dashboard/settings/agentBots/components/CSMLMonacoEditor.vue +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - - - - diff --git a/app/javascript/dashboard/routes/dashboard/settings/agentBots/csml/Edit.vue b/app/javascript/dashboard/routes/dashboard/settings/agentBots/csml/Edit.vue deleted file mode 100644 index 3c909fdba..000000000 --- a/app/javascript/dashboard/routes/dashboard/settings/agentBots/csml/Edit.vue +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - diff --git a/app/javascript/dashboard/routes/dashboard/settings/agentBots/csml/New.vue b/app/javascript/dashboard/routes/dashboard/settings/agentBots/csml/New.vue deleted file mode 100644 index 10868e58c..000000000 --- a/app/javascript/dashboard/routes/dashboard/settings/agentBots/csml/New.vue +++ /dev/null @@ -1,41 +0,0 @@ - - - - - diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue index 9a105a045..aa9eaf596 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue @@ -141,11 +141,7 @@ export default { } if ( - this.isFeatureEnabledonAccount( - this.accountId, - FEATURE_FLAGS.AGENT_BOTS - ) && - !(this.isAnEmailChannel || this.isATwitterInbox) + this.isFeatureEnabledonAccount(this.accountId, FEATURE_FLAGS.AGENT_BOTS) ) { visibleToAllChannelTabs = [ ...visibleToAllChannelTabs, diff --git a/app/javascript/dashboard/store/modules/agentBots.js b/app/javascript/dashboard/store/modules/agentBots.js index 57ecf9912..3e9931057 100644 --- a/app/javascript/dashboard/store/modules/agentBots.js +++ b/app/javascript/dashboard/store/modules/agentBots.js @@ -12,6 +12,7 @@ export const state = { isCreating: false, isDeleting: false, isUpdating: false, + isUpdatingAvatar: false, isFetchingAgentBot: false, isSettingAgentBot: false, isDisconnecting: false, @@ -48,10 +49,23 @@ export const actions = { commit(types.SET_AGENT_BOT_UI_FLAG, { isFetching: false }); } }, - create: async ({ commit }, agentBotObj) => { + + create: async ({ commit }, botData) => { commit(types.SET_AGENT_BOT_UI_FLAG, { isCreating: true }); try { - const response = await AgentBotsAPI.create(agentBotObj); + // Create FormData for file upload + const formData = new FormData(); + formData.append('name', botData.name); + formData.append('description', botData.description); + formData.append('bot_type', botData.bot_type || 'webhook'); + formData.append('outgoing_url', botData.outgoing_url); + + // Add avatar file if available + if (botData.avatar) { + formData.append('avatar', botData.avatar); + } + + const response = await AgentBotsAPI.create(formData); commit(types.ADD_AGENT_BOT, response.data); return response.data; } catch (error) { @@ -61,10 +75,22 @@ export const actions = { } return null; }, - update: async ({ commit }, { id, ...agentBotObj }) => { + + update: async ({ commit }, { id, data }) => { commit(types.SET_AGENT_BOT_UI_FLAG, { isUpdating: true }); try { - const response = await AgentBotsAPI.update(id, agentBotObj); + // Create FormData for file upload + const formData = new FormData(); + formData.append('name', data.name); + formData.append('description', data.description); + formData.append('bot_type', data.bot_type || 'webhook'); + formData.append('outgoing_url', data.outgoing_url); + + if (data.avatar) { + formData.append('avatar', data.avatar); + } + + const response = await AgentBotsAPI.update(id, formData); commit(types.EDIT_AGENT_BOT, response.data); } catch (error) { throwErrorMessage(error); @@ -72,6 +98,7 @@ export const actions = { commit(types.SET_AGENT_BOT_UI_FLAG, { isUpdating: false }); } }, + delete: async ({ commit }, id) => { commit(types.SET_AGENT_BOT_UI_FLAG, { isDeleting: true }); try { @@ -83,6 +110,20 @@ export const actions = { commit(types.SET_AGENT_BOT_UI_FLAG, { isDeleting: false }); } }, + + deleteAgentBotAvatar: async ({ commit }, id) => { + commit(types.SET_AGENT_BOT_UI_FLAG, { isUpdatingAvatar: true }); + try { + await AgentBotsAPI.deleteAgentBotAvatar(id); + // Update the thumbnail to empty string after deletion + commit(types.UPDATE_AGENT_BOT_AVATAR, { id, thumbnail: '' }); + } catch (error) { + throwErrorMessage(error); + } finally { + commit(types.SET_AGENT_BOT_UI_FLAG, { isUpdatingAvatar: false }); + } + }, + show: async ({ commit }, id) => { commit(types.SET_AGENT_BOT_UI_FLAG, { isFetchingItem: true }); try { @@ -150,6 +191,12 @@ export const mutations = { [inboxId]: agentBotId, }; }, + [types.UPDATE_AGENT_BOT_AVATAR]($state, { id, thumbnail }) { + const botIndex = $state.records.findIndex(bot => bot.id === id); + if (botIndex !== -1) { + $state.records[botIndex].thumbnail = thumbnail || ''; + } + }, }; export default { diff --git a/app/javascript/dashboard/store/modules/specs/agentBots/agentBots.spec.js b/app/javascript/dashboard/store/modules/specs/agentBots/agentBots.spec.js index 90cae45d9..b2fa47313 100644 --- a/app/javascript/dashboard/store/modules/specs/agentBots/agentBots.spec.js +++ b/app/javascript/dashboard/store/modules/specs/agentBots/agentBots.spec.js @@ -1,7 +1,7 @@ import axios from 'axios'; import { actions } from '../../agentBots'; import types from '../../../mutation-types'; -import { agentBotRecords } from './fixtures'; +import { agentBotRecords, agentBotData } from './fixtures'; const commit = vi.fn(); global.axios = axios; @@ -30,16 +30,22 @@ describe('#actions', () => { describe('#create', () => { it('sends correct actions if API is success', async () => { axios.post.mockResolvedValue({ data: agentBotRecords[0] }); - await actions.create({ commit }, agentBotRecords[0]); + await actions.create({ commit }, agentBotData); + expect(commit.mock.calls).toEqual([ [types.SET_AGENT_BOT_UI_FLAG, { isCreating: true }], [types.ADD_AGENT_BOT, agentBotRecords[0]], [types.SET_AGENT_BOT_UI_FLAG, { isCreating: false }], ]); + + expect(axios.post.mock.calls.length).toBe(1); + const formDataArg = axios.post.mock.calls[0][1]; + expect(formDataArg instanceof FormData).toBe(true); }); + it('sends correct actions if API is error', async () => { axios.post.mockRejectedValue({ message: 'Incorrect header' }); - await expect(actions.create({ commit })).rejects.toThrow(Error); + await expect(actions.create({ commit }, {})).rejects.toThrow(Error); expect(commit.mock.calls).toEqual([ [types.SET_AGENT_BOT_UI_FLAG, { isCreating: true }], [types.SET_AGENT_BOT_UI_FLAG, { isCreating: false }], @@ -50,17 +56,29 @@ describe('#actions', () => { describe('#update', () => { it('sends correct actions if API is success', async () => { axios.patch.mockResolvedValue({ data: agentBotRecords[0] }); - await actions.update({ commit }, agentBotRecords[0]); + await actions.update( + { commit }, + { + id: agentBotRecords[0].id, + data: agentBotData, + } + ); + expect(commit.mock.calls).toEqual([ [types.SET_AGENT_BOT_UI_FLAG, { isUpdating: true }], [types.EDIT_AGENT_BOT, agentBotRecords[0]], [types.SET_AGENT_BOT_UI_FLAG, { isUpdating: false }], ]); + + expect(axios.patch.mock.calls.length).toBe(1); + const formDataArg = axios.patch.mock.calls[0][1]; + expect(formDataArg instanceof FormData).toBe(true); }); + it('sends correct actions if API is error', async () => { axios.patch.mockRejectedValue({ message: 'Incorrect header' }); await expect( - actions.update({ commit }, agentBotRecords[0]) + actions.update({ commit }, { id: 1, data: {} }) ).rejects.toThrow(Error); expect(commit.mock.calls).toEqual([ [types.SET_AGENT_BOT_UI_FLAG, { isUpdating: true }], @@ -68,7 +86,6 @@ describe('#actions', () => { ]); }); }); - describe('#delete', () => { it('sends correct actions if API is success', async () => { axios.delete.mockResolvedValue({ data: agentBotRecords[0] }); diff --git a/app/javascript/dashboard/store/modules/specs/agentBots/fixtures.js b/app/javascript/dashboard/store/modules/specs/agentBots/fixtures.js index e13735b14..d41585904 100644 --- a/app/javascript/dashboard/store/modules/specs/agentBots/fixtures.js +++ b/app/javascript/dashboard/store/modules/specs/agentBots/fixtures.js @@ -1,15 +1,35 @@ export const agentBotRecords = [ { + account_id: 1, id: 11, name: 'Agent Bot 11', description: 'Agent Bot Description', - type: 'csml', + bot_type: 'webhook', + thumbnail: 'https://example.com/thumbnail.jpg', + bot_config: {}, + outgoing_url: 'https://example.com/outgoing', + access_token: 'hN8QwG769RqBXmme', + system_bot: false, }, { + account_id: 1, id: 12, name: 'Agent Bot 12', description: 'Agent Bot Description 12', - type: 'csml', + bot_type: 'webhook', + thumbnail: 'https://example.com/thumbnail.jpg', + bot_config: {}, + outgoing_url: 'https://example.com/outgoing', + access_token: 'hN8QwG769RqBXmme', + system_bot: false, }, ]; + +export const agentBotData = { + name: 'Test Bot', + description: 'Test Description', + outgoing_url: 'https://test.com', + bot_type: 'webhook', + avatar: new File([''], 'filename'), +}; diff --git a/app/javascript/dashboard/store/modules/specs/agentBots/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/agentBots/mutations.spec.js index aff191ecf..6a8d36a75 100644 --- a/app/javascript/dashboard/store/modules/specs/agentBots/mutations.spec.js +++ b/app/javascript/dashboard/store/modules/specs/agentBots/mutations.spec.js @@ -51,4 +51,16 @@ describe('#mutations', () => { expect(state.agentBotInbox).toEqual({ 3: 2 }); }); }); + describe('#UPDATE_AGENT_BOT_AVATAR', () => { + it('update agent bot avatar', () => { + const state = { records: [agentBotRecords[0]] }; + mutations[types.UPDATE_AGENT_BOT_AVATAR](state, { + id: 11, + thumbnail: 'https://example.com/thumbnail.jpg', + }); + expect(state.records[0].thumbnail).toEqual( + 'https://example.com/thumbnail.jpg' + ); + }); + }); }); diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js index 4443f0732..a74207e92 100644 --- a/app/javascript/dashboard/store/mutation-types.js +++ b/app/javascript/dashboard/store/mutation-types.js @@ -301,6 +301,7 @@ export default { EDIT_AGENT_BOT: 'EDIT_AGENT_BOT', DELETE_AGENT_BOT: 'DELETE_AGENT_BOT', SET_AGENT_BOT_INBOX: 'SET_AGENT_BOT_INBOX', + UPDATE_AGENT_BOT_AVATAR: 'UPDATE_AGENT_BOT_AVATAR', // MACROS SET_MACROS_UI_FLAG: 'SET_MACROS_UI_FLAG', diff --git a/app/javascript/shared/store/globalConfig.js b/app/javascript/shared/store/globalConfig.js index 5a266bd21..608a31ec1 100644 --- a/app/javascript/shared/store/globalConfig.js +++ b/app/javascript/shared/store/globalConfig.js @@ -5,7 +5,6 @@ const { AZURE_APP_ID: azureAppId, BRAND_NAME: brandName, CHATWOOT_INBOX_TOKEN: chatwootInboxToken, - CSML_EDITOR_HOST: csmlEditorHost, CREATE_NEW_ACCOUNT_FROM_DASHBOARD: createNewAccountFromDashboard, DIRECT_UPLOADS_ENABLED: directUploadsEnabled, DISPLAY_MANIFEST: displayManifest, @@ -29,7 +28,6 @@ const state = { azureAppId, brandName, chatwootInboxToken, - csmlEditorHost, deploymentEnv, createNewAccountFromDashboard, directUploadsEnabled: directUploadsEnabled === 'true', diff --git a/app/jobs/agent_bots/csml_job.rb b/app/jobs/agent_bots/csml_job.rb deleted file mode 100644 index d0975e364..000000000 --- a/app/jobs/agent_bots/csml_job.rb +++ /dev/null @@ -1,10 +0,0 @@ -class AgentBots::CsmlJob < ApplicationJob - queue_as :high - - def perform(event, agent_bot, message) - event_data = { message: message } - Integrations::Csml::ProcessorService.new( - event_name: event, agent_bot: agent_bot, event_data: event_data - ).perform - end -end diff --git a/app/listeners/agent_bot_listener.rb b/app/listeners/agent_bot_listener.rb index 12e8c6570..38c046e34 100644 --- a/app/listeners/agent_bot_listener.rb +++ b/app/listeners/agent_bot_listener.rb @@ -59,14 +59,10 @@ class AgentBotListener < BaseListener true end - def process_message_event(method_name, agent_bot, message, event) - case agent_bot.bot_type - when 'webhook' - payload = message.webhook_data.merge(event: method_name) - process_webhook_bot_event(agent_bot, payload) - when 'csml' - process_csml_bot_event(event.name, agent_bot, message) - end + def process_message_event(method_name, agent_bot, message, _event) + # Only webhook bots are supported + payload = message.webhook_data.merge(event: method_name) + process_webhook_bot_event(agent_bot, payload) end def process_webhook_bot_event(agent_bot, payload) @@ -74,8 +70,4 @@ class AgentBotListener < BaseListener AgentBots::WebhookJob.perform_later(agent_bot.outgoing_url, payload) end - - def process_csml_bot_event(event, agent_bot, message) - AgentBots::CsmlJob.perform_later(event, agent_bot, message) - end end diff --git a/app/models/agent_bot.rb b/app/models/agent_bot.rb index 07119fbca..be63aebc5 100644 --- a/app/models/agent_bot.rb +++ b/app/models/agent_bot.rb @@ -25,9 +25,8 @@ class AgentBot < ApplicationRecord has_many :inboxes, through: :agent_bot_inboxes has_many :messages, as: :sender, dependent: :nullify belongs_to :account, optional: true - enum bot_type: { webhook: 0, csml: 1 } + enum bot_type: { webhook: 0 } - validate :validate_agent_bot_config validates :outgoing_url, length: { maximum: Limits::URL_LENGTH_LIMIT } def available_name @@ -51,9 +50,7 @@ class AgentBot < ApplicationRecord } end - private - - def validate_agent_bot_config - errors.add(:bot_config, 'Invalid Bot Configuration') unless AgentBots::ValidateBotService.new(agent_bot: self).perform + def system_bot? + account.nil? end end diff --git a/app/services/agent_bots/validate_bot_service.rb b/app/services/agent_bots/validate_bot_service.rb deleted file mode 100644 index d8c0b8a9c..000000000 --- a/app/services/agent_bots/validate_bot_service.rb +++ /dev/null @@ -1,38 +0,0 @@ -class AgentBots::ValidateBotService - pattr_initialize [:agent_bot] - def perform - return true unless agent_bot.bot_type == 'csml' - - validate_csml_bot - end - - private - - def csml_client - @csml_client ||= CsmlEngine.new - end - - def csml_bot_payload - { - id: agent_bot[:name], - name: agent_bot[:name], - default_flow: 'Default', - flows: [ - { - id: SecureRandom.uuid, - name: 'Default', - content: agent_bot.bot_config['csml_content'], - commands: [] - } - ] - } - end - - def validate_csml_bot - response = csml_client.validate(csml_bot_payload) - response.blank? || response['valid'] - rescue StandardError => e - ChatwootExceptionTracker.new(e, account: agent_bot&.account).capture_exception - false - end -end diff --git a/app/views/api/v1/models/_agent_bot.json.jbuilder b/app/views/api/v1/models/_agent_bot.json.jbuilder index eafa31cd6..2547bb2a8 100644 --- a/app/views/api/v1/models/_agent_bot.json.jbuilder +++ b/app/views/api/v1/models/_agent_bot.json.jbuilder @@ -1,8 +1,10 @@ json.id resource.id json.name resource.name json.description resource.description -json.outgoing_url resource.outgoing_url +json.thumbnail resource.avatar_url +json.outgoing_url resource.outgoing_url unless resource.system_bot? json.bot_type resource.bot_type json.bot_config resource.bot_config json.account_id resource.account_id json.access_token resource.access_token if resource.access_token.present? +json.system_bot resource.system_bot? diff --git a/config/features.yml b/config/features.yml index 59b9aa2ad..3396733ab 100644 --- a/config/features.yml +++ b/config/features.yml @@ -37,7 +37,7 @@ help_url: https://chwt.app/hc/help-center - name: agent_bots display_name: Agent Bots - enabled: false + enabled: true help_url: https://chwt.app/hc/agent-bots - name: macros display_name: Macros diff --git a/db/migrate/20250410061725_convert_csml_bots_to_webhook_bots.rb b/db/migrate/20250410061725_convert_csml_bots_to_webhook_bots.rb new file mode 100644 index 000000000..f4a953965 --- /dev/null +++ b/db/migrate/20250410061725_convert_csml_bots_to_webhook_bots.rb @@ -0,0 +1,13 @@ +class ConvertCsmlBotsToWebhookBots < ActiveRecord::Migration[7.0] + def up + # Find all CSML bots (bot_type = 1) and convert them to webhook (bot_type = 0) + AgentBot.where(bot_type: 1).find_each do |bot| + bot.update(bot_type: 0, bot_config: {}) + end + end + + def down + # This migration is not reversible - we've removed CSML support + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/db/schema.rb b/db/schema.rb index e4706342e..b3c2727cb 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2025_04_02_233933) do +ActiveRecord::Schema[7.0].define(version: 2025_04_10_061725) do # These extensions should be enabled to support this database enable_extension "pg_stat_statements" enable_extension "pg_trgm" diff --git a/lib/csml_engine.rb b/lib/csml_engine.rb deleted file mode 100644 index 5e238ac2d..000000000 --- a/lib/csml_engine.rb +++ /dev/null @@ -1,52 +0,0 @@ -class CsmlEngine - API_KEY_HEADER = 'X-Api-Key'.freeze - - def initialize - @host_url = GlobalConfigService.load('CSML_BOT_HOST', '') - @api_key = GlobalConfigService.load('CSML_BOT_API_KEY', '') - - raise ArgumentError, 'Missing Credentials' if @host_url.blank? || @api_key.blank? - end - - def status - response = HTTParty.get("#{@host_url}/status") - process_response(response) - end - - def run(bot, params) - payload = { - bot: bot, - event: { - request_id: SecureRandom.uuid, - client: params[:client], - payload: params[:payload], - metadata: params[:metadata], - ttl_duration: 4000 - } - } - response = post('run', payload) - process_response(response) - end - - def validate(bot) - response = post('validate', bot) - process_response(response) - end - - private - - def process_response(response) - return response.parsed_response if response.success? - - { error: response.parsed_response, status: response.code } - end - - def post(path, payload) - HTTParty.post( - "#{@host_url}/#{path}", { - headers: { API_KEY_HEADER => @api_key, 'Content-Type' => 'application/json' }, - body: payload.to_json - } - ) - end -end diff --git a/lib/integrations/bot_processor_service.rb b/lib/integrations/bot_processor_service.rb index 600ee4305..8249a2993 100644 --- a/lib/integrations/bot_processor_service.rb +++ b/lib/integrations/bot_processor_service.rb @@ -1,5 +1,4 @@ class Integrations::BotProcessorService - # TODO: In CSML processor service, the argument is agent bot, update initializers accordingly. pattr_initialize [:event_name!, :hook!, :event_data!] def perform diff --git a/lib/integrations/csml/processor_service.rb b/lib/integrations/csml/processor_service.rb deleted file mode 100644 index 3a8f21077..000000000 --- a/lib/integrations/csml/processor_service.rb +++ /dev/null @@ -1,142 +0,0 @@ -class Integrations::Csml::ProcessorService < Integrations::BotProcessorService - pattr_initialize [:event_name!, :event_data!, :agent_bot!] - - private - - def csml_client - @csml_client ||= CsmlEngine.new - end - - def get_response(session_id, content) - csml_client.run( - bot_payload, - { - client: client_params(session_id), - payload: message_payload(content), - metadata: metadata_params - } - ) - end - - def client_params(session_id) - { - bot_id: "chatwoot-bot-#{conversation.inbox.id}", - channel_id: "chatwoot-bot-inbox-#{conversation.inbox.id}", - user_id: session_id - } - end - - def message_payload(content) - { - content_type: 'text', - content: { text: content } - } - end - - def metadata_params - { - conversation: conversation, - contact: conversation.contact - } - end - - def bot_payload - { - id: "chatwoot-csml-bot-#{agent_bot.id}", - name: "chatwoot-csml-bot-#{agent_bot.id}", - default_flow: 'chatwoot_bot_flow', - flows: [ - { - id: "chatwoot-csml-bot-flow-#{agent_bot.id}-inbox-#{conversation.inbox.id}", - name: 'chatwoot_bot_flow', - content: agent_bot.bot_config['csml_content'], - commands: [] - } - ] - } - end - - def process_response(message, response) - csml_messages = response['messages'] - has_conversation_ended = response['conversation_end'] - - process_action(message, 'handoff') if has_conversation_ended.present? - - return if csml_messages.blank? - - # We do not support wait, typing now. - csml_messages.each do |csml_message| - create_messages(csml_message, conversation) - end - end - - def create_messages(message, conversation) - message_payload = message['payload'] - - case message_payload['content_type'] - when 'text' - process_text_messages(message_payload, conversation) - when 'question' - process_question_messages(message_payload, conversation) - when 'image' - process_image_messages(message_payload, conversation) - end - end - - def process_text_messages(message_payload, conversation) - conversation.messages.create!( - { - message_type: :outgoing, - account_id: conversation.account_id, - inbox_id: conversation.inbox_id, - content: message_payload['content']['text'], - sender: agent_bot - } - ) - end - - def process_question_messages(message_payload, conversation) - buttons = message_payload['content']['buttons'].map do |button| - { title: button['content']['title'], value: button['content']['payload'] } - end - conversation.messages.create!( - { - message_type: :outgoing, - account_id: conversation.account_id, - inbox_id: conversation.inbox_id, - content: message_payload['content']['title'], - content_type: 'input_select', - content_attributes: { items: buttons }, - sender: agent_bot - } - ) - end - - def prepare_attachment(message_payload, message, account_id) - attachment_params = { file_type: :image, account_id: account_id } - attachment_url = message_payload['content']['url'] - attachment = message.attachments.new(attachment_params) - attachment_file = Down.download(attachment_url) - attachment.file.attach( - io: attachment_file, - filename: attachment_file.original_filename, - content_type: attachment_file.content_type - ) - end - - def process_image_messages(message_payload, conversation) - message = conversation.messages.new( - { - message_type: :outgoing, - account_id: conversation.account_id, - inbox_id: conversation.inbox_id, - content: '', - content_type: 'text', - sender: agent_bot - } - ) - - prepare_attachment(message_payload, message, conversation.account_id) - message.save! - end -end diff --git a/spec/controllers/api/v1/accounts/agent_bots_controller_spec.rb b/spec/controllers/api/v1/accounts/agent_bots_controller_spec.rb index 918552406..b5cdff018 100644 --- a/spec/controllers/api/v1/accounts/agent_bots_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/agent_bots_controller_spec.rb @@ -28,6 +28,31 @@ RSpec.describe 'Agent Bot API', type: :request do expect(response.body).to include(agent_bot.access_token.token) expect(response.body).not_to include(global_bot.access_token.token) end + + it 'properly differentiates between system bots and account bots' do + global_bot = create(:agent_bot) + get "/api/v1/accounts/#{account.id}/agent_bots", + headers: agent.create_new_auth_token, + as: :json + + response_data = response.parsed_body + # Find the global bot in the response + global_bot_response = response_data.find { |bot| bot['id'] == global_bot.id } + # Find the account bot in the response + account_bot_response = response_data.find { |bot| bot['id'] == agent_bot.id } + + # Verify system_bot attribute and outgoing_url for global bot + expect(global_bot_response['system_bot']).to be(true) + expect(global_bot_response).not_to include('outgoing_url') + + # Verify account bot has system_bot attribute false and includes outgoing_url + expect(account_bot_response['system_bot']).to be(false) + expect(account_bot_response).to include('outgoing_url') + + # Verify both bots have thumbnail field + expect(global_bot_response).to include('thumbnail') + expect(account_bot_response).to include('thumbnail') + end end end @@ -60,6 +85,10 @@ RSpec.describe 'Agent Bot API', type: :request do expect(response).to have_http_status(:success) expect(response.body).to include(global_bot.name) expect(response.body).not_to include(global_bot.access_token.token) + + # Test for system_bot attribute and webhook URL not being exposed + expect(response.parsed_body['system_bot']).to be(true) + expect(response.parsed_body).not_to include('outgoing_url') end end end @@ -142,7 +171,7 @@ RSpec.describe 'Agent Bot API', type: :request do expect(response.body).not_to include(global_bot.access_token.token) end - it 'updates avatar' do + it 'updates avatar and includes thumbnail in response' do # no avatar before upload expect(agent_bot.avatar.attached?).to be(false) file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png') @@ -153,6 +182,9 @@ RSpec.describe 'Agent Bot API', type: :request do expect(response).to have_http_status(:success) agent_bot.reload expect(agent_bot.avatar.attached?).to be(true) + + # Verify thumbnail is included in the response + expect(response.parsed_body).to include('thumbnail') end it 'updated avatar with avatar_url' do diff --git a/spec/jobs/agent_bots/csml_job_spec.rb b/spec/jobs/agent_bots/csml_job_spec.rb deleted file mode 100644 index 46fa23fba..000000000 --- a/spec/jobs/agent_bots/csml_job_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -require 'rails_helper' - -RSpec.describe AgentBots::CsmlJob do - it 'runs csml processor service' do - event = 'message.created' - message = create(:message) - agent_bot = create(:agent_bot) - processor = double - - allow(Integrations::Csml::ProcessorService).to receive(:new).and_return(processor) - allow(processor).to receive(:perform) - - described_class.perform_now(event, agent_bot, message) - - expect(Integrations::Csml::ProcessorService) - .to have_received(:new) - .with(event_name: event, agent_bot: agent_bot, event_data: { message: message }) - end -end diff --git a/spec/lib/csml_engine_spec.rb b/spec/lib/csml_engine_spec.rb deleted file mode 100644 index 966e4d95a..000000000 --- a/spec/lib/csml_engine_spec.rb +++ /dev/null @@ -1,99 +0,0 @@ -require 'rails_helper' - -describe CsmlEngine do - it 'raises an exception if host and api is absent' do - expect { described_class.new }.to raise_error(StandardError) - end - - context 'when CSML_BOT_HOST & CSML_BOT_API_KEY is present' do - before do - create(:installation_config, { name: 'CSML_BOT_HOST', value: 'https://csml.chatwoot.dev' }) - create(:installation_config, { name: 'CSML_BOT_API_KEY', value: 'random_api_key' }) - end - - let(:csml_request) { double } - - context 'when status is called' do - it 'returns api response if client response is valid' do - allow(HTTParty).to receive(:get).and_return(csml_request) - allow(csml_request).to receive(:success?).and_return(true) - allow(csml_request).to receive(:parsed_response).and_return({ 'engine_version': '1.11.1' }) - - response = described_class.new.status - - expect(HTTParty).to have_received(:get).with('https://csml.chatwoot.dev/status') - expect(csml_request).to have_received(:success?) - expect(csml_request).to have_received(:parsed_response) - expect(response).to eq({ 'engine_version': '1.11.1' }) - end - - it 'returns error if client response is invalid' do - allow(HTTParty).to receive(:get).and_return(csml_request) - allow(csml_request).to receive(:success?).and_return(false) - allow(csml_request).to receive(:code).and_return(401) - allow(csml_request).to receive(:parsed_response).and_return({ 'error': true }) - - response = described_class.new.status - - expect(HTTParty).to have_received(:get).with('https://csml.chatwoot.dev/status') - expect(csml_request).to have_received(:success?) - expect(response).to eq({ error: { 'error': true }, status: 401 }) - end - end - - context 'when run is called' do - it 'returns api response if client response is valid' do - allow(HTTParty).to receive(:post).and_return(csml_request) - allow(SecureRandom).to receive(:uuid).and_return('xxxx-yyyy-wwww-cccc') - allow(csml_request).to receive(:success?).and_return(true) - allow(csml_request).to receive(:parsed_response).and_return({ 'success': true }) - - response = described_class.new.run({ flow: 'default' }, { client: 'client', payload: { id: 1 }, metadata: {} }) - - payload = { - bot: { flow: 'default' }, - event: { - request_id: 'xxxx-yyyy-wwww-cccc', - client: 'client', - payload: { id: 1 }, - metadata: {}, - ttl_duration: 4000 - } - } - expect(HTTParty).to have_received(:post) - .with( - 'https://csml.chatwoot.dev/run', { - body: payload.to_json, - headers: { 'X-Api-Key' => 'random_api_key', 'Content-Type' => 'application/json' } - } - ) - expect(csml_request).to have_received(:success?) - expect(csml_request).to have_received(:parsed_response) - expect(response).to eq({ 'success': true }) - end - end - - context 'when validate is called' do - it 'returns api response if client response is valid' do - allow(HTTParty).to receive(:post).and_return(csml_request) - allow(SecureRandom).to receive(:uuid).and_return('xxxx-yyyy-wwww-cccc') - allow(csml_request).to receive(:success?).and_return(true) - allow(csml_request).to receive(:parsed_response).and_return({ 'success': true }) - - payload = { flow: 'default' } - response = described_class.new.validate(payload) - - expect(HTTParty).to have_received(:post) - .with( - 'https://csml.chatwoot.dev/validate', { - body: payload.to_json, - headers: { 'X-Api-Key' => 'random_api_key', 'Content-Type' => 'application/json' } - } - ) - expect(csml_request).to have_received(:success?) - expect(csml_request).to have_received(:parsed_response) - expect(response).to eq({ 'success': true }) - end - end - end -end diff --git a/spec/lib/integrations/csml/processor_service_spec.rb b/spec/lib/integrations/csml/processor_service_spec.rb deleted file mode 100644 index a6d213114..000000000 --- a/spec/lib/integrations/csml/processor_service_spec.rb +++ /dev/null @@ -1,108 +0,0 @@ -require 'rails_helper' - -describe Integrations::Csml::ProcessorService do - let(:account) { create(:account) } - let(:inbox) { create(:inbox, account: account) } - let(:agent_bot) { create(:agent_bot, :skip_validate, bot_type: 'csml', account: account) } - let(:agent_bot_inbox) { create(:agent_bot_inbox, agent_bot: agent_bot, inbox: inbox, account: account) } - let(:conversation) { create(:conversation, account: account, status: :pending) } - let(:message) { create(:message, account: account, conversation: conversation) } - let(:event_name) { 'message.created' } - let(:event_data) { { message: message } } - - describe '#perform' do - let(:csml_client) { double } - let(:processor) { described_class.new(event_name: event_name, agent_bot: agent_bot, event_data: event_data) } - - before do - allow(CsmlEngine).to receive(:new).and_return(csml_client) - end - - context 'when a conversation is completed from CSML' do - it 'open the conversation and handsoff it to an agent' do - csml_response = ActiveSupport::HashWithIndifferentAccess.new(conversation_end: true) - allow(csml_client).to receive(:run).and_return(csml_response) - - processor.perform - expect(conversation.reload.status).to eql('open') - end - end - - context 'when a new message is returned from CSML' do - it 'creates a text message' do - csml_response = ActiveSupport::HashWithIndifferentAccess.new( - messages: [ - { payload: { content_type: 'text', content: { text: 'hello payload' } } } - ] - ) - allow(csml_client).to receive(:run).and_return(csml_response) - processor.perform - expect(conversation.messages.last.content).to eql('hello payload') - end - - it 'creates a question message' do - csml_response = ActiveSupport::HashWithIndifferentAccess.new( - messages: [{ - payload: { - content_type: 'question', - content: { title: 'Question Payload', buttons: [{ content: { title: 'Q1', payload: 'q1' } }] } - } - }] - ) - allow(csml_client).to receive(:run).and_return(csml_response) - processor.perform - expect(conversation.messages.last.content).to eql('Question Payload') - expect(conversation.messages.last.content_type).to eql('input_select') - expect(conversation.messages.last.content_attributes).to eql({ items: [{ title: 'Q1', value: 'q1' }] }.with_indifferent_access) - end - end - - context 'when conversation status is not pending' do - let(:conversation) { create(:conversation, account: account, status: :open) } - - it 'returns nil' do - expect(processor.perform).to be_nil - end - end - - context 'when message is private' do - let(:message) { create(:message, account: account, conversation: conversation, private: true) } - - it 'returns nil' do - expect(processor.perform).to be_nil - end - end - - context 'when message type is template (not outgoing or incoming)' do - let(:message) { create(:message, account: account, conversation: conversation, message_type: :template) } - - it 'returns nil' do - expect(processor.perform).to be_nil - end - end - - context 'when message updated' do - let(:event_name) { 'message.updated' } - - context 'when content_type is input_select' do - let(:message) do - create(:message, account: account, conversation: conversation, private: true, - submitted_values: [{ 'title' => 'Support', 'value' => 'selected_gas' }]) - end - - it 'returns submitted value for message content' do - expect(processor.send(:message_content, message)).to eql('selected_gas') - end - end - - context 'when content_type is not input_select' do - let(:message) { create(:message, account: account, conversation: conversation, message_type: :outgoing, content_type: :text) } - let(:event_name) { 'message.updated' } - - it 'returns nil' do - expect(processor.perform).to be_nil - end - end - end - end -end diff --git a/spec/listeners/agent_bot_listener_spec.rb b/spec/listeners/agent_bot_listener_spec.rb index 5e17bfbef..2dc2b41df 100644 --- a/spec/listeners/agent_bot_listener_spec.rb +++ b/spec/listeners/agent_bot_listener_spec.rb @@ -37,15 +37,6 @@ describe AgentBotListener do listener.message_created(event) end end - - context 'when agent bot csml type is configured' do - it 'sends message to agent bot' do - agent_bot_csml = create(:agent_bot, :skip_validate, bot_type: 'csml') - create(:agent_bot_inbox, inbox: inbox, agent_bot: agent_bot_csml) - expect(AgentBots::CsmlJob).to receive(:perform_later).with('message.created', agent_bot_csml, message).once - listener.message_created(event) - end - end end describe '#webwidget_triggered' do diff --git a/spec/models/agent_bot_spec.rb b/spec/models/agent_bot_spec.rb index 280446f7c..d3e5e2d64 100644 --- a/spec/models/agent_bot_spec.rb +++ b/spec/models/agent_bot_spec.rb @@ -39,4 +39,23 @@ RSpec.describe AgentBot do expect(message.reload.sender).to be_nil end end + + describe '#system_bot?' do + context 'when account_id is nil' do + let(:agent_bot) { create(:agent_bot, account_id: nil) } + + it 'returns true' do + expect(agent_bot.system_bot?).to be true + end + end + + context 'when account_id is present' do + let(:account) { create(:account) } + let(:agent_bot) { create(:agent_bot, account: account) } + + it 'returns false' do + expect(agent_bot.system_bot?).to be false + end + end + end end diff --git a/spec/services/agent_bots/validate_bot_service_spec.rb b/spec/services/agent_bots/validate_bot_service_spec.rb deleted file mode 100644 index 005d5b94e..000000000 --- a/spec/services/agent_bots/validate_bot_service_spec.rb +++ /dev/null @@ -1,25 +0,0 @@ -require 'rails_helper' - -describe AgentBots::ValidateBotService do - describe '#perform' do - it 'returns true if bot_type is not csml' do - agent_bot = create(:agent_bot) - valid = described_class.new(agent_bot: agent_bot).perform - expect(valid).to be true - end - - it 'returns true if validate csml returns true' do - agent_bot = create(:agent_bot, :skip_validate, bot_type: 'csml', bot_config: {}) - csml_client = double - csml_response = double - allow(CsmlEngine).to receive(:new).and_return(csml_client) - allow(csml_client).to receive(:validate).and_return(csml_response) - allow(csml_response).to receive(:blank?).and_return(false) - allow(csml_response).to receive(:[]).with('valid').and_return(true) - - valid = described_class.new(agent_bot: agent_bot).perform - expect(valid).to be true - expect(CsmlEngine).to have_received(:new) - end - end -end