Merge pull request #2698 from langbot-app/test/quality-session-quota-oracles-20261017

test(web): verify session pagination and Workspace quota behavior
This commit is contained in:
huanghuoguoguo
2026-10-17 20:34:41 +08:00
committed by GitHub
5 changed files with 559 additions and 236 deletions
@@ -138,7 +138,8 @@ async def test_same_session_and_resource_ids_do_not_collide(service):
assert (await service.get_message_details(context_a, message_b))['found'] is False
async def test_session_search_matches_user_id_or_name_within_workspace(service):
@pytest.mark.parametrize('bot_ids', [None, ['same-bot']])
async def test_session_search_matches_user_id_or_name_within_workspace(service, bot_ids):
context_a = _context(WORKSPACE_A)
context_b = _context(WORKSPACE_B)
fixtures = [
@@ -159,8 +160,8 @@ async def test_session_search_matches_user_id_or_name_within_workspace(service):
user_name=user_name,
)
by_id, id_total = await service.get_sessions(context_a, user_query='customer-42')
by_name, name_total = await service.get_sessions(context_a, user_query='alice')
by_id, id_total = await service.get_sessions(context_a, bot_ids=bot_ids, user_query='customer-42')
by_name, name_total = await service.get_sessions(context_a, bot_ids=bot_ids, user_query='alice')
assert id_total == 1
assert [session['session_id'] for session in by_id] == ['session-id-match']
@@ -0,0 +1,256 @@
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');
});
@@ -0,0 +1,299 @@
import { expect, test, type Page } from '@playwright/test';
import {
installLangBotApiMocks,
makeWorkspaceEntry,
} from './fixtures/langbot-api';
const unlimited = {
max_bots: -1,
max_pipelines: -1,
max_knowledge_bases: -1,
max_extensions: -1,
};
const resourceLimits = { bots: 2, pipelines: 3, knowledge: 4, extensions: 6 };
const fields = {
bots: 'max_bots',
pipelines: 'max_pipelines',
knowledge: 'max_knowledge_bases',
extensions: 'max_extensions',
} as const;
const labels = {
bots: 'Create Bots',
pipelines: 'Create Processors',
knowledge: 'Create Knowledge',
} as const;
const types = ['plugin', 'mcp', 'skill'] as const;
const marketplaceEntry = (label: string, type: (typeof types)[number]) => ({
id: 1,
author: 'quota-qa',
name: label.toLowerCase().replaceAll(' ', '-'),
label: { en_US: label },
description: { en_US: `${label} description` },
type,
latest_version: '1.0.0',
install_count: 0,
like_count: 0,
components: {},
tags: [],
repository: 'https://example.invalid/extension',
icon: '',
created_at: '',
updated_at: '',
});
async function installQuotaFixtures(
page: Page,
source: 'local' | 'cloud_projection',
limits: typeof unlimited,
) {
const workspace = {
...makeWorkspaceEntry('workspace-playwright', 'Quota workspace', source),
limitation:
source === 'cloud_projection'
? limits
: {
max_bots: 0,
max_pipelines: 0,
max_knowledge_bases: 0,
max_extensions: 0,
},
};
await installLangBotApiMocks(page, {
authenticated: true,
workspaces: [workspace],
});
const installationPosts: string[] = [];
page.on('request', (request) => {
if (
request.method() === 'POST' &&
/\/(plugins|skills|mcp)\//.test(request.url())
)
installationPosts.push(request.url());
});
const fulfill = (
route: Parameters<Parameters<Page['route']>[1]>[0],
data: unknown,
) => route.fulfill({ json: { code: 0, data } });
await page.route('**/api/v1/system/info', (route) =>
fulfill(route, {
debug: false,
version: 'quota-contract',
edition: 'community',
cloud_service_url: 'https://space.langbot.app',
enable_marketplace: true,
allow_modify_login_info: true,
disable_models_service: false,
limitation: source === 'local' ? limits : unlimited,
outbound_ips: [],
wizard_status: 'completed',
wizard_progress: null,
}),
);
await page.route('**/api/v1/platform/bots', (route) =>
fulfill(route, {
bots: Array.from({ length: 2 }, (_, i) => ({
uuid: `quota-bot-${i}`,
name: `Quota Bot ${i}`,
description: '',
adapter: 'aiocqhttp',
enable: true,
updated_at: '2026-03-08T10:00:00Z',
})),
}),
);
await page.route('**/api/v1/agents', (route) =>
fulfill(route, {
agents: Array.from({ length: 3 }, (_, i) => ({
uuid: `quota-pipeline-${i}`,
name: `Quota Pipeline ${i}`,
description: '',
kind: 'pipeline',
updated_at: '2026-03-08T10:00:00Z',
})),
}),
);
await page.route('**/api/v1/knowledge/bases', (route) =>
fulfill(route, {
bases: Array.from({ length: 4 }, (_, i) => ({
uuid: `quota-kb-${i}`,
name: `Quota Knowledge ${i}`,
description: '',
updated_at: '2026-03-08T10:00:00Z',
})),
}),
);
await page.route('**/api/v1/plugins', (route) =>
fulfill(route, {
plugins: [
{
plugin_id: 'quota-qa/installed-plugin',
install_source: 'marketplace',
debug: false,
manifest: {
manifest: {
metadata: {
author: 'quota-qa',
name: 'installed-plugin',
label: { en_US: 'Installed plugin' },
version: '1.0.0',
},
spec: {},
},
},
},
],
}),
);
await page.route('**/api/v1/mcp/servers', (route) =>
fulfill(route, {
servers: Array.from({ length: 2 }, (_, i) => ({
name: `installed-mcp-${i}`,
enable: true,
runtime_info: { status: 'connected' },
})),
}),
);
await page.route('**/api/v1/skills', (route) =>
fulfill(route, {
skills: Array.from({ length: 3 }, (_, i) => ({
name: `installed-skill-${i}`,
display_name: `Installed skill ${i}`,
description: '',
updated_at: '2026-03-08T10:00:00Z',
})),
}),
);
const grid = types.map((type) => marketplaceEntry(`Grid ${type}`, type));
const recommended = types.map((type) =>
marketplaceEntry(`Recommended ${type}`, type),
);
await page.route('**/api/v1/marketplace/**', async (route) => {
const path = new URL(route.request().url()).pathname;
if (path.endsWith('/recommendation-lists'))
return fulfill(route, {
lists: recommended.map((plugin, i) => ({
uuid: `recommendation-${i}`,
label: { en_US: `${plugin.type} picks` },
sort_order: i,
plugins: [plugin],
})),
});
if (path.endsWith('/extensions/search'))
return fulfill(route, { extensions: grid, total: grid.length });
if (path.endsWith('/search') || path.endsWith('/plugins'))
return fulfill(route, { plugins: grid, total: grid.length });
const plugin = [...grid, ...recommended].find((entry) =>
decodeURIComponent(path).endsWith(`/quota-qa/${entry.name}`),
);
if (plugin) return fulfill(route, { plugin });
return route.fallback();
});
return {
installationPosts,
cards: [...grid, ...recommended].map((entry) => entry.label.en_US),
};
}
for (const source of ['local', 'cloud_projection'] as const) {
for (const resource of Object.keys(
resourceLimits,
) as (keyof typeof resourceLimits)[]) {
test(`${source}: only the ${resource} resource is blocked at its own quota`, async ({
page,
}) => {
const limits = {
...unlimited,
[fields[resource]]: resourceLimits[resource],
};
await installQuotaFixtures(page, source, limits);
await page.goto('/home/bots');
for (const [key, label] of Object.entries(labels)) {
const button = page.getByRole('button', { name: label, exact: true });
if (key !== resource) await expect(button).toBeEnabled();
}
if (resource !== 'extensions') {
await expect(
page.getByRole('button', {
name: labels[resource as keyof typeof labels],
exact: true,
}),
).toBeDisabled();
}
await page
.getByRole('button', { name: 'Add Extension', exact: true })
.click();
const manual = page.getByRole('button', {
name: 'Manual Add',
exact: true,
});
if (resource === 'extensions') {
await expect(manual).toBeDisabled();
await manual.locator('..').focus();
await expect(
page.getByText(
'The Extensions limit (6) for this workspace has been reached. Delete one existing item before creating another.',
{ exact: true },
),
).toBeVisible();
} else await expect(manual).toBeEnabled();
if (resource !== 'extensions') {
const button = page.getByRole('button', {
name: labels[resource as keyof typeof labels],
exact: true,
});
await button.locator('..').focus();
await expect(
page.getByText(
`The ${labels[resource as keyof typeof labels].slice(7)} limit (${resourceLimits[resource]}) for this workspace has been reached. Delete one existing item before creating another.`,
{ exact: true },
),
).toBeVisible();
}
});
}
test(`${source}: every grid, recommended and manual install entry respects the mixed extension quota`, async ({
page,
}) => {
const limits = { ...unlimited, max_extensions: 6 };
const { cards, installationPosts } = await installQuotaFixtures(
page,
source,
limits,
);
await page.goto('/home/add-extension');
await expect(
page.getByRole('button', { name: 'Manual Add', exact: true }),
).toBeDisabled();
for (const name of cards) {
const card = page.getByRole('group', {
name: `Install ${name}`,
exact: true,
});
await expect(card).toHaveAttribute('aria-disabled', 'true');
await card.focus();
await page.keyboard.press('Enter');
await expect(page.getByRole('dialog')).toHaveCount(0);
}
expect(installationPosts).toEqual([]);
// One free slot enables every entry again, including each recommended type.
limits.max_extensions = 7;
await page.reload();
await expect(
page.getByRole('button', { name: 'Manual Add', exact: true }),
).toBeEnabled();
for (const name of cards) {
const card = page.getByRole('button', {
name: `Install ${name}`,
exact: true,
});
await expect(card).toHaveAttribute('aria-disabled', 'false');
await card.click();
await expect(page.getByRole('dialog')).toBeVisible();
await page.keyboard.press('Escape');
await expect(page.getByRole('dialog')).toHaveCount(0);
}
expect(installationPosts).toEqual([]);
});
}
@@ -4,101 +4,8 @@ import path from 'node:path';
import test from 'node:test';
const root = process.cwd();
const quotaPath = path.join(
root,
'src/app/home/components/workspace-quota/useWorkspaceQuotaStatus.ts',
);
const sidebarPath = path.join(
root,
'src/app/home/components/home-sidebar/HomeSidebar.tsx',
);
const tooltipPath = path.join(
root,
'src/app/home/components/workspace-quota/WorkspaceQuotaTooltip.tsx',
);
const baseTooltipPath = path.join(root, 'src/components/ui/tooltip.tsx');
const addExtensionPath = path.join(root, 'src/app/home/add-extension/page.tsx');
const marketPath = path.join(
root,
'src/app/home/plugins/components/plugin-market/PluginMarketComponent.tsx',
);
const marketCardPath = path.join(
root,
'src/app/home/plugins/components/plugin-market/plugin-market-card/PluginMarketCardComponent.tsx',
);
const recommendationPath = path.join(
root,
'src/app/home/plugins/components/plugin-market/RecommendationLists.tsx',
);
const zhPath = path.join(root, 'src/i18n/locales/zh-Hans.ts');
test('workspace quota hook exposes reached states for every creatable resource', () => {
assert.equal(
fs.existsSync(quotaPath),
true,
'workspace quota hook is missing',
);
const source = fs.readFileSync(quotaPath, 'utf8');
for (const token of [
'botsReached',
'pipelinesReached',
'knowledgeBasesReached',
'extensionsReached',
'max_bots',
'max_pipelines',
'max_knowledge_bases',
'max_extensions',
]) {
assert.match(source, new RegExp(token));
}
});
test('sidebar quota-disables create controls and renders a tooltip', () => {
const source = fs.readFileSync(sidebarPath, 'utf8');
const tooltip = fs.readFileSync(tooltipPath, 'utf8');
const baseTooltip = fs.readFileSync(baseTooltipPath, 'utf8');
assert.match(source, /useWorkspaceQuotaStatus/);
assert.match(source, /quota\.disabled/);
assert.match(source, /disabled=\{quota\.disabled\}/);
assert.match(source, /WorkspaceQuotaTooltip/);
assert.match(tooltip, /TooltipContent/);
assert.match(tooltip, /limitation\.createDisabledTooltip/);
assert.match(tooltip, /limitation\.quotaLoadingTooltip/);
assert.match(tooltip, /tabIndex=\{0\}/);
assert.match(tooltip, /max-w-72 text-left/);
assert.doesNotMatch(tooltip, /text-center/);
assert.doesNotMatch(baseTooltip, /text-balance/);
assert.match(source, /config\.id === 'add-extension'/);
assert.doesNotMatch(
source,
/config\.id === 'add-extension'\s*\?\s*quotaStatus\.extensions/,
);
});
test('add-extension page disables all install entry points at the quota', () => {
const page = fs.readFileSync(addExtensionPath, 'utf8');
const market = fs.readFileSync(marketPath, 'utf8');
const card = fs.readFileSync(marketCardPath, 'utf8');
const recommendations = fs.readFileSync(recommendationPath, 'utf8');
assert.match(page, /extensionsReached/);
assert.match(page, /installDisabled=\{extensionsReached\}/);
assert.match(page, /disabled=\{extensionsReached/);
assert.match(page, /limitation\.createDisabledTooltip/);
assert.match(market, /installDisabled/);
assert.match(card, /installDisabled/);
assert.match(card, /disabled=\{installDisabled\}/);
assert.match(card, /TooltipContent/);
assert.match(card, /max-w-72 text-left/);
assert.doesNotMatch(card, /max-w-72 text-center/);
assert.match(recommendations, /installDisabled=\{installDisabled\}/);
assert.match(
recommendations,
/installDisabledTooltip=\{installDisabledTooltip\}/,
);
assert.match(page, /quota=\{extensionQuota\}/);
});
test('quota tooltip copy is localized in Simplified Chinese', () => {
const source = fs.readFileSync(zhPath, 'utf8');
assert.match(source, /createDisabledTooltip/);
@@ -1,140 +0,0 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
const root = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
'../..',
);
const repoRoot = path.resolve(root, '..');
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
const readRepo = (file) => fs.readFileSync(path.join(repoRoot, file), 'utf8');
const includes = (source, token, message) =>
assert.ok(source.includes(token), message);
test('session list request supports a server-side page and operator filters', () => {
const client = read('src/app/infra/http/BackendClient.ts');
includes(client, 'startTime?: string', 'client accepts a start date');
includes(client, 'endTime?: string', 'client accepts an end date');
includes(client, 'userQuery?: string', 'client accepts a user query');
includes(
client,
"queryParams.append('offset', options.offset.toString())",
'client sends the requested session offset',
);
includes(
client,
"queryParams.append('userQuery', options.userQuery)",
'client sends the user query',
);
const monitor = read(
'src/app/home/bots/components/bot-session/BotSessionMonitor.tsx',
);
for (const token of [
'SESSION_PAGE_SIZE',
'sessionTotal',
'sessionPage',
'startDate',
'endDate',
'userQuery',
]) {
includes(monitor, token, `monitor includes ${token}`);
}
});
test('session detail requests and renders a bounded message page', () => {
const monitor = read(
'src/app/home/bots/components/bot-session/BotSessionMonitor.tsx',
);
for (const token of [
'MESSAGE_PAGE_SIZE',
'messageTotal',
'messagePage',
'page * MESSAGE_PAGE_SIZE',
]) {
includes(monitor, token, `message pagination includes ${token}`);
}
});
test('backend filters sessions by user id or user name in the existing endpoint', () => {
const controller = readRepo(
'src/langbot/pkg/api/http/controller/groups/monitoring.py',
);
const service = readRepo('src/langbot/pkg/api/http/service/monitoring.py');
includes(
controller,
"quart.request.args.get('userQuery')",
'route accepts userQuery',
);
includes(controller, 'user_query=user_query', 'route forwards userQuery');
includes(
service,
'user_query: str | None = None',
'service accepts userQuery',
);
includes(
service,
'MonitoringSession.user_id.ilike',
'service searches user ids',
);
includes(
service,
'MonitoringSession.user_name.ilike',
'service searches user names',
);
});
test('changing the session page or filters clears the selected detail', () => {
const monitor = read(
'src/app/home/bots/components/bot-session/BotSessionMonitor.tsx',
);
includes(monitor, 'setSelectedSessionId(null)', 'selection is cleared');
includes(
monitor,
'[appliedUserQuery, botId, endDate, sessionPage, startDate]',
'page and filters invalidate the selected session',
);
});
test('date filters use the operator local calendar day', () => {
const monitor = read(
'src/app/home/bots/components/bot-session/BotSessionMonitor.tsx',
);
includes(
monitor,
'localDateBoundaryToISOString(startDate, false)',
'local start-of-day conversion',
);
includes(
monitor,
'localDateBoundaryToISOString(endDate, true)',
'local end-of-day conversion',
);
});
test('session tool calls are bounded to the visible message page', () => {
const monitor = read(
'src/app/home/bots/components/bot-session/BotSessionMonitor.tsx',
);
includes(monitor, 'startTime: sorted[0]?.timestamp', 'analysis page start');
includes(
monitor,
'endTime: sorted[sorted.length - 1]?.timestamp',
'analysis page end',
);
includes(monitor, 'sessionId, botId, {', 'bot-scoped analysis');
const client = read('src/app/infra/http/BackendClient.ts');
includes(
client,
"queryParams.set('startTime', options.startTime)",
'analysis start query',
);
includes(
client,
"queryParams.set('endTime', options.endTime)",
'analysis end query',
);
});