mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-22 08:25:21 +08:00
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:
@@ -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',
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user