mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-22 16:35:20 +08:00
257 lines
8.4 KiB
TypeScript
257 lines
8.4 KiB
TypeScript
import { expect, test, type Page } from '@playwright/test';
|
|
import { installLangBotApiMocks } from './fixtures/langbot-api';
|
|
|
|
const botId = 'bot-tool-timeline';
|
|
const timestamp = (index: number) =>
|
|
new Date(Date.UTC(2026, 2, 8, 10, index)).toISOString();
|
|
const message = (index: number) => ({
|
|
id: `page-message-${index}`,
|
|
timestamp: timestamp(index),
|
|
bot_id: botId,
|
|
bot_name: 'Boundary Bot',
|
|
pipeline_id: 'pipeline-tool-timeline',
|
|
pipeline_name: 'Boundary Pipeline',
|
|
message_content: `message row ${index}`,
|
|
session_id: 'person-boundary-0',
|
|
role: 'assistant',
|
|
status: 'success',
|
|
level: 'info',
|
|
platform: 'bot',
|
|
user_id: 'boundary-user',
|
|
user_name: 'Boundary User',
|
|
});
|
|
|
|
async function openMonitor(page: Page) {
|
|
await installLangBotApiMocks(page, { authenticated: true });
|
|
const requests: {
|
|
path: string;
|
|
query: Record<string, string>;
|
|
headers: Record<string, string>;
|
|
}[] = [];
|
|
await page.route('**/api/v1/monitoring/**', async (route) => {
|
|
const url = new URL(route.request().url());
|
|
const path = url.pathname;
|
|
if (!/\/(sessions|messages|analysis)$/.test(path)) return route.fallback();
|
|
requests.push({
|
|
path,
|
|
query: Object.fromEntries(url.searchParams),
|
|
headers: route.request().headers(),
|
|
});
|
|
const offset = Number(url.searchParams.get('offset') ?? 0);
|
|
let data: unknown;
|
|
if (path.endsWith('/sessions')) {
|
|
data = {
|
|
total: 21,
|
|
sessions: [
|
|
{
|
|
session_id: `person-boundary-${offset}`,
|
|
bot_id: botId,
|
|
bot_name: 'Boundary Bot',
|
|
pipeline_id: 'pipeline-tool-timeline',
|
|
pipeline_name: 'Boundary Pipeline',
|
|
user_id: `boundary-${offset}`,
|
|
user_name: offset ? 'Page two User' : 'Boundary User',
|
|
message_count: 52,
|
|
start_time: timestamp(0),
|
|
last_activity: timestamp(51),
|
|
is_active: true,
|
|
},
|
|
],
|
|
};
|
|
} else if (path.endsWith('/messages')) {
|
|
data = {
|
|
total: 52,
|
|
messages: Array.from({ length: 52 }, (_, index) => message(index))
|
|
.slice(offset, offset + 50)
|
|
.reverse(),
|
|
};
|
|
} else {
|
|
const start =
|
|
Date.parse(url.searchParams.get('startTime') ?? '') || -Infinity;
|
|
const end = Date.parse(url.searchParams.get('endTime') ?? '') || Infinity;
|
|
data = {
|
|
found: true,
|
|
tool_calls: [1, 50]
|
|
.filter(
|
|
(index) =>
|
|
Date.parse(timestamp(index)) >= start &&
|
|
Date.parse(timestamp(index)) <= end,
|
|
)
|
|
.map((index) => ({
|
|
id: `tool-${index}`,
|
|
timestamp: timestamp(index),
|
|
tool_name: index === 1 ? 'first_page_tool' : 'second_page_tool',
|
|
tool_source: 'native',
|
|
status: 'success',
|
|
duration: 20,
|
|
bot_id: botId,
|
|
session_id: 'person-boundary-0',
|
|
arguments: '{}',
|
|
result: '{}',
|
|
})),
|
|
};
|
|
}
|
|
await route.fulfill({ json: { code: 0, data } });
|
|
});
|
|
await page.goto(`/home/bots?id=${botId}`);
|
|
await page.getByRole('tab', { name: /Sessions/ }).click();
|
|
await expect(
|
|
page.getByRole('button', { name: /Boundary User/ }),
|
|
).toBeVisible();
|
|
return requests;
|
|
}
|
|
|
|
async function selectSession(page: Page) {
|
|
await page.getByRole('button', { name: /Boundary User/ }).click();
|
|
await expect(page.getByText('message row 0', { exact: true })).toBeVisible();
|
|
}
|
|
|
|
test('session pages send the server offset and reset it when the user filter is applied', async ({
|
|
page,
|
|
}) => {
|
|
const requests = await openMonitor(page);
|
|
await page.getByRole('button', { name: 'Next', exact: true }).click();
|
|
await expect(
|
|
page.getByRole('button', { name: /Page two User/ }),
|
|
).toBeVisible();
|
|
const pageRequest = requests
|
|
.filter((request) => request.path.endsWith('/sessions'))
|
|
.at(-1)!;
|
|
expect(pageRequest.query).toMatchObject({ botId, limit: '20', offset: '20' });
|
|
expect(pageRequest.headers['x-workspace-id']).toBe('workspace-playwright');
|
|
expect(pageRequest.headers.authorization).toBe('Bearer playwright-token');
|
|
await page.getByRole('textbox', { name: 'User ID or name' }).fill('alice');
|
|
await page.getByRole('textbox', { name: 'User ID or name' }).press('Enter');
|
|
await expect
|
|
.poll(
|
|
() =>
|
|
requests.filter((request) => request.path.endsWith('/sessions')).at(-1)
|
|
?.query,
|
|
)
|
|
.toMatchObject({ botId, limit: '20', offset: '0', userQuery: 'alice' });
|
|
await expect(
|
|
page.getByRole('button', { name: /Boundary User/ }),
|
|
).toBeVisible();
|
|
await expect(page.getByRole('button', { name: /Page two User/ })).toHaveCount(
|
|
0,
|
|
);
|
|
});
|
|
|
|
test('message pages replace the visible rows with the requested bounded page', async ({
|
|
page,
|
|
}) => {
|
|
const requests = await openMonitor(page);
|
|
await selectSession(page);
|
|
await expect(page.getByText('message row 49', { exact: true })).toHaveCount(
|
|
1,
|
|
);
|
|
await expect(page.getByText('message row 50', { exact: true })).toHaveCount(
|
|
0,
|
|
);
|
|
await page.getByRole('button', { name: 'Next page', exact: true }).click();
|
|
await expect(page.getByText('message row 50', { exact: true })).toBeVisible();
|
|
await expect(page.getByText('message row 51', { exact: true })).toHaveCount(
|
|
1,
|
|
);
|
|
await expect(page.getByText('message row 0', { exact: true })).toHaveCount(0);
|
|
await expect(page.getByText('message row 49', { exact: true })).toHaveCount(
|
|
0,
|
|
);
|
|
expect(
|
|
requests.filter((request) => request.path.endsWith('/messages')).at(-1)
|
|
?.query,
|
|
).toMatchObject({
|
|
botId,
|
|
limit: '50',
|
|
offset: '50',
|
|
sessionId: 'person-boundary-0',
|
|
});
|
|
});
|
|
|
|
for (const change of ['page', 'user', 'start date', 'end date']) {
|
|
test(`changing ${change} clears the selected session detail`, async ({
|
|
page,
|
|
}) => {
|
|
await openMonitor(page);
|
|
await selectSession(page);
|
|
if (change === 'page')
|
|
await page.getByRole('button', { name: 'Next', exact: true }).click();
|
|
else if (change === 'user') {
|
|
await page
|
|
.getByRole('textbox', { name: 'User ID or name' })
|
|
.fill('alice');
|
|
await page
|
|
.getByRole('textbox', { name: 'User ID or name' })
|
|
.press('Enter');
|
|
} else
|
|
await page
|
|
.getByLabel(change === 'start date' ? 'Start date' : 'End date')
|
|
.fill('2026-03-08');
|
|
await expect(
|
|
page.getByText('Select a session to view messages', { exact: true }),
|
|
).toBeVisible();
|
|
await expect(page.getByText('message row 0', { exact: true })).toHaveCount(
|
|
0,
|
|
);
|
|
});
|
|
}
|
|
|
|
for (const [timezoneId, start, end] of [
|
|
[
|
|
'America/Los_Angeles',
|
|
'2026-03-08T08:00:00.000Z',
|
|
'2026-03-09T06:59:59.999Z',
|
|
],
|
|
['Asia/Shanghai', '2026-03-07T16:00:00.000Z', '2026-03-08T15:59:59.999Z'],
|
|
]) {
|
|
test.describe(timezoneId, () => {
|
|
test.use({ timezoneId });
|
|
test('date filters send the operator local calendar day boundaries', async ({
|
|
page,
|
|
}) => {
|
|
const requests = await openMonitor(page);
|
|
await page.getByLabel('Start date').fill('2026-03-08');
|
|
await page.getByLabel('End date').fill('2026-03-08');
|
|
await expect
|
|
.poll(
|
|
() =>
|
|
requests
|
|
.filter((request) => request.path.endsWith('/sessions'))
|
|
.at(-1)?.query,
|
|
)
|
|
.toMatchObject({ startTime: start, endTime: end, offset: '0' });
|
|
});
|
|
});
|
|
}
|
|
|
|
test('tool analysis follows the visible message page rather than the previous page', async ({
|
|
page,
|
|
}) => {
|
|
const requests = await openMonitor(page);
|
|
await selectSession(page);
|
|
await expect(
|
|
page.getByText('first_page_tool', { exact: true }),
|
|
).toBeVisible();
|
|
await expect(page.getByText('second_page_tool', { exact: true })).toHaveCount(
|
|
0,
|
|
);
|
|
await page.getByRole('button', { name: 'Next page', exact: true }).click();
|
|
await expect(
|
|
page.getByText('second_page_tool', { exact: true }),
|
|
).toBeVisible();
|
|
await expect(page.getByText('first_page_tool', { exact: true })).toHaveCount(
|
|
0,
|
|
);
|
|
const analysis = requests
|
|
.filter((request) => request.path.endsWith('/analysis'))
|
|
.at(-1)!;
|
|
expect(analysis.path).toContain('/sessions/person-boundary-0/analysis');
|
|
expect(analysis.query).toMatchObject({
|
|
botId,
|
|
startTime: timestamp(50),
|
|
endTime: timestamp(51),
|
|
});
|
|
expect(analysis.headers['x-workspace-id']).toBe('workspace-playwright');
|
|
expect(analysis.headers.authorization).toBe('Bearer playwright-token');
|
|
});
|