diff --git a/tests/unit_tests/api/service/test_monitoring_tenancy.py b/tests/unit_tests/api/service/test_monitoring_tenancy.py index e745757c1..97280e15e 100644 --- a/tests/unit_tests/api/service/test_monitoring_tenancy.py +++ b/tests/unit_tests/api/service/test_monitoring_tenancy.py @@ -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'] diff --git a/web/tests/e2e/bot-session-pagination-contract.spec.ts b/web/tests/e2e/bot-session-pagination-contract.spec.ts new file mode 100644 index 000000000..61d6e9c04 --- /dev/null +++ b/web/tests/e2e/bot-session-pagination-contract.spec.ts @@ -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; + headers: Record; + }[] = []; + 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'); +}); diff --git a/web/tests/e2e/workspace-quota-contract.spec.ts b/web/tests/e2e/workspace-quota-contract.spec.ts new file mode 100644 index 000000000..036513149 --- /dev/null +++ b/web/tests/e2e/workspace-quota-contract.spec.ts @@ -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[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([]); + }); +} diff --git a/web/tests/unit/quota-create-actions.test.mjs b/web/tests/unit/quota-create-actions.test.mjs index 8bea7a364..d496cf65c 100644 --- a/web/tests/unit/quota-create-actions.test.mjs +++ b/web/tests/unit/quota-create-actions.test.mjs @@ -4,101 +4,9 @@ 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('extension confirmation checks fail closed and enter an in-flight state first', () => { const page = fs.readFileSync(addExtensionPath, 'utf8'); diff --git a/web/tests/unit/session-monitor-pagination.test.mjs b/web/tests/unit/session-monitor-pagination.test.mjs index 38b09f306..d78fab5e9 100644 --- a/web/tests/unit/session-monitor-pagination.test.mjs +++ b/web/tests/unit/session-monitor-pagination.test.mjs @@ -8,85 +8,10 @@ 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('stale session and message page responses cannot overwrite the latest page', () => { const monitor = read( 'src/app/home/bots/components/bot-session/BotSessionMonitor.tsx', @@ -101,55 +26,3 @@ test('stale session and message page responses cannot overwrite the latest page' includes(monitor, token, `stale response guard includes ${token}`); } }); - -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', - ); -});