mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-16 14:57:15 +00:00
feat(agent-debug): edit event data with compact forms
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { installLangBotApiMocks } from './fixtures/langbot-api';
|
||||
|
||||
test('event data stays in sync across the compact form, JSON and the request', async ({
|
||||
page,
|
||||
}) => {
|
||||
await installLangBotApiMocks(page, {
|
||||
authenticated: true,
|
||||
withAdapterEvents: true,
|
||||
});
|
||||
await page.route(
|
||||
'**/api/v1/agents/agent-workbench/debug/stream',
|
||||
async (route) => {
|
||||
await route.fulfill({
|
||||
contentType: 'application/x-ndjson',
|
||||
body:
|
||||
JSON.stringify({ kind: 'completed', data: { final_text: 'Done' } }) +
|
||||
'\n',
|
||||
});
|
||||
},
|
||||
);
|
||||
await page.goto('/home/agents?id=agent-workbench');
|
||||
const panel = page.getByRole('region', { name: 'Event Debug' });
|
||||
await panel
|
||||
.getByRole('textbox', { name: 'Message content', exact: true })
|
||||
.fill('Hello Alice');
|
||||
await panel.getByRole('button', { name: 'Full JSON' }).click();
|
||||
const json = panel.getByRole('textbox', { name: 'Full JSON' });
|
||||
expect(JSON.parse(await json.inputValue()).text).toBe('Hello Alice');
|
||||
await json.fill('{');
|
||||
await expect(panel.getByRole('button', { name: 'Run test' })).toBeDisabled();
|
||||
await expect(panel.getByRole('alert').last()).toContainText('valid JSON');
|
||||
await json.fill(
|
||||
JSON.stringify({
|
||||
text: 'Edited in JSON',
|
||||
user_name: 'Alice',
|
||||
extra: { keep: true },
|
||||
}),
|
||||
);
|
||||
await panel.getByRole('button', { name: 'Common fields' }).click();
|
||||
await expect(
|
||||
panel.getByRole('textbox', { name: 'Message content', exact: true }),
|
||||
).toHaveValue('Edited in JSON');
|
||||
await panel.getByRole('textbox', { name: 'User name' }).fill('Bob');
|
||||
await panel.getByRole('button', { name: 'Full JSON' }).click();
|
||||
expect(JSON.parse(await json.inputValue())).toMatchObject({
|
||||
user_name: 'Bob',
|
||||
extra: { keep: true },
|
||||
});
|
||||
|
||||
await panel.getByRole('combobox', { name: 'Event type' }).click();
|
||||
await page
|
||||
.getByRole('option')
|
||||
.filter({ hasText: 'group.member_joined' })
|
||||
.click();
|
||||
await expect(
|
||||
panel.getByRole('textbox', { name: 'Message content', exact: true }),
|
||||
).toHaveCount(0);
|
||||
await expect(
|
||||
panel.getByRole('textbox', { name: 'Event summary' }),
|
||||
).toHaveCount(0);
|
||||
await panel.getByRole('textbox', { name: 'Member name' }).fill('Carol');
|
||||
await panel.getByRole('textbox', { name: 'Member ID' }).fill('member-42');
|
||||
await panel.getByRole('textbox', { name: 'Group ID' }).fill('group-42');
|
||||
const request = page.waitForRequest(
|
||||
(req) => req.method() === 'POST' && req.url().endsWith('/debug/stream'),
|
||||
);
|
||||
await panel.getByRole('button', { name: 'Run test' }).click();
|
||||
expect((await request).postDataJSON()).toMatchObject({
|
||||
event_type: 'group.member_joined',
|
||||
text: '',
|
||||
data: {
|
||||
member_name: 'Carol',
|
||||
member_id: 'member-42',
|
||||
group_id: 'group-42',
|
||||
},
|
||||
});
|
||||
await expect(panel.getByText('Done', { exact: true })).toBeVisible();
|
||||
await page.setViewportSize({ width: 1280, height: 650 });
|
||||
await panel.getByRole('button', { name: 'Full JSON' }).click();
|
||||
await panel.locator('summary').filter({ hasText: 'Mock scenario' }).click();
|
||||
await expect(
|
||||
panel.getByRole('button', { name: 'Run test' }),
|
||||
).toBeInViewport();
|
||||
|
||||
await panel.getByRole('combobox', { name: 'Event type' }).click();
|
||||
await page.getByRole('option').filter({ hasText: 'Custom event' }).click();
|
||||
await expect(panel.getByRole('textbox', { name: 'Full JSON' })).toBeVisible();
|
||||
await expect(
|
||||
panel.getByRole('button', { name: 'Common fields' }),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
@@ -36,7 +36,7 @@ test.describe('processor detail workbench', () => {
|
||||
.boundingBox();
|
||||
const eventPickerBox = await debugEventPicker.boundingBox();
|
||||
const conversationInputBox = await debugPanel
|
||||
.getByRole('textbox', { name: 'Conversation input' })
|
||||
.getByRole('textbox', { name: 'Message content' })
|
||||
.boundingBox();
|
||||
expect(transcriptBox).not.toBeNull();
|
||||
expect(eventPickerBox).not.toBeNull();
|
||||
@@ -204,9 +204,7 @@ test.describe('processor detail workbench', () => {
|
||||
.fill('Updated before debugging');
|
||||
await basicInfoDialog.getByRole('button', { name: 'Save' }).click();
|
||||
await expect(basicInfoDialog).toHaveCount(0);
|
||||
await page
|
||||
.getByRole('textbox', { name: 'Conversation input' })
|
||||
.fill('Hello');
|
||||
await page.getByRole('textbox', { name: 'Message content' }).fill('Hello');
|
||||
await page.getByRole('button', { name: 'Run test' }).click();
|
||||
|
||||
await expect(page.getByText('Mock Agent response')).toBeVisible();
|
||||
@@ -253,9 +251,7 @@ test.describe('processor detail workbench', () => {
|
||||
);
|
||||
|
||||
await page.goto('/home/agents?id=agent-workbench');
|
||||
await page
|
||||
.getByRole('textbox', { name: 'Conversation input' })
|
||||
.fill('Hello');
|
||||
await page.getByRole('textbox', { name: 'Message content' }).fill('Hello');
|
||||
await page.getByRole('button', { name: 'Run test' }).click();
|
||||
|
||||
await expect(
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import test from 'node:test';
|
||||
import ts from 'typescript';
|
||||
|
||||
const source = fs.readFileSync(
|
||||
new URL(
|
||||
'../../src/app/home/agents/components/debug-event-data.ts',
|
||||
import.meta.url,
|
||||
),
|
||||
'utf8',
|
||||
);
|
||||
const module = { exports: {} };
|
||||
new Function(
|
||||
'exports',
|
||||
ts.transpileModule(source, {
|
||||
compilerOptions: {
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
target: ts.ScriptTarget.ES2022,
|
||||
},
|
||||
}).outputText,
|
||||
)(module.exports);
|
||||
const {
|
||||
createDebugEventData,
|
||||
parseDebugEventData,
|
||||
debugEventInputText,
|
||||
invalidDebugEventField,
|
||||
} = module.exports;
|
||||
const samples = { user: '测试用户', message: '你好', feedback: '很有帮助' };
|
||||
|
||||
test('message text and common fields belong to the same event data', () => {
|
||||
const data = createDebugEventData('message.received', samples);
|
||||
data.text = 'A different message';
|
||||
data.user_name = 'Alice';
|
||||
data.custom = { nested: true };
|
||||
const restored = parseDebugEventData(JSON.stringify(data));
|
||||
assert.deepEqual(restored, data);
|
||||
assert.equal(
|
||||
debugEventInputText('message.received', restored),
|
||||
'A different message',
|
||||
);
|
||||
assert.equal(restored.user_name, 'Alice');
|
||||
assert.equal(createDebugEventData('message.received', samples).text, '你好');
|
||||
});
|
||||
|
||||
test('non-message events and deleted messages do not require fabricated conversation text', () => {
|
||||
for (const event of [
|
||||
'group.member_left',
|
||||
'friend.request_received',
|
||||
'message.deleted',
|
||||
'message.reaction',
|
||||
]) {
|
||||
const data = createDebugEventData(event, samples);
|
||||
assert.equal(invalidDebugEventField(event, data), undefined);
|
||||
assert.equal(debugEventInputText(event, data), '');
|
||||
assert.equal(data.text, undefined);
|
||||
}
|
||||
});
|
||||
|
||||
test('edited message uses the edited content, not an event description', () => {
|
||||
const data = createDebugEventData('message.edited', samples);
|
||||
data.text = 'Corrected message';
|
||||
assert.equal(
|
||||
debugEventInputText('message.edited', data),
|
||||
'Corrected message',
|
||||
);
|
||||
assert.equal(
|
||||
invalidDebugEventField('message.edited', { ...data, text: ' ' }).key,
|
||||
'text',
|
||||
);
|
||||
});
|
||||
|
||||
test('invalid JSON cannot silently become an empty event', () => {
|
||||
for (const text of ['', '{', 'null', '[]', '"hello"', '12']) {
|
||||
assert.equal(parseDebugEventData(text), null);
|
||||
}
|
||||
const data = { arbitrary: { list: [1, true] } };
|
||||
assert.deepEqual(parseDebugEventData(JSON.stringify(data)), data);
|
||||
assert.equal(invalidDebugEventField('custom.example', data), undefined);
|
||||
assert.equal(invalidDebugEventField('constructor', data), undefined);
|
||||
});
|
||||
|
||||
test('duration and rating remain numbers and reject invalid values', () => {
|
||||
const data = createDebugEventData('bot.muted', samples);
|
||||
assert.equal(data.duration, 60);
|
||||
for (const duration of [-1, 0.5, '60']) {
|
||||
assert.equal(
|
||||
invalidDebugEventField('bot.muted', { ...data, duration }).key,
|
||||
'duration',
|
||||
);
|
||||
}
|
||||
assert.equal(
|
||||
invalidDebugEventField('bot.muted', { ...data, duration: 0 }),
|
||||
undefined,
|
||||
);
|
||||
assert.equal(
|
||||
invalidDebugEventField('feedback.received', { rating: 6 }).key,
|
||||
'rating',
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user