mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-07 04:53:24 -05:00
Notice draws what is off, why, who can fix it and one fix button, in that order, with a live role set by severity. FixNotice draws the same markup for add-on UIs, styled by the host's ui.css. The chat notice for commands now says that the server's built-in bash is off. An administrator gets the Settings row and a link to the Add-ons page; anyone else is told to ask an administrator. Hide keeps working. The Add-ons library card, the update notice and a turn on refused as installed_incompatible use the notice, with an Upgrade fix. Assisted-by: Claude:claude-sonnet-5-5
129 lines
7.9 KiB
JavaScript
129 lines
7.9 KiB
JavaScript
import { test, expect } from '@playwright/test'
|
|
import { ADMIN, app, installedAddon, release, sessionFor, mockAddons, fail } from './addons-fixtures.js'
|
|
|
|
// The Add-ons page says that something is off or unavailable with the shared
|
|
// notice (what, why, who, fix): the update states on a library card, the update
|
|
// on the detail pane, and a turn on that the server refuses because the
|
|
// installed package cannot run here. The routes are stand-ins.
|
|
|
|
const shots = process.env.NOTICE_SHOTS
|
|
const theme = process.env.NOTICE_THEME
|
|
async function shoot(page, name) {
|
|
if (!shots) return
|
|
await page.emulateMedia({ reducedMotion: 'reduce' })
|
|
await page.waitForTimeout(400)
|
|
await page.screenshot({ path: `${shots}/${name}${process.env.NOTICE_SUFFIX || ''}.png`, fullPage: false })
|
|
}
|
|
|
|
const CANDIDATE = 'sha256:' + 'b'.repeat(64)
|
|
const entry = (page, id) => page.locator('.cat-item', { has: page.locator(`[data-addon="${id}"]`) })
|
|
const rec = (id, title, update, over = {}) => installedAddon({ id, title, icon: undefined, mainPage: undefined, digest: 'sha256:' + id.padEnd(32, '0'), update, ...over })
|
|
|
|
async function signIn(page) {
|
|
if (theme) await page.addInitScript((value) => { try { localStorage.setItem('localai-theme', value) } catch { /* private window */ } }, theme)
|
|
await page.route('**/api/auth/status', route => route.fulfill({ json: sessionFor(ADMIN) }))
|
|
}
|
|
|
|
test.describe('Add-ons: notices', () => {
|
|
test('a library card says what an installed add-on is not up to date on, in the notice', async ({ page }) => {
|
|
await signIn(page)
|
|
await mockAddons(page, {
|
|
releases: ['wiki', 'docs', 'slack', 'tables', 'mail'].map(id => release(id, { version: '1.0.0' })),
|
|
installed: [
|
|
rec('wiki', 'Wiki', { available: true, state: 'update-available', version: '1.3.0', digest: CANDIDATE, checkedAt: '2026-10-01T00:00:00Z' }),
|
|
rec('docs', 'Docs', { available: false, state: 'incompatible', version: '3.0.0', reasons: ['host-version'] }),
|
|
rec('slack', 'Slack', { state: 'check-failed', available: false, error: 'refresh-failed' }),
|
|
rec('tables', 'Tables', undefined),
|
|
rec('mail', 'Mail', { state: 'up-to-date', available: false }),
|
|
],
|
|
})
|
|
await page.setViewportSize({ width: 1280, height: 1500 })
|
|
await page.goto('/app/addons?tab=browse')
|
|
const note = (id) => entry(page, id).getByTestId('entry-update-note')
|
|
|
|
await expect(note('docs')).toHaveAttribute('role', 'status')
|
|
await expect(note('docs')).toHaveAttribute('data-state', 'incompatible')
|
|
await expect(note('docs')).toContainText('cannot run on this host. It is not offered.')
|
|
await expect(note('slack')).toHaveAttribute('role', 'status')
|
|
await expect(note('slack')).toContainText('The update check failed, so it is not known whether an update exists.')
|
|
await expect(note('tables')).toHaveAttribute('role', 'note')
|
|
await expect(note('tables')).toContainText('The update status is not known. Check for updates to ask the library.')
|
|
// Up to date is not a problem: one quiet line, not a notice.
|
|
await expect(note('mail')).toContainText('This add-on is up to date.')
|
|
await expect(note('mail')).not.toHaveClass(/ui-notice/)
|
|
|
|
// An update is a notice with the Upgrade fix, which opens the confirmation.
|
|
const update = entry(page, 'wiki').getByTestId('entry-upgrade')
|
|
await expect(update).toHaveClass(/ui-notice/)
|
|
await expect(update).toContainText('Version 1.3.0 is available.')
|
|
await shoot(page, 'library')
|
|
await update.getByTestId('update-upgrade-card').click()
|
|
await expect(page.getByTestId('upgrade-confirm')).toContainText('1.3.0')
|
|
})
|
|
|
|
test('the pane shows the update as a notice with the Upgrade fix', async ({ page }) => {
|
|
await signIn(page)
|
|
await mockAddons(page, {
|
|
apps: [app({ id: 'wiki', title: 'Wiki', update: 'update-pending', mainPage: undefined, admin: { canStart: false, canStop: true, managementUrl: '/app/addons?manage=wiki' } })],
|
|
installed: [rec('wiki', 'Wiki', { available: true, state: 'update-available', version: '1.3.0', digest: CANDIDATE, checkedAt: '2026-10-01T00:00:00Z' })],
|
|
})
|
|
await page.goto('/app/addons?detail=wiki')
|
|
const notice = page.getByTestId('pane-update')
|
|
await expect(notice).toHaveClass(/ui-notice/)
|
|
await expect(notice).toContainText('1.3.0')
|
|
await shoot(page, 'pane-update')
|
|
await notice.getByRole('button', { name: /^Upgrade/ }).click()
|
|
await expect(page.getByTestId('upgrade-confirm')).toContainText('1.3.0')
|
|
})
|
|
|
|
test('a turn on refused as incompatible says why and offers Upgrade', async ({ page }) => {
|
|
await signIn(page)
|
|
await mockAddons(page, {
|
|
apps: [app({ id: 'tg', title: 'Telegram', enabled: false, state: 'stopped', update: 'update-pending', mainPage: undefined, admin: { canStart: true, canStop: false, managementUrl: '/app/addons?manage=tg' } })],
|
|
installed: [rec('tg', 'Telegram', { available: true, state: 'update-available', version: '1.5.0', digest: CANDIDATE, checkedAt: '2026-10-01T00:00:00Z' }, { enabled: false, state: 'stopped' })],
|
|
start: () => fail(403, 'installed_incompatible', { message: 'This version of the add-on is not compatible with this server, so it cannot be turned on. Upgrade it to a newer version, then turn it on. Nothing was changed' }),
|
|
})
|
|
await page.goto('/app/addons?detail=tg')
|
|
await page.getByRole('button', { name: 'Turn on' }).click()
|
|
const notice = page.getByTestId('pane-enable-failed')
|
|
await expect(notice).toHaveAttribute('role', 'alert')
|
|
await expect(notice).toHaveAttribute('data-cause', 'installed_incompatible')
|
|
await expect(notice).toContainText('Cannot turn on Telegram: not compatible with this server')
|
|
await expect(notice).toContainText('Upgrade it to a newer version, then turn it on.')
|
|
// The plain update notice steps aside: one Upgrade, on the failure.
|
|
await expect(page.getByTestId('pane-update')).toHaveCount(0)
|
|
await shoot(page, 'enable-incompatible')
|
|
await notice.getByRole('button', { name: /^Upgrade/ }).click()
|
|
await expect(page.getByTestId('upgrade-confirm')).toContainText('1.5.0')
|
|
})
|
|
|
|
test('with no newer version listed the refusal says there is nothing to upgrade to', async ({ page }) => {
|
|
await signIn(page)
|
|
await mockAddons(page, {
|
|
apps: [app({ id: 'tg', title: 'Telegram', enabled: false, state: 'stopped', mainPage: undefined, admin: { canStart: true, canStop: false, managementUrl: '/app/addons?manage=tg' } })],
|
|
installed: [rec('tg', 'Telegram', { available: false, state: 'incompatible', version: '', reasons: ['api-version'] }, { enabled: false, state: 'stopped' })],
|
|
start: () => fail(403, 'installed_incompatible', { message: 'This version of the add-on is not compatible with this server, so it cannot be turned on. Upgrade it to a newer version, then turn it on. Nothing was changed' }),
|
|
})
|
|
await page.goto('/app/addons?detail=tg')
|
|
await page.getByRole('button', { name: 'Turn on' }).click()
|
|
const notice = page.getByTestId('pane-enable-failed')
|
|
await expect(notice).toContainText('No newer version is listed yet. Keep it off, or remove it.')
|
|
await expect(notice.getByRole('button')).toHaveCount(0)
|
|
await shoot(page, 'enable-no-upgrade')
|
|
})
|
|
|
|
test('another refused turn on is a notice with the server reason, and no fix', async ({ page }) => {
|
|
await signIn(page)
|
|
await mockAddons(page, {
|
|
apps: [app({ id: 'tg', title: 'Telegram', enabled: false, state: 'stopped', mainPage: undefined, admin: { canStart: true, canStop: false, managementUrl: '/app/addons?manage=tg' } })],
|
|
start: () => fail(403, 'addon_error', { message: 'missing configuration: data directory' }),
|
|
})
|
|
await page.goto('/app/addons?detail=tg')
|
|
await page.getByRole('button', { name: 'Turn on' }).click()
|
|
const notice = page.getByTestId('pane-enable-failed')
|
|
await expect(notice).toContainText('Could not turn on Telegram')
|
|
await expect(notice).toContainText('missing configuration: data directory')
|
|
await shoot(page, 'enable-failed')
|
|
})
|
|
})
|