import { expect, test } from '@playwright/test'; import { installLangBotApiMocks, buildExecutionList, } from './fixtures/langbot-api'; const monitoring = { messages: [ { id: 'input-one', role: 'user', message_content: 'Open this execution', timestamp: '2026-07-02T10:00:00Z', bot_id: 'bot', pipeline_id: 'pipe', pipeline_name: 'Processor A', }, ], }; const row = buildExecutionList(monitoring).items[0]; const pageOf = ( items: unknown[], has_more = false, next_offset = items.length, ) => ({ items, has_more, next_offset }); test('loads collapsed history on demand, retries and pages without reloading it on reopen', async ({ page, }) => { await installLangBotApiMocks(page, { authenticated: true, monitoringData: monitoring, }); const requests: string[] = []; let failHistory = true; await page.route('**/api/v1/monitoring/executions/*/*?*', async (route) => { const url = new URL(route.request().url()); const section = url.searchParams.get('section'); requests.push(url.search); if (section === 'conversation' && failHistory) { failHistory = false; await route.fulfill({ status: 500, json: { code: 500, msg: 'Temporary failure' }, }); return; } const offset = Number(url.searchParams.get('offset') || 0); const pages = section === 'overview' ? { inputs: pageOf([{ id: 'input', content: 'Selected message' }]), outputs: pageOf([]), deliveries: pageOf([]), errors: pageOf([]), } : section === 'conversation' ? { conversation: pageOf( [ { id: `history-${offset}`, content: offset ? 'Older history' : 'Recent history', }, ], offset === 0, offset + 1, ), } : { related: pageOf([]) }; await route.fulfill({ json: { code: 0, data: { source: 'pipeline', row, pages } }, }); }); await page.goto('/home/monitoring'); await page .getByRole('button') .filter({ hasText: 'Open this execution' }) .click(); const sheet = page.getByRole('dialog'); await expect( sheet.getByText('Selected message').filter({ visible: true }), ).toBeVisible(); expect(requests).toEqual(['?section=overview']); const history = sheet.locator('details').filter({ has: page.locator('summary', { hasText: /^Conversation context/ }), }); await history.locator(':scope > summary').click(); await expect(history.getByRole('alert')).toBeVisible(); await history.getByRole('button', { name: 'Retry', exact: true }).click(); await expect( history.getByText('Recent history').filter({ visible: true }), ).toBeVisible(); await history.getByRole('button', { name: 'Load more', exact: true }).click(); await expect( history.getByText('Older history').filter({ visible: true }), ).toBeVisible(); await expect( history.getByText('Recent history').filter({ visible: true }), ).toBeVisible(); const count = requests.length; await history.locator(':scope > summary').click(); await history.locator(':scope > summary').click(); await expect( history.getByText('Older history').filter({ visible: true }), ).toBeVisible(); expect(requests).toHaveLength(count); expect(requests.every((query) => !query.includes('section=related'))).toBe( true, ); }); test('ignores a late history response after selecting a different execution', async ({ page, }) => { await installLangBotApiMocks(page, { authenticated: true, monitoringData: monitoring, }); let releaseHistory: (() => void) | undefined; const requested: string[] = []; await page.route('**/api/v1/monitoring/executions/*/*?*', async (route) => { const url = new URL(route.request().url()); const section = url.searchParams.get('section'); const sibling = url.pathname.endsWith('/sibling'); requested.push(`${sibling ? 'sibling' : 'original'}:${section}`); if (section === 'conversation') await new Promise((resolve) => { releaseHistory = resolve; }); const pages = section === 'overview' ? { inputs: pageOf([ { id: 'input', content: sibling ? 'Sibling selection' : 'Original selection', }, ]), outputs: pageOf([]), deliveries: pageOf([]), errors: pageOf([]), } : section === 'related' ? { related: pageOf([ { ...row, id: 'sibling', target_name: 'Processor B' }, ]), } : { conversation: pageOf([ { id: 'late', content: 'Late old history' }, ]), }; await route.fulfill({ json: { code: 0, data: { source: 'pipeline', row: sibling ? { ...row, id: 'sibling' } : row, pages, }, }, }); }); await page.goto('/home/monitoring'); await page .getByRole('button') .filter({ hasText: 'Open this execution' }) .click(); const sheet = page.getByRole('dialog'); await expect( sheet.getByText('Original selection').filter({ visible: true }), ).toBeVisible(); await sheet .locator('summary') .filter({ hasText: /^Conversation context/ }) .click(); await expect.poll(() => !!releaseHistory).toBe(true); await sheet .locator('summary') .filter({ hasText: /^Related executions/ }) .click(); await sheet.getByRole('button', { name: /Processor B/ }).click(); await expect( sheet.getByText('Sibling selection').filter({ visible: true }), ).toBeVisible(); const response = page.waitForResponse( (res) => new URL(res.url()).searchParams.get('section') === 'conversation', ); releaseHistory!(); await response; await expect(sheet.getByText('Late old history')).toHaveCount(0); expect(requested.filter((value) => value.startsWith('sibling:'))).toEqual([ 'sibling:overview', ]); }); test('renders typed and custom events without duplicate text or empty attachment messages', async ({ page, }) => { await installLangBotApiMocks(page, { authenticated: true, monitoringData: monitoring, }); await page.route('**/api/v1/monitoring/executions/*/*?*', async (route) => { await route.fulfill({ json: { code: 0, data: { source: 'pipeline', row, pages: { inputs: pageOf([ { id: 'message', content: { text: '你好~', contents: [{ type: 'text', text: '你好~', image_url: null }], attachments: [], }, }, { id: 'member', event_type: 'group.member_banned', content: { member: { id: 'member-42', nickname: 'Alice' }, group: { id: 'group-9' }, duration: 0, permanent: false, custom: { reason: 'Moderation reason' }, }, }, { id: 'edited', content: { message_chain: [{ type: 'Plain', text: 'Typed chain text' }], editor: { id: 'editor-7' }, new_content: [{ type: 'Plain', text: 'Updated text' }], }, }, { id: 'custom', content: { type: 'CustomEvent', text: 'Distinct text', content: 'Different content', properties: { nested: ['custom-value'] }, }, }, { id: 'same-again', content: { text: '你好~', contents: [{ type: 'text', text: '你好~' }], }, }, { id: 'image', content: { contents: [ { type: 'image_url', image_url: { url: 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7', }, }, ], }, }, ]), outputs: pageOf([ { id: 'assistant', content: { content: 'Agent answer', tool_calls: [ { id: 'tool-1', function: { name: 'event_reply', arguments: '{"text":"sent"}', }, }, ], }, attachments: [], }, ]), deliveries: pageOf([]), }, }, }, }); }); await page.goto('/home/monitoring'); await page .getByRole('button') .filter({ hasText: 'Open this execution' }) .click(); const sheet = page.getByRole('dialog'); await expect( sheet.getByText('你好~', { exact: true }).filter({ visible: true }), ).toHaveCount(2); await expect(sheet.getByText('[Empty message]', { exact: true })).toHaveCount( 0, ); for (const details of await sheet .locator('summary') .filter({ hasText: /^Details$/ }) .all()) await details.click(); for (const text of [ 'Agent answer', 'member-42', 'Alice', 'group-9', 'Moderation reason', '0', 'false', 'Typed chain text', 'editor-7', 'Updated text', 'Distinct text', 'Different content', 'custom-value', 'event_reply', ]) { await expect( sheet.getByText(text, { exact: true }).filter({ visible: true }).first(), ).toBeVisible(); } await expect( sheet.getByRole('img', { name: 'Message attachment' }), ).toBeVisible(); }); test('pages a long trace without dropping inputs and opens related executions', async ({ page, }) => { await installLangBotApiMocks(page, { authenticated: true, monitoringData: monitoring, }); const requests: string[] = []; await page.route('**/api/v1/monitoring/executions/*/*?*', async (route) => { const url = new URL(route.request().url()); requests.push(url.search); const sibling = url.pathname.endsWith('/sibling'); const more = url.searchParams.get('section') === 'events'; await route.fulfill({ json: { code: 0, data: { source: 'pipeline', row: sibling ? { ...row, id: 'sibling', target_name: 'Processor B' } : row, pages: more ? { events: pageOf( [ { id: 101, sequence: 101, type: 'run.completed', data: { final: 'Last trace event' }, }, ], false, 101, ), } : { inputs: pageOf([ { id: 'in', content: sibling ? 'Sibling input' : 'Original input', }, ]), outputs: pageOf([ { id: 'out', content: 'Generated but not sent' }, ]), deliveries: pageOf([]), events: pageOf( [{ id: 1, sequence: 1, type: 'run.started', data: {} }], true, 100, ), related: pageOf( sibling ? [] : [{ ...row, id: 'sibling', target_name: 'Processor B' }], ), }, }, }, }); }); await page.goto('/home/monitoring'); await page .getByRole('button') .filter({ hasText: 'Open this execution' }) .click(); const sheet = page.getByRole('dialog'); await expect( sheet.getByText('Generated but not sent').filter({ visible: true }), ).toBeVisible(); await expect(sheet.getByText('Delivery records (0)')).toBeVisible(); await sheet.getByText('Execution events (1+)', { exact: true }).click(); await sheet.getByRole('button', { name: 'Load more' }).click(); await expect(sheet.getByText('run.completed', { exact: true })).toBeVisible(); await expect( sheet.getByText('Original input').filter({ visible: true }), ).toBeVisible(); expect( requests.some( (query) => query.includes('section=events') && query.includes('offset=100'), ), ).toBe(true); await sheet.getByText('Related executions (1)', { exact: true }).click(); await sheet.getByRole('button', { name: /Processor B/ }).click(); await expect( sheet.getByText('Sibling input').filter({ visible: true }), ).toBeVisible(); await expect( sheet.getByText('Original input').filter({ visible: true }), ).toHaveCount(0); }); test('shows non-message events without fabricating a reply or a processor', async ({ page, }) => { await installLangBotApiMocks(page, { authenticated: true }); const event = { ...row, source: 'event', id: 'event-only', event_id: 'event-only', target_id: null, target_kind: 'event', target_name: null, title: 'group.member.joined', input_preview: 'Member joined', status_group: 'ignored', status: 'ignored', }; await page.route('**/api/v1/monitoring/executions?*', (route) => route.fulfill({ json: { code: 0, data: { ...buildExecutionList(monitoring), items: [event] }, }, }), ); await page.route('**/api/v1/monitoring/executions/event/*?*', (route) => route.fulfill({ json: { code: 0, data: { source: 'event', row: event, pages: { inputs: pageOf([ { id: 'event-only', event_type: 'group.member.joined', content: { member_id: 'member-42' }, metadata: { routes: [ { status: 'not_matched', reason: 'No matching route' }, ], }, }, ]), outputs: pageOf([]), deliveries: pageOf([]), }, }, }, }), ); await page.goto('/home/monitoring'); await page.getByRole('button').filter({ hasText: 'Member joined' }).click(); const sheet = page.getByRole('dialog'); await expect( sheet.getByRole('heading', { name: 'No processor run' }), ).toBeVisible(); await sheet .locator('summary') .filter({ hasText: /^Details$/ }) .first() .click(); await expect(sheet.getByText(/member-42/)).toBeVisible(); await expect(sheet.getByText(/No matching route/)).toBeVisible(); await expect(sheet.getByText('Delivery records (0)')).toBeVisible(); }); test('filters all three processor kinds with the sidebar icons', async ({ page, }) => { await installLangBotApiMocks(page, { authenticated: true }); const processors = [ { uuid: 'agent-one', name: 'Agent One', kind: 'agent', emoji: '🤖' }, { uuid: 'pipeline-one', name: 'Pipeline One', kind: 'pipeline', emoji: '⚙️', }, { uuid: 'event-one', name: 'Event One', kind: 'event_processor', emoji: '🧩', }, ]; await page.route('**/api/v1/agents', (route) => route.fulfill({ json: { code: 0, data: { agents: processors } } }), ); await page.goto('/home/monitoring'); const filter = page.locator('#monitoring-filter-processor'); await filter.click(); const popup = page.getByRole('dialog'); for (const processor of processors) { const option = popup.getByRole('checkbox', { name: processor.name, exact: true, }); await expect( option.locator('..').getByText(processor.emoji, { exact: true }), ).toBeVisible(); const request = page.waitForRequest((req) => { const url = new URL(req.url()); return ( url.pathname.endsWith('/monitoring/executions') && url.searchParams.getAll('pipelineId').includes(processor.uuid) ); }); await option.check(); await request; await expect(option).toBeChecked(); } await expect(filter).toContainText('3'); await popup .getByRole('button', { name: 'All Processors', exact: true }) .click(); await expect(popup.getByRole('checkbox', { checked: true })).toHaveCount(0); await expect(filter).toContainText('All Processors'); }); test('shares mode and status filters with dashboard charts and records', async ({ page, }) => { await installLangBotApiMocks(page, { authenticated: true }); await page.goto('/home/monitoring'); for (const [id, option, parameter, value] of [ ['monitoring-filter-mode', 'Debug only', 'mode', 'debug'], ['monitoring-filter-status', 'Failed', 'status', 'failed'], ]) { await page.locator(`#${id}`).click(); const requests = ['/monitoring/data', '/monitoring/executions'].map( (path) => page.waitForRequest((req) => { const url = new URL(req.url()); return ( url.pathname.endsWith(path) && url.searchParams.get(parameter) === value ); }), ); await page.getByRole('option', { name: option, exact: true }).click(); await Promise.all(requests); } }); test('restores filter trigger focus when closing with Escape', async ({ page, }) => { await installLangBotApiMocks(page, { authenticated: true }); await page.goto('/home/monitoring'); for (const id of ['bot', 'mode', 'status', 'time']) { const trigger = page.locator(`#monitoring-filter-${id}`); await trigger.click(); await expect(page.getByRole('listbox')).toBeVisible(); await page.keyboard.press('Escape'); await expect(trigger).toBeFocused(); await expect(page.getByRole('listbox')).toHaveCount(0); } }); test('keeps the custom range dialog focused when its select closes', async ({ page, }) => { await installLangBotApiMocks(page, { authenticated: true }); await page.goto('/home/monitoring'); await page.locator('#monitoring-filter-time').click(); await page .getByRole('option', { name: 'Custom time range', exact: true }) .click(); const dialog = page.getByRole('dialog', { name: 'Custom time range' }); await expect(dialog).toBeVisible(); await expect(dialog.locator('input').first()).toBeFocused(); }); test('remembers all filters on reload and persists resetting them', async ({ page, }) => { await installLangBotApiMocks(page, { authenticated: true }); await page.route('**/api/v1/platform/bots', (route) => route.fulfill({ json: { code: 0, data: { bots: [{ uuid: 'bot-one', name: 'Saved Bot', adapter: 'aiocqhttp' }], }, }, }), ); await page.route('**/api/v1/agents', (route) => route.fulfill({ json: { code: 0, data: { agents: [ { uuid: 'processor-one', name: 'Saved Processor', kind: 'agent', emoji: '🤖', }, ], }, }, }), ); await page.goto('/home/monitoring'); const selections = [ ['bot', 'Saved Bot'], ['processor', 'Saved Processor'], ['mode', 'Debug only'], ['status', 'Failed'], ['time', 'Last 7 days'], ]; for (const [id, label] of selections) { await page.locator(`#monitoring-filter-${id}`).click(); if (id === 'processor') { const checkbox = page.getByRole('dialog').getByRole('checkbox', { name: label, exact: true, }); await checkbox.check(); await expect(checkbox).toBeChecked(); await page.keyboard.press('Escape'); await expect(page.getByRole('dialog')).toHaveCount(0); } else { await page.getByRole('option', { name: label, exact: true }).click(); await expect(page.getByRole('listbox')).toHaveCount(0); } // The previous popup must finish restoring focus before opening another. await expect(page.locator(`#monitoring-filter-${id}`)).toBeFocused(); } await page.reload(); for (const [id, label] of selections) { if (id === 'processor') { await page.locator('#monitoring-filter-processor').click(); await expect( page.getByRole('checkbox', { name: label, exact: true }), ).toBeChecked(); await page.keyboard.press('Escape'); } else await expect(page.locator(`#monitoring-filter-${id}`)).toContainText( label, ); } await page .getByRole('button', { name: 'Reset Filters', exact: true }) .click(); await page.reload(); for (const [id, label] of [ ['bot', 'All Bots'], ['processor', 'All Processors'], ['mode', 'Live + debug'], ['status', 'All statuses'], ['time', 'Last 24 hours'], ]) { await expect(page.locator(`#monitoring-filter-${id}`)).toContainText(label); } }); test('reset removes linked resource filters from the URL', async ({ page }) => { await installLangBotApiMocks(page, { authenticated: true }); await page.goto( '/home/monitoring?botId=linked-bot&pipelineId=linked-processor', ); await page .getByRole('button', { name: 'Reset Filters', exact: true }) .click(); await expect(page).toHaveURL(/\/home\/monitoring$/); await page.reload(); await expect(page.locator('#monitoring-filter-bot')).toContainText( 'All Bots', ); await expect(page.locator('#monitoring-filter-processor')).toContainText( 'All Processors', ); });