mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-06 12:33:16 -05:00
The confirmations, refusals and notes that the host writes in a chat
without the model were fixed English: "Started a new chat", the question
about a proposal, the file notes, the pairing replies. They now come from
a catalog (core/services/studio/hostmsg) with a template for English,
Italian, German, Spanish, French and Portuguese (Brazil), and the same
{placeholders} in each. Text for the model (tool descriptions, the held
back note, the thread block) stays English.
A connection has a reply_language setting: auto (default), en, it, de,
es, fr or pt-BR. With auto the host scores the sender's message against
the common words of the six languages, with no model and no dependency.
A message that does not show a language ("ok", "yes", "thanks") changes
nothing, and a short message cannot move a conversation to another
language. The last clear language of a sender and of a chat is kept for
24 hours; a trigger that writes on its own uses the chat's. The setting
is stored in the JSON and the gorm store, is kept by an update that
leaves it out, and is on the connection page under "How it answers".
Every sentence goes through msg or a hostmsg.Voice. A guard test fails
when a fixed text reaches say, Post or Outgoing, when a key is not in the
catalog, or when a key is never used. The reply parser now reads the
yes, no and undo words of the six languages, which the new texts ask for.
Assisted-by: Claude:claude-sonnet-5-5
263 lines
16 KiB
JavaScript
263 lines
16 KiB
JavaScript
import { test, expect } from './coverage-fixtures.js'
|
|
import { mockChat } from './chat-fixtures.js'
|
|
import { installStudioAddons } from './studio-addon.js'
|
|
import { connectorApi as api, DECLARED } from './connector-fixtures.js'
|
|
|
|
// The page of the Telegram add-on (the real built bundle, inside the add-on
|
|
// shell) with the core API stubbed: the token only goes in, "anyone"
|
|
// needs a confirmation, and a sender in the log can be allowed.
|
|
|
|
const PAGE = '/app/addons/telegram/bots'
|
|
|
|
test.describe('Telegram add-on', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await mockChat(page)
|
|
await installStudioAddons(page, { ids: ['telegram'] })
|
|
})
|
|
|
|
test('creates a binding with a token and an agent, and lists who it accepts', async ({ page }) => {
|
|
const a = api(page)
|
|
await a.install()
|
|
await page.goto(PAGE)
|
|
|
|
await expect(page.getByText('No Telegram bots yet.')).toBeVisible()
|
|
await page.getByRole('button', { name: 'New Telegram bot' }).click()
|
|
await expect(page.getByRole('option', { name: 'No model (needs a model)' })).toHaveAttribute('disabled', '')
|
|
await page.getByLabel('Name', { exact: true }).fill('Support bot')
|
|
await page.getByLabel('Bot token').fill('555:secret-token')
|
|
await page.getByLabel('Agent', { exact: true }).selectOption('a1')
|
|
await page.getByRole('textbox', { name: 'Allowed senders +' }).fill('42')
|
|
await page.getByRole('textbox', { name: 'Allowed senders +' }).press('Enter')
|
|
await page.getByRole('button', { name: 'Save' }).click()
|
|
|
|
await expect(page.getByTestId('connector-row')).toContainText('Support bot')
|
|
await expect(page.getByTestId('connector-row')).toContainText('Agent: Support')
|
|
const body = a.s.calls.find(c => c.method === 'POST' && c.url === '/api/chats/connectors').body
|
|
expect(body).toMatchObject({ name: 'Support bot', token: '555:secret-token', agent_id: 'a1', allow: ['42'], accept_any: false })
|
|
await expect(page.locator('body')).not.toContainText('secret-token')
|
|
// A new token is checked as soon as it is saved.
|
|
await expect(page.getByText('Connected to @support_bot.')).toBeVisible()
|
|
})
|
|
|
|
test('the token field is empty on edit, and an update without a new token sends none', async ({ page }) => {
|
|
const a = api(page, [{ id: 'b1', name: 'Support bot', kind: 'telegram', agent_id: 'a1', enabled: true, accept_any: false, allow: ['42'], has_token: true }])
|
|
await a.install()
|
|
await page.goto(PAGE)
|
|
await page.getByRole('button', { name: 'Edit' }).click()
|
|
await expect(page.getByLabel('Bot token')).toHaveValue('')
|
|
await expect(page.getByLabel('Bot token')).toHaveAttribute('placeholder', /Saved/)
|
|
await page.getByLabel('Name', { exact: true }).fill('Renamed')
|
|
await page.getByRole('button', { name: 'Save' }).click()
|
|
const put = a.s.calls.find(c => c.method === 'PUT')
|
|
expect(put.url).toBe('/api/chats/connectors/b1')
|
|
expect('token' in put.body).toBe(false)
|
|
})
|
|
|
|
test('anyone needs the warning confirmed before it can be saved', async ({ page }) => {
|
|
await api(page).install()
|
|
await page.goto(PAGE)
|
|
await page.getByRole('button', { name: 'New Telegram bot' }).click()
|
|
await page.getByLabel('Name', { exact: true }).fill('Public bot')
|
|
await page.getByLabel('Bot token').fill('1:x')
|
|
await page.getByLabel('Agent', { exact: true }).selectOption('a1')
|
|
await page.getByRole('radio', { name: 'Anyone who can message the bot' }).check()
|
|
await expect(page.getByTestId('anyone-warning')).toContainText("can use this agent's tools")
|
|
await expect(page.getByRole('button', { name: 'Save' })).toBeDisabled()
|
|
await page.getByRole('checkbox', { name: 'I understand' }).check()
|
|
await expect(page.getByRole('button', { name: 'Save' })).toBeEnabled()
|
|
})
|
|
|
|
test('tests the token, shows the sender log and allows a sender', async ({ page }) => {
|
|
const a = api(page, [{ id: 'b1', name: 'Support bot', kind: 'telegram', agent_id: 'a1', enabled: true, accept_any: false, allow: [], has_token: true }])
|
|
await a.install()
|
|
await page.goto(PAGE)
|
|
await page.getByRole('button', { name: 'Test' }).click()
|
|
await expect(page.getByText('Connected to @support_bot.')).toBeVisible()
|
|
await page.getByRole('button', { name: 'Senders' }).click()
|
|
await expect(page.getByTestId('sender-log')).toContainText('Newcomer · 2 messages · 42')
|
|
await expect(page.getByTestId('sender-log')).toContainText('Ignored')
|
|
await page.getByRole('button', { name: 'Allow', exact: true }).click()
|
|
await expect(page.getByTestId('sender-log')).toContainText('Accepted')
|
|
expect(a.s.calls.some(c => c.url === '/api/chats/connectors/b1/senders/42/allow')).toBe(true)
|
|
})
|
|
|
|
test('switches a binding off and deletes it', async ({ page }) => {
|
|
const a = api(page, [{ id: 'b1', name: 'Support bot', kind: 'telegram', agent_id: 'a1', enabled: true, accept_any: false, allow: [], has_token: true }])
|
|
await a.install()
|
|
await page.goto(PAGE)
|
|
await page.getByRole('switch', { name: 'Support bot on' }).uncheck()
|
|
await expect.poll(() => a.s.calls.some(c => c.url.endsWith('/disable'))).toBe(true)
|
|
// Deleting asks first, and says the bot token goes with it.
|
|
await page.getByTestId('connector-row').getByRole('button', { name: 'Delete' }).click()
|
|
const dialog = page.getByRole('alertdialog')
|
|
await expect(dialog).toContainText('Delete Support bot?')
|
|
await expect(dialog).toContainText('bot token')
|
|
await dialog.getByRole('button', { name: 'Cancel' }).click()
|
|
await expect(page.getByTestId('connector-row')).toHaveCount(1)
|
|
expect(a.s.calls.some(c => c.method === 'DELETE')).toBe(false)
|
|
|
|
await page.getByTestId('connector-row').getByRole('button', { name: 'Delete' }).click()
|
|
await dialog.getByRole('button', { name: 'Delete' }).click()
|
|
await expect(page.getByTestId('connector-row')).toHaveCount(0)
|
|
})
|
|
|
|
test('a delete the server refuses shows its reason and keeps the binding', async ({ page }) => {
|
|
const a = api(page, [{ id: 'b1', name: 'Support bot', kind: 'telegram', agent_id: 'a1', enabled: true, accept_any: false, allow: [], has_token: true }])
|
|
await a.install()
|
|
await page.route(/\/api\/chats\/connectors\/b1$/, r => r.fulfill({ status: 500, json: { error: { message: 'the store is read-only' } } }))
|
|
await page.goto(PAGE)
|
|
await page.getByTestId('connector-row').getByRole('button', { name: 'Delete' }).click()
|
|
await page.getByRole('alertdialog').getByRole('button', { name: 'Delete' }).click()
|
|
await expect(page.getByRole('alert')).toContainText('the store is read-only')
|
|
await expect(page.getByTestId('connector-row')).toHaveCount(1)
|
|
})
|
|
|
|
test('sets how the bot answers, and the API keeps what was set at each step', async ({ page }) => {
|
|
const a = api(page)
|
|
await a.install()
|
|
await page.goto(PAGE)
|
|
await page.getByRole('button', { name: 'New Telegram bot' }).click()
|
|
// A new binding shows progress, one chat per person, and sends files.
|
|
await expect(page.getByTestId('connector-reply')).toHaveValue('progress')
|
|
await expect(page.getByTestId('connector-conversation')).toHaveValue('sender')
|
|
await expect(page.getByText('Send /new (or !new) in the app to start a new chat.')).toBeVisible()
|
|
await expect(page.getByTestId('connector-files')).toBeChecked()
|
|
await expect(page.getByTestId('connector-window')).toHaveCount(0)
|
|
// The notes the bot writes itself follow the sender unless a language is chosen.
|
|
await expect(page.getByTestId('connector-reply-language')).toHaveValue('auto')
|
|
await expect(page.getByText('The notes the bot writes itself, like confirmations and refusals.')).toBeVisible()
|
|
await page.getByLabel('Name', { exact: true }).fill('Live bot')
|
|
await page.getByLabel('Bot token').fill('555:secret-token')
|
|
await page.getByLabel('Agent', { exact: true }).selectOption('a1')
|
|
await page.getByTestId('connector-reply').selectOption('stream')
|
|
await page.getByTestId('connector-conversation').selectOption('window')
|
|
await page.getByTestId('connector-window').fill('45')
|
|
await page.getByTestId('connector-files').uncheck()
|
|
await page.getByTestId('connector-reply-language').selectOption('it')
|
|
await page.getByRole('button', { name: 'Save' }).click()
|
|
|
|
await expect(page.getByTestId('connector-answer-summary')).toHaveText('Answers: live answer · new chat after 45 min')
|
|
const post = a.s.calls.find(c => c.method === 'POST' && c.url === '/api/chats/connectors').body
|
|
expect(post).toMatchObject({ reply_mode: 'stream', conversation: 'window', window_minutes: 45, send_files: false, reply_language: 'it' })
|
|
expect(a.s.items[0]).toMatchObject({ reply_mode: 'stream', conversation: 'window', window_minutes: 45, send_files: false })
|
|
|
|
// Edit: the form shows what the API holds; the thread mode drops the window.
|
|
await page.getByRole('button', { name: 'Edit' }).click()
|
|
await expect(page.getByTestId('connector-reply')).toHaveValue('stream')
|
|
await expect(page.getByTestId('connector-conversation')).toHaveValue('window')
|
|
await expect(page.getByTestId('connector-window')).toHaveValue('45')
|
|
await expect(page.getByTestId('connector-files')).not.toBeChecked()
|
|
await expect(page.getByTestId('connector-reply-language')).toHaveValue('it')
|
|
await page.getByTestId('connector-reply-language').selectOption('pt-BR')
|
|
await page.getByTestId('connector-reply').selectOption('off')
|
|
await page.getByTestId('connector-conversation').selectOption('thread')
|
|
await page.getByTestId('connector-files').check()
|
|
await page.getByRole('button', { name: 'Save' }).click()
|
|
await expect(page.getByTestId('connector-answer-summary')).toHaveText('Answers: typing · a chat per message')
|
|
const put = a.s.calls.filter(c => c.method === 'PUT').at(-1).body
|
|
expect(put).toMatchObject({ reply_mode: 'off', conversation: 'thread', send_files: true, reply_language: 'pt-BR' })
|
|
expect(put).not.toHaveProperty('window_minutes')
|
|
expect(a.s.items[0]).toMatchObject({ reply_mode: 'off', conversation: 'thread', send_files: true })
|
|
})
|
|
|
|
test('a window out of range cannot be saved', async ({ page }) => {
|
|
await api(page).install()
|
|
await page.goto(PAGE)
|
|
await page.getByRole('button', { name: 'New Telegram bot' }).click()
|
|
await page.getByLabel('Name', { exact: true }).fill('Window bot')
|
|
await page.getByLabel('Bot token').fill('1:x')
|
|
await page.getByLabel('Agent', { exact: true }).selectOption('a1')
|
|
await page.getByTestId('connector-conversation').selectOption('window')
|
|
await page.getByTestId('connector-window').fill('0')
|
|
await expect(page.getByRole('button', { name: 'Save' })).toBeDisabled()
|
|
await page.getByTestId('connector-window').fill('30')
|
|
await expect(page.getByRole('button', { name: 'Save' })).toBeEnabled()
|
|
})
|
|
|
|
test('the Files section says which limit applies', async ({ page }) => {
|
|
const a = api(page)
|
|
await a.install()
|
|
await page.goto(PAGE)
|
|
await page.getByRole('button', { name: 'New Telegram bot' }).click()
|
|
const files = page.getByTestId('connector-filelimits')
|
|
await expect(files).toContainText('Now: 1 MiB (limit of the add-on).')
|
|
// The owner can only lower the limit: 2 MiB is above the 1 MiB the kind allows.
|
|
await page.getByTestId('connector-file-send').fill('2')
|
|
await expect(files).toContainText('The add-on allows at most 1 MiB here.')
|
|
await page.getByLabel('Name', { exact: true }).fill('Files bot')
|
|
await page.getByLabel('Bot token').fill('555:secret-token')
|
|
await page.getByLabel('Agent', { exact: true }).selectOption('a1')
|
|
await expect(page.getByRole('button', { name: 'Save' })).toBeDisabled()
|
|
await page.getByTestId('connector-file-send').fill('0.25')
|
|
await expect(files).toContainText('Now: 256 KiB (limit set for this bot).')
|
|
await page.getByRole('button', { name: 'Save' }).click()
|
|
const post = a.s.calls.find(c => c.method === 'POST' && c.url === '/api/chats/connectors').body
|
|
expect(post).toMatchObject({ file_send_max_bytes: 256 * 1024, file_receive_max_bytes: 0 })
|
|
})
|
|
|
|
test('a kind that declares abilities shows its own limits', async ({ page }) => {
|
|
const a = api(page, [], 'telegram', [], DECLARED)
|
|
await a.install()
|
|
await page.goto(PAGE)
|
|
await page.getByRole('button', { name: 'New Telegram bot' }).click()
|
|
const files = page.getByTestId('connector-filelimits')
|
|
await expect(files).toContainText('Now: 20 MiB (limit of the add-on).')
|
|
await expect(files).toContainText('Now: 256 KiB (limit of the add-on).')
|
|
// Reading is allowed up to 20 MiB, so 5 MiB is accepted and becomes the limit in effect.
|
|
await page.getByTestId('connector-file-receive').fill('5')
|
|
await expect(files).not.toContainText('allows at most')
|
|
await expect(files).toContainText('Now: 5 MiB (limit set for this bot).')
|
|
})
|
|
|
|
test('an edit shows the limit the owner set and a save with it empty clears it', async ({ page }) => {
|
|
const a = api(page, [{ id: 'b1', name: 'Support bot', kind: 'telegram', agent_id: 'a1', enabled: true, accept_any: false, allow: ['42'], has_token: true, file_send_max_bytes: 100 * 1024 }])
|
|
await a.install()
|
|
await page.goto(PAGE)
|
|
await page.getByRole('button', { name: 'Edit' }).click()
|
|
await expect(page.getByTestId('connector-file-send')).toHaveValue('0.09765625')
|
|
await expect(page.getByTestId('connector-file-receive')).toHaveValue('')
|
|
await page.getByTestId('connector-file-send').fill('')
|
|
await page.getByRole('button', { name: 'Save' }).click()
|
|
const put = a.s.calls.find(c => c.method === 'PUT').body
|
|
expect(put).toMatchObject({ file_send_max_bytes: 0, file_receive_max_bytes: 0 })
|
|
})
|
|
|
|
test('readiness and the preview follow a new bot: token, agent and who, then the form values reach the preview', async ({ page }) => {
|
|
const a = api(page)
|
|
await a.install()
|
|
await page.goto(PAGE)
|
|
await page.getByRole('button', { name: 'New Telegram bot' }).click()
|
|
const ready = page.getByTestId('connector-readiness')
|
|
await expect(ready.getByTestId('connector-ready-token')).toContainText('Paste the token')
|
|
await expect(ready.getByTestId('connector-ready-agent')).toContainText('Choose the agent')
|
|
await expect(ready.getByTestId('connector-ready-who')).toContainText('Nobody is allowed yet')
|
|
await ready.getByTestId('connector-ready-token').getByRole('button', { name: 'Paste the token' }).click()
|
|
await expect(page.getByLabel('Bot token')).toBeFocused()
|
|
await page.getByLabel('Bot token').fill('555:secret-token')
|
|
await expect(ready.getByTestId('connector-ready-token')).toHaveAttribute('data-state', 'after_save')
|
|
await page.getByLabel('Agent', { exact: true }).selectOption('a1')
|
|
await page.getByRole('textbox', { name: 'Allowed senders +' }).fill('42')
|
|
await page.getByRole('textbox', { name: 'Allowed senders +' }).press('Enter')
|
|
await expect(ready.getByTestId('connector-ready-who')).toHaveAttribute('data-state', 'after_save')
|
|
await expect.poll(() => a.s.previews.at(-1)?.body).toMatchObject({ kind: 'telegram', agent_id: 'a1', allow: ['42'] })
|
|
expect(a.s.previews.at(-1).id).toBe('new')
|
|
// One list of senders: no member-of-a-channel choice.
|
|
await expect(page.getByTestId('connector-can-sender').locator('option')).toHaveText(['A person on the allowed list', 'A person you paired'])
|
|
expect(JSON.stringify(a.s.previews)).not.toContain('secret-token')
|
|
})
|
|
|
|
test('a token the platform refuses shows as not working in the readiness list', async ({ page }) => {
|
|
const a = api(page, [{ id: 'b1', name: 'Support bot', kind: 'telegram', agent_id: 'a1', enabled: true, accept_any: false, allow: ['42'], has_token: true }])
|
|
await a.install()
|
|
await page.route(/\/api\/chats\/connectors\/[^/?]+\/test$/, r => r.fulfill({ status: 502, json: { error: { message: 'Unauthorized' } } }))
|
|
await page.goto(PAGE)
|
|
await page.getByRole('button', { name: 'Test' }).click()
|
|
await page.getByRole('button', { name: 'Edit' }).click()
|
|
const token = page.getByTestId('connector-ready-token')
|
|
await expect(token).toHaveAttribute('data-state', 'broken')
|
|
await expect(token).toContainText('The platform refused the token: Unauthorized')
|
|
await token.getByRole('button', { name: 'Paste a new token' }).click()
|
|
await expect(page.getByLabel('Bot token')).toBeFocused()
|
|
})
|
|
})
|