mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-16 14:57:15 +00:00
fix(monitoring): restore Cloud messages and bot-scoped sessions (#2526)
* fix(monitoring): restore Cloud message persistence and bot-scoped sessions * fix(migrations): support partial monitoring schemas and align regression fixtures * test(migrations): complete raw bot session fixture values --------- Co-authored-by: dadachann <185672915+dadachann@users.noreply.github.com>
This commit is contained in:
@@ -66,7 +66,381 @@ function toolCall(
|
||||
};
|
||||
}
|
||||
|
||||
test.describe('bot session request recovery', () => {
|
||||
for (const failure of [
|
||||
'initial list',
|
||||
'list page',
|
||||
'session switch',
|
||||
'message page',
|
||||
'analysis',
|
||||
]) {
|
||||
test(`${failure} failure is visible and retry recovers`, async ({
|
||||
page,
|
||||
}) => {
|
||||
await installLangBotApiMocks(page, { authenticated: true });
|
||||
let failing = true;
|
||||
await page.route('**/api/v1/monitoring/**', async (route) => {
|
||||
const url = new URL(route.request().url());
|
||||
const offset = Number(url.searchParams.get('offset') || 0);
|
||||
const second = url.searchParams.get('sessionId') === 'person-second';
|
||||
const list = url.pathname.endsWith('/sessions');
|
||||
const message = url.pathname.endsWith('/messages');
|
||||
const analysis = url.pathname.endsWith('/analysis');
|
||||
if (!list && !message && !analysis) return route.fallback();
|
||||
const fail =
|
||||
failing &&
|
||||
((list && failure === 'initial list') ||
|
||||
(list && failure === 'list page' && offset > 0) ||
|
||||
(message && failure === 'session switch' && second) ||
|
||||
(message && failure === 'message page' && offset > 0) ||
|
||||
(analysis && failure === 'analysis'));
|
||||
if (fail)
|
||||
return route.fulfill({
|
||||
status: 500,
|
||||
json: { code: 500, message: 'fixture failure' },
|
||||
});
|
||||
const data = list
|
||||
? {
|
||||
sessions: [sessionId, 'person-second'].map((id, i) => ({
|
||||
session_id: id,
|
||||
bot_id: botId,
|
||||
bot_name: botName,
|
||||
pipeline_id: pipelineId,
|
||||
pipeline_name: pipelineName,
|
||||
message_count: 51,
|
||||
start_time: at(0),
|
||||
last_activity: at(4),
|
||||
is_active: true,
|
||||
user_name: offset ? `Page two ${i}` : `Recovery user ${i}`,
|
||||
})),
|
||||
total: 21,
|
||||
}
|
||||
: message
|
||||
? {
|
||||
messages: [
|
||||
sessionMessage(
|
||||
'recovery-message',
|
||||
'user',
|
||||
0,
|
||||
second
|
||||
? 'Second session message'
|
||||
: offset
|
||||
? 'Second page message'
|
||||
: 'Successful message',
|
||||
),
|
||||
],
|
||||
total: 51,
|
||||
}
|
||||
: {
|
||||
tool_calls: [
|
||||
toolCall('recovery-tool', 1, 'recovered_tool', 40),
|
||||
],
|
||||
};
|
||||
return route.fulfill({ json: { code: 0, data } });
|
||||
});
|
||||
await page.goto(`/home/bots?id=${botId}`);
|
||||
await page.getByRole('tab', { name: /Sessions/ }).click();
|
||||
if (failure === 'list page') {
|
||||
await page.getByRole('button', { name: 'Next', exact: true }).click();
|
||||
} else if (failure !== 'initial list') {
|
||||
await page.getByRole('button', { name: /Recovery user 0/ }).click();
|
||||
if (failure !== 'analysis') {
|
||||
await expect(
|
||||
page.getByText('Successful message', { exact: true }),
|
||||
).toBeVisible();
|
||||
if (failure === 'session switch')
|
||||
await page.getByRole('button', { name: /Recovery user 1/ }).click();
|
||||
else
|
||||
await page
|
||||
.getByRole('button', { name: 'Next', exact: true })
|
||||
.last()
|
||||
.click();
|
||||
}
|
||||
}
|
||||
await expect(page.getByRole('alert')).toBeVisible();
|
||||
await expect(
|
||||
page.getByText('No sessions found', { exact: true }),
|
||||
).toHaveCount(0);
|
||||
if (failure === 'analysis') {
|
||||
await expect(page.getByRole('alert')).toContainText(/Tool/i);
|
||||
await expect(
|
||||
page.getByText('Successful message', { exact: true }),
|
||||
).toBeVisible();
|
||||
} else {
|
||||
await expect(
|
||||
page.getByText('Successful message', { exact: true }),
|
||||
).toHaveCount(0);
|
||||
}
|
||||
if (failure === 'list page')
|
||||
await expect(
|
||||
page.getByRole('button', { name: /Recovery user 0/ }),
|
||||
).toHaveCount(0);
|
||||
failing = false;
|
||||
await page
|
||||
.getByRole('alert')
|
||||
.getByRole('button', { name: 'Retry', exact: true })
|
||||
.click();
|
||||
await expect(page.getByRole('alert')).toHaveCount(0);
|
||||
if (failure === 'initial list' || failure === 'list page') {
|
||||
await expect(
|
||||
page.getByRole('button', {
|
||||
name: failure === 'list page' ? /Page two 0/ : /Recovery user 0/,
|
||||
}),
|
||||
).toBeVisible();
|
||||
} else {
|
||||
await expect(
|
||||
page.getByText(
|
||||
failure === 'session switch'
|
||||
? 'Second session message'
|
||||
: failure === 'message page'
|
||||
? 'Second page message'
|
||||
: 'Successful message',
|
||||
{ exact: true },
|
||||
),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText('recovered_tool', { exact: true }),
|
||||
).toBeVisible();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test.describe('bot session request races', () => {
|
||||
for (const kind of ['messages', 'analysis', 'sessions']) {
|
||||
for (const status of [200, 500]) {
|
||||
test(`ignores stale ${kind} ${status} after switching`, async ({
|
||||
page,
|
||||
}) => {
|
||||
await installLangBotApiMocks(page, { authenticated: true });
|
||||
let release!: () => void;
|
||||
const gate = new Promise<void>((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
let held = false;
|
||||
let released = false;
|
||||
await page.route('**/api/v1/monitoring/**', async (route) => {
|
||||
const url = new URL(route.request().url());
|
||||
const list = url.pathname.endsWith('/sessions');
|
||||
const message = url.pathname.endsWith('/messages');
|
||||
const analysis = url.pathname.endsWith('/analysis');
|
||||
if (!list && !message && !analysis) return route.fallback();
|
||||
const old =
|
||||
kind === 'sessions'
|
||||
? url.searchParams.get('userQuery') === 'old'
|
||||
: message
|
||||
? url.searchParams.get('sessionId') === sessionId
|
||||
: url.pathname.includes(sessionId);
|
||||
const isHeld = old && url.pathname.endsWith(`/${kind}`);
|
||||
if (isHeld) {
|
||||
held = true;
|
||||
await gate;
|
||||
if (status === 500) {
|
||||
await route.fulfill({ status: 500, json: { code: 500 } });
|
||||
released = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
const data = list
|
||||
? {
|
||||
sessions: [sessionId, 'person-new'].map((id, i) => ({
|
||||
session_id: id,
|
||||
bot_id: botId,
|
||||
bot_name: botName,
|
||||
pipeline_id: pipelineId,
|
||||
pipeline_name: pipelineName,
|
||||
message_count: 1,
|
||||
start_time: at(0),
|
||||
last_activity: at(4),
|
||||
is_active: true,
|
||||
user_name: isHeld ? 'Stale list' : `Race user ${i}`,
|
||||
})),
|
||||
total: 2,
|
||||
}
|
||||
: message
|
||||
? {
|
||||
messages: [
|
||||
sessionMessage(
|
||||
'race-message',
|
||||
'user',
|
||||
0,
|
||||
old ? 'Old message' : 'Current message',
|
||||
),
|
||||
],
|
||||
total: 1,
|
||||
}
|
||||
: {
|
||||
tool_calls: [
|
||||
toolCall(
|
||||
'race-tool',
|
||||
1,
|
||||
old ? 'old_tool' : 'current_tool',
|
||||
40,
|
||||
),
|
||||
],
|
||||
};
|
||||
await route.fulfill({ json: { code: 0, data } });
|
||||
if (isHeld) released = true;
|
||||
});
|
||||
await page.goto(`/home/bots?id=${botId}`);
|
||||
await page.getByRole('tab', { name: /Sessions/ }).click();
|
||||
if (kind === 'sessions') {
|
||||
await page
|
||||
.getByRole('textbox', { name: 'User ID or name' })
|
||||
.fill('old');
|
||||
await page
|
||||
.getByRole('textbox', { name: 'User ID or name' })
|
||||
.press('Enter');
|
||||
} else await page.getByRole('button', { name: /Race user 0/ }).click();
|
||||
await expect.poll(() => held).toBe(true);
|
||||
if (kind === 'sessions') {
|
||||
await page
|
||||
.getByRole('textbox', { name: 'User ID or name' })
|
||||
.fill('new');
|
||||
await page
|
||||
.getByRole('textbox', { name: 'User ID or name' })
|
||||
.press('Enter');
|
||||
await expect(
|
||||
page.getByRole('button', { name: /Race user 0/ }),
|
||||
).toBeVisible();
|
||||
} else {
|
||||
await page.getByRole('button', { name: /Race user 1/ }).click();
|
||||
await expect(
|
||||
page.getByText('Current message', { exact: true }),
|
||||
).toBeVisible();
|
||||
}
|
||||
release();
|
||||
await expect.poll(() => released).toBe(true);
|
||||
// Allow the released HTTP response and React's queued update to settle.
|
||||
await page.waitForTimeout(200);
|
||||
await expect(page.getByRole('alert')).toHaveCount(0);
|
||||
await expect(page.getByText('Stale list', { exact: true })).toHaveCount(
|
||||
0,
|
||||
);
|
||||
if (kind !== 'sessions') {
|
||||
await expect(
|
||||
page.getByText('Current message', { exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText('current_tool', { exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText('Old message', { exact: true }),
|
||||
).toHaveCount(0);
|
||||
await expect(page.getByText('old_tool', { exact: true })).toHaveCount(
|
||||
0,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test.describe('bot session monitor tool timeline', () => {
|
||||
test('isolates messages and analysis for two bots sharing a raw session id', async ({
|
||||
page,
|
||||
}) => {
|
||||
await installLangBotApiMocks(page, { authenticated: true });
|
||||
const requests: Array<{ bot: string; path: string }> = [];
|
||||
await page.route('**/api/v1/monitoring/**', async (route) => {
|
||||
const url = new URL(route.request().url());
|
||||
const selectedBot = url.searchParams.get('botId');
|
||||
if (
|
||||
!url.pathname.endsWith('/sessions') &&
|
||||
!url.pathname.endsWith('/messages') &&
|
||||
!url.pathname.endsWith('/analysis')
|
||||
) {
|
||||
return route.fallback();
|
||||
}
|
||||
expect(['bot-shared-a', 'bot-shared-b']).toContain(selectedBot);
|
||||
expect(route.request().headers().authorization).toBe(
|
||||
'Bearer playwright-token',
|
||||
);
|
||||
expect(route.request().headers()['x-workspace-id']).toBe(
|
||||
'workspace-playwright',
|
||||
);
|
||||
requests.push({ bot: selectedBot!, path: url.pathname });
|
||||
const shared = {
|
||||
session_id: sessionId,
|
||||
bot_id: selectedBot,
|
||||
bot_name: selectedBot,
|
||||
pipeline_id: pipelineId,
|
||||
pipeline_name: pipelineName,
|
||||
message_count: 1,
|
||||
start_time: at(0),
|
||||
last_activity: at(4),
|
||||
is_active: true,
|
||||
platform: 'person',
|
||||
user_id: 'shared-user',
|
||||
user_name: 'Shared User',
|
||||
};
|
||||
const data = url.pathname.endsWith('/sessions')
|
||||
? { sessions: [shared], total: 1 }
|
||||
: url.pathname.endsWith('/messages')
|
||||
? {
|
||||
messages: [
|
||||
{
|
||||
...sessionMessage(
|
||||
'shared-message',
|
||||
'user',
|
||||
0,
|
||||
`Message for ${selectedBot}`,
|
||||
),
|
||||
bot_id: selectedBot,
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
}
|
||||
: {
|
||||
session_id: sessionId,
|
||||
found: true,
|
||||
tool_calls: [
|
||||
{
|
||||
...toolCall('shared-tool', 1, `tool_${selectedBot}`, 40),
|
||||
bot_id: selectedBot,
|
||||
},
|
||||
],
|
||||
};
|
||||
if (url.pathname.endsWith('/messages'))
|
||||
expect(url.searchParams.get('sessionId')).toBe(sessionId);
|
||||
if (url.pathname.endsWith('/analysis'))
|
||||
expect(decodeURIComponent(url.pathname)).toContain(
|
||||
`/sessions/${sessionId}/analysis`,
|
||||
);
|
||||
await route.fulfill({ json: { code: 0, data } });
|
||||
});
|
||||
for (const selectedBot of ['bot-shared-a', 'bot-shared-b']) {
|
||||
await page.goto(`/home/bots?id=${selectedBot}`);
|
||||
await page.getByRole('tab', { name: /Sessions/ }).click();
|
||||
await page.getByRole('button', { name: /Shared User/ }).click();
|
||||
await expect(
|
||||
page.getByText(`Message for ${selectedBot}`, { exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText(`tool_${selectedBot}`, { exact: true }),
|
||||
).toBeVisible();
|
||||
const otherBot =
|
||||
selectedBot === 'bot-shared-a' ? 'bot-shared-b' : 'bot-shared-a';
|
||||
await expect(
|
||||
page.getByText(`Message for ${otherBot}`, { exact: true }),
|
||||
).toHaveCount(0);
|
||||
await expect(
|
||||
page.getByText(`tool_${otherBot}`, { exact: true }),
|
||||
).toHaveCount(0);
|
||||
expect(
|
||||
requests.some(
|
||||
(request) =>
|
||||
request.bot === selectedBot && request.path.endsWith('/messages'),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
requests.some(
|
||||
(request) =>
|
||||
request.bot === selectedBot && request.path.endsWith('/analysis'),
|
||||
),
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
test('renders tool calls as left-side agent events interleaved with messages', async ({
|
||||
page,
|
||||
}) => {
|
||||
@@ -117,11 +491,41 @@ test.describe('bot session monitor tool timeline', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const monitoringRequests: import('@playwright/test').Request[] = [];
|
||||
page.on('request', (request) => {
|
||||
if (request.url().includes('/api/v1/monitoring/'))
|
||||
monitoringRequests.push(request);
|
||||
});
|
||||
await page.goto(`/home/bots?id=${botId}`);
|
||||
await page.getByRole('tab', { name: /Sessions/ }).click();
|
||||
await page.getByRole('button', { name: /Timeline User/ }).click();
|
||||
|
||||
await expect(page.getByText('Need a timeline check')).toBeVisible();
|
||||
await expect
|
||||
.poll(() =>
|
||||
monitoringRequests.some((request) =>
|
||||
request.url().includes('/analysis?'),
|
||||
),
|
||||
)
|
||||
.toBe(true);
|
||||
for (const request of monitoringRequests.filter((request) =>
|
||||
/\/messages\?|\/analysis\?/.test(request.url()),
|
||||
)) {
|
||||
const url = new URL(request.url());
|
||||
expect(url.searchParams.get('botId')).toBe(botId);
|
||||
if (url.pathname.endsWith('/analysis')) {
|
||||
expect(url.searchParams.get('startTime')).toBe(at(0));
|
||||
expect(url.searchParams.get('endTime')).toBe(at(4));
|
||||
}
|
||||
expect(request.headers().authorization).toBe('Bearer playwright-token');
|
||||
expect(request.headers()['x-workspace-id']).toBe('workspace-playwright');
|
||||
if (url.pathname.endsWith('/messages'))
|
||||
expect(url.searchParams.get('sessionId')).toBe(sessionId);
|
||||
else
|
||||
expect(decodeURIComponent(url.pathname)).toContain(
|
||||
`/sessions/${sessionId}/analysis`,
|
||||
);
|
||||
}
|
||||
await expect(
|
||||
page.getByText('repo_file_read', { exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { expect, test, Route } from '@playwright/test';
|
||||
|
||||
import { installLangBotApiMocks } from './fixtures/langbot-api';
|
||||
import { buildConversationTurns } from '../../src/app/home/monitoring/utils/conversationTurns';
|
||||
@@ -271,7 +271,198 @@ function rawMonitoringData() {
|
||||
};
|
||||
}
|
||||
|
||||
async function respond(route: Route, label: string) {
|
||||
const data = rawMonitoringData();
|
||||
data.messages = [rawMessage(message(label, 'user', 10, label))];
|
||||
await route.fulfill({ json: { code: 0, data } });
|
||||
}
|
||||
|
||||
test.describe('monitoring request contracts', () => {
|
||||
test('shows failures instead of empty success and retries with auth and Workspace headers', async ({
|
||||
page,
|
||||
}) => {
|
||||
await installLangBotApiMocks(page, { authenticated: true });
|
||||
let failing = true;
|
||||
await page.route('**/api/v1/monitoring/data?*', async (route) => {
|
||||
expect(route.request().headers().authorization).toBe(
|
||||
'Bearer playwright-token',
|
||||
);
|
||||
expect(route.request().headers()['x-workspace-id']).toBe(
|
||||
'workspace-playwright',
|
||||
);
|
||||
if (failing)
|
||||
await route.fulfill({
|
||||
status: 500,
|
||||
json: { code: 500, msg: 'fixture database unavailable' },
|
||||
});
|
||||
else await respond(route, 'Recovered monitoring');
|
||||
});
|
||||
await page.goto('/home/monitoring');
|
||||
await expect(page.getByRole('alert')).toContainText(
|
||||
'Failed to load monitoring data',
|
||||
);
|
||||
await expect(page.getByText('No message records')).toHaveCount(0);
|
||||
failing = false;
|
||||
await page.getByRole('button', { name: 'Retry', exact: true }).click();
|
||||
await expect(
|
||||
page.getByText('Recovered monitoring', { exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole('alert')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('latest filter request wins over delayed data and delayed failures', async ({
|
||||
page,
|
||||
}) => {
|
||||
await installLangBotApiMocks(page, { authenticated: true });
|
||||
const pending: Route[] = [];
|
||||
await page.route('**/api/v1/monitoring/data?*', (route) => {
|
||||
pending.push(route);
|
||||
});
|
||||
await page.goto('/home/monitoring');
|
||||
await expect.poll(() => pending.length).toBe(2);
|
||||
await page.getByRole('combobox').last().click();
|
||||
await page.getByRole('option', { name: /Last 7 days/i }).click();
|
||||
await expect.poll(() => pending.length).toBe(3);
|
||||
await respond(pending[2], 'Latest filter data');
|
||||
await expect(
|
||||
page.getByText('Latest filter data', { exact: true }),
|
||||
).toBeVisible();
|
||||
await respond(pending[0], 'Obsolete filter data');
|
||||
await respond(pending[1], 'Obsolete filter data');
|
||||
await page.evaluate(
|
||||
() =>
|
||||
new Promise<void>((resolve) =>
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => resolve())),
|
||||
),
|
||||
);
|
||||
await expect(
|
||||
page.getByText('Latest filter data', { exact: true }),
|
||||
).toBeVisible();
|
||||
await page
|
||||
.getByRole('button', { name: 'Refresh Data', exact: true })
|
||||
.click();
|
||||
await expect.poll(() => pending.length).toBe(4);
|
||||
await expect(
|
||||
page.getByText('Obsolete filter data', { exact: true }),
|
||||
).toHaveCount(0);
|
||||
await page.getByRole('combobox').last().click();
|
||||
await page.getByRole('option', { name: /Last 24 hours/i }).click();
|
||||
await expect.poll(() => pending.length).toBe(5);
|
||||
await respond(pending[4], 'Current result');
|
||||
await expect(
|
||||
page.getByText('Current result', { exact: true }),
|
||||
).toBeVisible();
|
||||
await pending[3].fulfill({
|
||||
status: 500,
|
||||
json: { code: 500, msg: 'old failure' },
|
||||
});
|
||||
await expect(
|
||||
page.getByText('Current result', { exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole('alert')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('uses aggregate traffic rather than the sparse record page and discloses truncation', async ({
|
||||
page,
|
||||
}) => {
|
||||
const data = rawMonitoringData();
|
||||
data.totalCount.messages = 125;
|
||||
await installLangBotApiMocks(page, {
|
||||
authenticated: true,
|
||||
monitoringData: {
|
||||
...data,
|
||||
traffic: {
|
||||
bucket: 'hour',
|
||||
truncated: true,
|
||||
points: [
|
||||
{ timestamp: time(0).toISOString(), messages: 125, llm_calls: 77 },
|
||||
{ timestamp: time(1).toISOString(), messages: 0, llm_calls: 0 },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
await page.goto('/home/monitoring');
|
||||
await expect(
|
||||
page.getByText(
|
||||
'Showing 7 of 125 messages. Conversation traces may be incomplete.',
|
||||
),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText('Traffic range truncated. Choose a shorter time range.'),
|
||||
).toBeVisible();
|
||||
const chart = page.locator('.recharts-wrapper');
|
||||
await expect(chart).toHaveCount(1);
|
||||
await chart
|
||||
.locator(':scope > .recharts-surface')
|
||||
.hover({ position: { x: 70, y: 100 } });
|
||||
await expect(chart.locator('.recharts-tooltip-wrapper')).toContainText(
|
||||
'125',
|
||||
);
|
||||
await expect(chart.locator('.recharts-tooltip-wrapper')).toContainText(
|
||||
'77',
|
||||
);
|
||||
});
|
||||
|
||||
test('does not invent traffic totals when aggregation is unavailable', async ({
|
||||
page,
|
||||
}) => {
|
||||
await installLangBotApiMocks(page, {
|
||||
authenticated: true,
|
||||
monitoringData: rawMonitoringData(),
|
||||
});
|
||||
await page.goto('/home/monitoring');
|
||||
await expect(
|
||||
page.getByText('Traffic aggregation unavailable'),
|
||||
).toBeVisible();
|
||||
await expect(page.locator('.recharts-wrapper')).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('monitoring conversation turn grouping', () => {
|
||||
test('does not reassign explicitly linked activity outside the visible page', () => {
|
||||
const turns = buildConversationTurns(
|
||||
[message('visible', 'user', 10, 'Visible turn')],
|
||||
[llmCall('older-call', 11, 'off-page', 10, 5, 40)],
|
||||
[errorLog('older-error', 11, 'off-page')],
|
||||
[toolCall('older-tool', 11, 'off-page', 'search', 40)],
|
||||
);
|
||||
expect(turns[0].llmCalls).toEqual([]);
|
||||
expect(turns[0].toolCalls).toEqual([]);
|
||||
expect(turns[0].errors).toEqual([]);
|
||||
});
|
||||
|
||||
test('does not assign unlinked activity before the first visible turn', () => {
|
||||
const turns = buildConversationTurns(
|
||||
[message('visible', 'user', 10, 'Visible turn')],
|
||||
[llmCall('older-call', 1, undefined, 10, 5, 40)],
|
||||
[{ ...errorLog('older-error', 1, ''), messageId: undefined }],
|
||||
[toolCall('older-tool', 1, undefined, 'search', 40)],
|
||||
);
|
||||
expect(turns[0].llmCalls).toEqual([]);
|
||||
expect(turns[0].toolCalls).toEqual([]);
|
||||
expect(turns[0].errors).toEqual([]);
|
||||
});
|
||||
|
||||
test('isolates same-session messages and activity by bot identity', () => {
|
||||
const first = message('first', 'user', 1, 'Bot one');
|
||||
const other = {
|
||||
...message('other', 'user', 2, 'Bot two'),
|
||||
botId: 'other-bot',
|
||||
};
|
||||
const reply = message('reply', 'assistant', 3, 'Bot one reply');
|
||||
const turns = buildConversationTurns(
|
||||
[first, other, reply],
|
||||
[llmCall('call', 3, undefined, 10, 5, 40)],
|
||||
[errorLog('error', 3, first.id)],
|
||||
[toolCall('tool', 3, undefined, 'search', 40)],
|
||||
);
|
||||
const own = turns.find((turn) => turn.id === first.id)!;
|
||||
expect(own.assistantMessages.map((item) => item.id)).toEqual(['reply']);
|
||||
expect(own.llmCalls.map((item) => item.id)).toEqual(['call']);
|
||||
expect(own.toolCalls.map((item) => item.id)).toEqual(['tool']);
|
||||
expect(turns.find((turn) => turn.id === other.id)?.totalTokens).toBe(0);
|
||||
});
|
||||
|
||||
test('keeps a single user message as one observable turn', () => {
|
||||
const userOnly = message(
|
||||
'single-user-only',
|
||||
|
||||
Reference in New Issue
Block a user