mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-08 10:37:14 +00:00
303 lines
10 KiB
TypeScript
303 lines
10 KiB
TypeScript
import { expect, test } from '@playwright/test';
|
|
import { installLangBotApiMocks } from './fixtures/langbot-api';
|
|
|
|
test('create first, select a plugin in the header, debug beside scrollable logs', async ({
|
|
page,
|
|
}) => {
|
|
await installLangBotApiMocks(page, { authenticated: true });
|
|
const ref = 'event_processor:qa/welcome/default';
|
|
const processor = {
|
|
uuid: 'processor-qa',
|
|
kind: 'event_processor',
|
|
name: 'Welcome processor',
|
|
component_ref: ref,
|
|
supported_event_patterns: ['group.member_joined'],
|
|
config: {
|
|
runner: { id: ref },
|
|
runner_config: { [ref]: { greeting: 'Hello' } },
|
|
},
|
|
};
|
|
const run = {
|
|
run_id: 'run-one',
|
|
status: 'completed',
|
|
status_reason: 'stop',
|
|
created_at: 1788000000,
|
|
started_at_ms: 1788000000000,
|
|
finished_at_ms: 1788000000500,
|
|
metadata: {
|
|
event_type: 'group.member_joined',
|
|
input_event: { member: { id: 'one' } },
|
|
delivery: {
|
|
reply_target: { target_type: 'group', target_id: 'test-group' },
|
|
},
|
|
},
|
|
};
|
|
const cursors: string[] = [];
|
|
const debugRequests: unknown[] = [];
|
|
const operations: string[] = [];
|
|
const creations: unknown[] = [];
|
|
await page.route('**/api/v1/agents**', async (route) => {
|
|
const url = new URL(route.request().url());
|
|
let data: unknown;
|
|
if (
|
|
url.pathname === '/api/v1/agents' &&
|
|
route.request().method() === 'POST'
|
|
) {
|
|
const payload = route.request().postDataJSON();
|
|
creations.push(payload);
|
|
Object.assign(processor, payload, {
|
|
component_ref: null,
|
|
config: {},
|
|
supported_event_patterns: [],
|
|
});
|
|
await route.fulfill({
|
|
json: { code: 0, data: { uuid: processor.uuid, kind: processor.kind } },
|
|
});
|
|
return;
|
|
} else if (url.pathname.endsWith('/debug/stream')) {
|
|
operations.push('debug');
|
|
debugRequests.push(route.request().postDataJSON());
|
|
const events = [
|
|
{
|
|
type: 'processor.log',
|
|
sequence: 1,
|
|
data: { level: 'info', text: 'Debug handler invoked once' },
|
|
},
|
|
{
|
|
type: 'tool.call.started',
|
|
sequence: 2,
|
|
data: { tool_name: 'event_reply', parameters: { text: 'Welcome' } },
|
|
},
|
|
{
|
|
type: 'tool.call.completed',
|
|
sequence: 3,
|
|
data: {
|
|
tool_name: 'event_reply',
|
|
result: { mock: true, ok: true, delivery: 'simulated' },
|
|
},
|
|
},
|
|
{ type: 'run.completed', sequence: 4, data: {} },
|
|
];
|
|
await route.fulfill({
|
|
contentType: 'application/x-ndjson',
|
|
body:
|
|
[
|
|
...events.map((data) => ({ kind: 'result', data })),
|
|
{ kind: 'completed', data: { final_text: '' } },
|
|
]
|
|
.map((frame) => JSON.stringify(frame))
|
|
.join('\n') + '\n',
|
|
});
|
|
return;
|
|
} else if (url.pathname.endsWith('/_/metadata')) {
|
|
data = {
|
|
kinds: [],
|
|
event_processors: [
|
|
{
|
|
id: ref,
|
|
label: { en_US: 'Welcome' },
|
|
supported_event_patterns: ['group.member_joined'],
|
|
config_schema: [
|
|
{
|
|
name: 'greeting',
|
|
type: 'string',
|
|
label: { en_US: 'Greeting' },
|
|
default: 'Hello',
|
|
required: true,
|
|
},
|
|
],
|
|
plugin_author: 'qa',
|
|
plugin_name: 'welcome',
|
|
},
|
|
],
|
|
};
|
|
} else if (url.pathname.endsWith('/runs/run-two/events')) {
|
|
data = {
|
|
run: {
|
|
...run,
|
|
run_id: 'run-two',
|
|
metadata: { event_type: 'group.member_left' },
|
|
},
|
|
items: [
|
|
{
|
|
sequence: 1,
|
|
type: 'processor.log',
|
|
created_at_ms: 1788000000000,
|
|
data: { text: 'Older run selected', level: 'info' },
|
|
},
|
|
],
|
|
has_more: false,
|
|
next_cursor: null,
|
|
};
|
|
} else if (url.pathname.endsWith('/runs/run-one/events')) {
|
|
cursors.push(url.searchParams.get('after_sequence') ?? '');
|
|
data = {
|
|
run,
|
|
items: url.searchParams.has('after_sequence')
|
|
? [
|
|
{
|
|
sequence: 101,
|
|
type: 'processor.log',
|
|
data: { level: 'info', text: 'Final log after pagination' },
|
|
},
|
|
]
|
|
: [
|
|
{
|
|
sequence: 1,
|
|
type: 'processor.log',
|
|
data: { level: 'info', text: 'Member received' },
|
|
created_at_ms: 1788000000000,
|
|
},
|
|
{
|
|
sequence: 2,
|
|
type: 'tool.call.started',
|
|
created_at_ms: 1788000000050,
|
|
data: {
|
|
tool_call_id: 'reply-one',
|
|
tool_name: 'event_reply',
|
|
parameters: { text: 'Welcome' },
|
|
},
|
|
},
|
|
{
|
|
sequence: 3,
|
|
type: 'tool.call.completed',
|
|
created_at_ms: 1788000000200,
|
|
data: {
|
|
tool_call_id: 'reply-one',
|
|
tool_name: 'event_reply',
|
|
result: { ok: true },
|
|
},
|
|
},
|
|
{
|
|
sequence: 100,
|
|
type: 'tool.call.completed',
|
|
data: {
|
|
result: Array.from(
|
|
{ length: 100 },
|
|
(_, i) => `Payload line ${i}`,
|
|
),
|
|
},
|
|
},
|
|
],
|
|
has_more: !url.searchParams.has('after_sequence'),
|
|
next_cursor: url.searchParams.has('after_sequence') ? null : 100,
|
|
};
|
|
} else if (url.pathname.endsWith('/runs')) {
|
|
data = {
|
|
items: debugRequests.length
|
|
? [
|
|
run,
|
|
{
|
|
...run,
|
|
run_id: 'run-two',
|
|
created_at: run.created_at - 60,
|
|
started_at_ms: 1787999940000,
|
|
finished_at_ms: 1787999940200,
|
|
metadata: { event_type: 'group.member_left' },
|
|
},
|
|
]
|
|
: [],
|
|
has_more: false,
|
|
next_cursor: null,
|
|
total: debugRequests.length,
|
|
};
|
|
} else if (url.pathname.endsWith('/processor-qa')) {
|
|
if (route.request().method() === 'PUT') {
|
|
operations.push('save');
|
|
Object.assign(processor, route.request().postDataJSON(), {
|
|
supported_event_patterns: ['group.member_joined'],
|
|
});
|
|
}
|
|
data = { agent: processor };
|
|
} else {
|
|
data = { agents: [processor] };
|
|
}
|
|
await route.fulfill({ json: { code: 0, data } });
|
|
});
|
|
await page.goto('/home/agents?id=new');
|
|
await page.locator('[data-processor-kind="event_processor"]').click();
|
|
await expect(
|
|
page.getByRole('combobox', { name: 'Plugin component' }),
|
|
).toHaveCount(0);
|
|
await page
|
|
.getByRole('textbox', { name: 'Name', exact: false })
|
|
.fill('Welcome processor');
|
|
await page.getByRole('button', { name: 'Submit', exact: true }).click();
|
|
await expect(page).toHaveURL(/id=processor-qa/);
|
|
expect(creations).toHaveLength(1);
|
|
expect(creations[0]).toMatchObject({
|
|
kind: 'event_processor',
|
|
name: 'Welcome processor',
|
|
});
|
|
expect(creations[0]).not.toHaveProperty('component_ref');
|
|
expect(creations[0]).not.toHaveProperty('config');
|
|
const panel = page.getByRole('region', { name: 'Event Debug' });
|
|
const logs = page.getByRole('region', { name: 'Logs and message flow' });
|
|
await expect(panel).toBeVisible();
|
|
await expect(logs).toBeVisible();
|
|
await expect(page.getByRole('tab')).toHaveCount(0);
|
|
await expect(page.getByRole('dialog')).toHaveCount(0);
|
|
await expect(panel.getByRole('button', { name: 'Run test' })).toHaveCount(0);
|
|
await page.getByRole('combobox', { name: 'Plugin component' }).click();
|
|
await page.getByRole('option').filter({ hasText: 'Welcome' }).click();
|
|
await expect(
|
|
panel.getByRole('combobox', { name: 'Event type' }),
|
|
).toContainText('group.member_joined');
|
|
await expect(page.getByText('Greeting', { exact: true })).toHaveCount(0);
|
|
await page
|
|
.getByRole('button', { name: 'Plugin settings', exact: true })
|
|
.click();
|
|
const settings = page.locator('[data-slot="popover-content"]');
|
|
await settings.getByRole('textbox').fill('Welcome');
|
|
await page.keyboard.press('Escape');
|
|
await panel
|
|
.getByRole('textbox', { name: 'Member ID' })
|
|
.fill('debug-member-42');
|
|
await panel.getByRole('button', { name: 'Save and run' }).click();
|
|
await expect(panel.getByText('Debug handler invoked once')).toBeVisible();
|
|
expect(operations).toEqual(['save', 'debug']);
|
|
expect(debugRequests).toHaveLength(1);
|
|
expect(debugRequests[0]).toMatchObject({
|
|
event_type: 'group.member_joined',
|
|
data: { member: { id: 'debug-member-42' } },
|
|
});
|
|
await expect(
|
|
logs.getByText('Member received', { exact: true }),
|
|
).toBeVisible();
|
|
await expect(
|
|
logs.getByText('Payload line 99', { exact: false }),
|
|
).toBeHidden();
|
|
const runList = logs.getByRole('group', { name: 'Runs', exact: true });
|
|
await expect(logs.getByRole('combobox')).toHaveCount(0);
|
|
await expect(runList.getByRole('button')).toHaveCount(2);
|
|
await expect(runList.getByText('500 ms', { exact: true })).toBeVisible();
|
|
await expect(runList.getByText('200 ms', { exact: true })).toBeVisible();
|
|
await expect(runList.getByText('Member received')).toHaveCount(0);
|
|
await runList.getByRole('button', { name: /group.member_left/ }).click();
|
|
await expect(logs.getByText('Older run selected')).toBeVisible();
|
|
await runList.getByRole('button', { name: /group.member_joined/ }).click();
|
|
await expect(
|
|
logs.getByText('Member received', { exact: true }),
|
|
).toBeVisible();
|
|
await logs
|
|
.getByRole('button', { name: 'Action result', exact: true })
|
|
.click();
|
|
await logs.getByRole('button', { name: 'Load more' }).click();
|
|
await logs.getByText('Final log after pagination').scrollIntoViewIfNeeded();
|
|
await expect(logs.getByText('Final log after pagination')).toBeInViewport();
|
|
expect(cursors).toContain('100');
|
|
await expect(panel.getByText('Debug handler invoked once')).toBeVisible();
|
|
const debugBox = await panel.boundingBox();
|
|
const logBox = await logs.boundingBox();
|
|
expect(debugBox!.x).toBeLessThan(logBox!.x);
|
|
await page.setViewportSize({ width: 390, height: 700 });
|
|
await panel
|
|
.getByRole('button', { name: 'Run test' })
|
|
.scrollIntoViewIfNeeded();
|
|
await expect(
|
|
panel.getByRole('button', { name: 'Run test' }),
|
|
).toBeInViewport();
|
|
await logs.getByText('Final log after pagination').scrollIntoViewIfNeeded();
|
|
await expect(logs.getByText('Final log after pagination')).toBeInViewport();
|
|
});
|