fix(agent): stabilize post-merge release paths

This commit is contained in:
huanghuoguoguo
2026-08-04 17:04:40 +08:00
parent 5c5ed940a9
commit 444024c5b4
19 changed files with 319 additions and 57 deletions
+25 -20
View File
@@ -15,6 +15,11 @@ async function submit(page: Page) {
await page.getByRole('button', { name: /^Submit$/ }).click();
}
async function selectPlaywrightAdapter(page: Page) {
await page.getByRole('combobox').click();
await page.getByRole('option', { name: 'Playwright Adapter' }).click();
}
async function confirmDelete(page: Page) {
await page
.getByRole('dialog')
@@ -83,22 +88,22 @@ test.describe('frontend CRUD smoke flows', () => {
});
await page.goto('/home/bots?id=new');
await selectPlaywrightAdapter(page);
await page.locator('input[name="name"]').fill('Viewer Test Bot');
await page
.locator('input[name="description"]')
.fill('Proves monitoring is ordinary resource visibility.');
await page.getByRole('combobox').click();
await page.getByRole('option', { name: 'Playwright Adapter' }).click();
await submit(page);
await expect(page).toHaveURL(/\/home\/bots\?id=bot-1$/);
await page.goto('/home/pipelines?id=new');
await page.locator('input[name="basic.name"]').fill('Viewer Pipeline');
await page.goto('/home/agents?id=new');
await page.getByRole('button', { name: /^Pipeline/ }).click();
await page.locator('input[name="name"]').fill('Viewer Pipeline');
await page
.locator('input[name="basic.description"]')
.locator('input[name="description"]')
.fill('Viewer monitoring permission regression.');
await submit(page);
await expect(page).toHaveURL(/\/home\/pipelines\?id=pipeline-1$/);
await expect(page).toHaveURL(/\/home\/agents\?id=pipeline-1$/);
workspace.membership.role = 'viewer';
workspace.permissions = ['member.view', 'resource.view', 'workspace.view'];
@@ -110,7 +115,7 @@ test.describe('frontend CRUD smoke flows', () => {
await page.getByRole('tab', { name: 'Logs' }).click();
await expect(page.getByText('No logs yet')).toBeVisible();
await page.goto('/home/pipelines?id=pipeline-1');
await page.goto('/home/agents?id=pipeline-1');
await expect(page.getByRole('tab', { name: 'Dashboard' })).toBeVisible();
await expect(page.getByRole('tab', { name: 'Debug Chat' })).toHaveCount(0);
await expect(page.getByRole('button', { name: /^Save$/ })).toHaveCount(0);
@@ -128,14 +133,13 @@ test.describe('frontend CRUD smoke flows', () => {
await installLangBotApiMocks(page, { authenticated: true });
await page.goto('/home/bots?id=new');
await selectPlaywrightAdapter(page);
await expect(page.locator('input[name="name"]')).toBeVisible();
await page.locator('input[name="name"]').fill('Support Bot');
await page
.locator('input[name="description"]')
.fill('Answers customer support questions.');
await page.getByRole('combobox').click();
await page.getByRole('option', { name: 'Playwright Adapter' }).click();
await submit(page);
await expect(page).toHaveURL(/\/home\/bots\?id=bot-1$/);
@@ -336,9 +340,8 @@ test.describe('bot advanced flows', () => {
// Create a bot first
await page.goto('/home/bots?id=new');
await selectPlaywrightAdapter(page);
await page.locator('input[name="name"]').fill('Toggle Test Bot');
await page.getByRole('combobox').click();
await page.getByRole('option', { name: 'Playwright Adapter' }).click();
await submit(page);
await expect(page).toHaveURL(/\/home\/bots\?id=bot-1$/);
@@ -365,9 +368,8 @@ test.describe('bot advanced flows', () => {
// Create a bot
await page.goto('/home/bots?id=new');
await selectPlaywrightAdapter(page);
await page.locator('input[name="name"]').fill('Tab Test Bot');
await page.getByRole('combobox').click();
await page.getByRole('option', { name: 'Playwright Adapter' }).click();
await submit(page);
// Verify we're on the Configuration tab
@@ -400,12 +402,17 @@ test.describe('bot advanced flows', () => {
// Create a bot
await page.goto('/home/bots?id=new');
await selectPlaywrightAdapter(page);
await page.locator('input[name="name"]').fill('Clean Form Bot');
await page.getByRole('combobox').click();
await page.getByRole('option', { name: 'Playwright Adapter' }).click();
await submit(page);
// After creation, save button should be disabled (form is clean)
// Reload the persisted record so post-create initialization has completed.
await page.reload();
await expect(page.locator('input[name="name"]')).toHaveValue(
'Clean Form Bot',
);
// After loading, save button should be disabled (form is clean)
const saveButton = page.getByRole('button', { name: /^Save$/ });
await expect(saveButton).toBeDisabled();
@@ -424,8 +431,7 @@ test.describe('bot advanced flows', () => {
await page.goto('/home/bots?id=new');
// Select adapter but leave name empty
await page.getByRole('combobox').click();
await page.getByRole('option', { name: 'Playwright Adapter' }).click();
await selectPlaywrightAdapter(page);
await submit(page);
// Should show validation error for name (zod validation)
@@ -690,9 +696,8 @@ test.describe('cross-resource flows', () => {
// Create a bot
await page.goto('/home/bots?id=new');
await selectPlaywrightAdapter(page);
await page.locator('input[name="name"]').fill('Bound Bot');
await page.getByRole('combobox').click();
await page.getByRole('option', { name: 'Playwright Adapter' }).click();
await submit(page);
await expect(page).toHaveURL(/\/home\/bots\?id=bot-1$/);
+124 -16
View File
@@ -17,6 +17,7 @@ interface PipelineMock {
description: string;
config: JsonRecord;
emoji: string;
kind: 'agent' | 'pipeline';
is_default: boolean;
updated_at: string;
}
@@ -62,6 +63,7 @@ interface BotMock {
adapter: string;
adapter_config: JsonRecord;
use_pipeline_uuid?: string;
event_bindings: unknown[];
pipeline_routing_rules: unknown[];
adapter_runtime_values: JsonRecord;
updated_at: string;
@@ -259,29 +261,52 @@ function makePipeline(
data: JsonRecord,
uuid = nextId(state, 'pipeline'),
): PipelineMock {
const kind =
data.kind === 'agent' || uuid.startsWith('agent-') ? 'agent' : 'pipeline';
const runnerId = 'plugin:langbot-team/LocalAgent/default';
const runnerConfig = {
model: {
primary: 'llm-valid',
fallbacks: [],
},
'enable-all-tools': false,
tools: ['unavailable_plugin_tool'],
};
const defaultConfig =
kind === 'agent'
? {
runner: { id: runnerId, 'expire-time': 0 },
runner_config: { [runnerId]: runnerConfig },
}
: {
ai: {
runner: { id: runnerId, 'expire-time': 0 },
runner_config: { [runnerId]: runnerConfig },
},
trigger: {},
safety: {},
output: {},
};
return {
uuid,
name: String(data.name || ''),
description: String(data.description || ''),
config: (data.config as JsonRecord | undefined) || {
ai: {},
trigger: {},
safety: {},
output: {},
},
config: (data.config as JsonRecord | undefined) || defaultConfig,
emoji: String(data.emoji || '⚙️'),
kind,
is_default: false,
updated_at: now(),
};
}
function pipelineMetadata(withRunnerToolSelector = false) {
const runnerId = 'plugin:langbot-team/LocalAgent/default';
return {
configs: [
{
name: 'ai',
label: {
en_US: 'AI Capabilities',
en_US: 'AI Feature',
zh_Hans: 'AI 能力',
},
stages: [
@@ -293,21 +318,20 @@ function pipelineMetadata(withRunnerToolSelector = false) {
},
config: [
{
id: 'runner',
name: 'runner',
name: 'id',
label: {
en_US: 'Runner',
zh_Hans: '运行器',
},
type: 'select',
required: true,
default: 'local-agent',
default: runnerId,
options: [
{
name: 'local-agent',
name: runnerId,
label: {
en_US: 'Built-in Agent',
zh_Hans: '内置 Agent',
en_US: 'Local Agent',
zh_Hans: '本地 Agent',
},
},
],
@@ -315,10 +339,10 @@ function pipelineMetadata(withRunnerToolSelector = false) {
],
},
{
name: 'local-agent',
name: runnerId,
label: {
en_US: 'Built-in Agent',
zh_Hans: '内置 Agent',
en_US: 'Local Agent',
zh_Hans: '本地 Agent',
},
config: [
{
@@ -358,6 +382,25 @@ function pipelineMetadata(withRunnerToolSelector = false) {
};
}
function agentMetadata(withRunnerToolSelector = false) {
const metadata = pipelineMetadata(withRunnerToolSelector);
return {
runner_config: metadata.configs[0],
kinds: [
{
name: 'agent',
supported_event_patterns: ['*'],
message_only: false,
},
{
name: 'pipeline',
supported_event_patterns: ['message.*'],
message_only: true,
},
],
};
}
function providerModelList() {
return {
models: [
@@ -460,6 +503,7 @@ function makeBot(
use_pipeline_uuid: data.use_pipeline_uuid
? String(data.use_pipeline_uuid)
: undefined,
event_bindings: (data.event_bindings as unknown[] | undefined) || [],
pipeline_routing_rules:
(data.pipeline_routing_rules as unknown[] | undefined) || [],
adapter_runtime_values: {
@@ -629,6 +673,70 @@ async function handleBackendApi(route: Route, state: LangBotApiMockState) {
return fulfillJson(route, { models: [] });
}
if (path === '/api/v1/tools') {
return fulfillJson(route, {
tools: [
{
name: 'available_plugin_tool',
human_desc: 'Available plugin tool for frontend E2E tests.',
source: 'plugin',
source_id: 'qa/plugin-smoke',
source_name: 'qa/plugin-smoke',
},
],
});
}
if (path === '/api/v1/agents/_/metadata') {
return fulfillJson(route, agentMetadata(true));
}
if (path === '/api/v1/agents') {
if (method === 'POST') {
const agent = makePipeline(state, parseJsonBody(route));
state.pipelines = [
...state.pipelines.filter((item) => item.uuid !== agent.uuid),
agent,
];
return fulfillJson(route, { uuid: agent.uuid, kind: agent.kind });
}
return fulfillJson(route, { agents: state.pipelines });
}
const agentMatch = path.match(/^\/api\/v1\/agents\/([^/]+)$/);
if (agentMatch) {
const agentId = decodeURIComponent(agentMatch[1]);
if (method === 'PUT') {
const agent = makePipeline(state, parseJsonBody(route), agentId);
state.pipelines = [
...state.pipelines.filter((item) => item.uuid !== agentId),
agent,
];
return fulfillJson(route, {});
}
if (method === 'DELETE') {
state.pipelines = state.pipelines.filter((item) => item.uuid !== agentId);
return fulfillJson(route, {});
}
const agent = state.pipelines.find((item) => item.uuid === agentId);
return fulfillJson(route, {
agent:
agent ||
makePipeline(
state,
{
name: agentId,
kind: agentId.startsWith('agent-') ? 'agent' : 'pipeline',
},
agentId,
),
});
}
if (path === '/api/v1/pipelines/_/metadata') {
return fulfillJson(route, pipelineMetadata(state.withRunnerToolSelector));
}
+3 -3
View File
@@ -9,9 +9,9 @@ const appRoutes = [
bodyText: 'Select a bot from the sidebar',
},
{
path: '/home/pipelines',
heading: 'Pipelines',
bodyText: 'Select a pipeline from the sidebar',
path: '/home/agents',
heading: 'Processors',
bodyText: 'Select an Agent or Pipeline from the sidebar',
},
{
path: '/home/extensions',
@@ -162,7 +162,7 @@ test.describe('pipeline monitoring conversation turns', () => {
monitoringData: monitoringData(),
});
await page.goto(`/home/pipelines?id=${pipeline.id}`);
await page.goto(`/home/agents?id=${pipeline.id}`);
await page.getByRole('tab', { name: 'Dashboard' }).click();
await expect(page.getByText('2 conversation turns')).toBeVisible();
+18 -1
View File
@@ -62,6 +62,15 @@ test('loads a Cloud plugin page through the authenticated asset route', async ({
});
let authenticatedAssetRequests = 0;
let pageSdkRequests = 0;
await page.route('**/api/v1/plugins/_sdk/page-sdk.js', async (route) => {
pageSdkRequests += 1;
await route.fulfill({
status: 200,
contentType: 'application/javascript',
body: 'window.langbot = { onReady(callback) { callback(); } };',
});
});
await page.route(
'**/api/v1/plugins/langbot-team/LangRAG/authenticated-assets/**',
async (route) => {
@@ -69,7 +78,14 @@ test('loads a Cloud plugin page through the authenticated asset route', async ({
await route.fulfill({
status: 200,
contentType: 'text/html',
body: '<!doctype html><html><body><h1>LangRAG Observability</h1></body></html>',
body: `<!doctype html><html><body><main></main>
<script src="/api/v1/plugins/_sdk/page-sdk.js"></script>
<script>
langbot.onReady(() => {
document.querySelector('main').innerHTML = '<h1>LangRAG Observability</h1>';
});
</script>
</body></html>`,
});
},
);
@@ -84,5 +100,6 @@ test('loads a Cloud plugin page through the authenticated asset route', async ({
.getByRole('heading', { name: 'LangRAG Observability' }),
).toBeVisible();
expect(authenticatedAssetRequests).toBeGreaterThan(0);
expect(pageSdkRequests).toBeGreaterThan(0);
await expect(page.getByText('Loading...')).toHaveCount(0);
});
+6 -5
View File
@@ -58,13 +58,14 @@ test('quota-reached create actions are disabled and explain the current limit',
})),
}),
);
await page.route('**/api/v1/pipelines**', (route) =>
await page.route('**/api/v1/agents', (route) =>
fulfill(route, {
pipelines: Array.from({ length: 3 }, (_, index) => ({
agents: Array.from({ length: 3 }, (_, index) => ({
uuid: `pipeline-${index}`,
name: `Pipeline ${index + 1}`,
description: '',
emoji: '⚙️',
kind: 'pipeline',
updated_at: new Date().toISOString(),
})),
}),
@@ -103,8 +104,8 @@ test('quota-reached create actions are disabled and explain the current limit',
name: 'Create Bots',
exact: true,
});
const pipelineCreate = page.getByRole('button', {
name: 'Create Pipelines',
const processorCreate = page.getByRole('button', {
name: 'Create Processors',
exact: true,
});
const knowledgeCreate = page.getByRole('button', {
@@ -117,7 +118,7 @@ test('quota-reached create actions are disabled and explain the current limit',
});
await expect(botCreate).toBeDisabled();
await expect(pipelineCreate).toBeDisabled();
await expect(processorCreate).toBeDisabled();
await expect(knowledgeCreate).toBeDisabled();
await expect(addExtension).toBeEnabled();