mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-09 04:40:57 +00:00
fix(agent): stabilize post-merge release paths
This commit is contained in:
@@ -5,6 +5,7 @@ export default defineConfig({
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 1 : 0,
|
||||
workers: process.env.CI ? undefined : 1,
|
||||
reporter: process.env.CI ? [['github'], ['list']] : 'list',
|
||||
use: {
|
||||
baseURL: 'http://127.0.0.1:4173',
|
||||
|
||||
@@ -788,11 +788,40 @@ export class BackendClient extends BaseHttpClient {
|
||||
name: string,
|
||||
filepath: string,
|
||||
): Promise<string> {
|
||||
return this.getAuthenticatedObjectURL(
|
||||
return this.getAuthenticatedPluginPageURL(
|
||||
`/api/v1/plugins/${author}/${name}/authenticated-assets/${filepath}`,
|
||||
);
|
||||
}
|
||||
|
||||
private async getAuthenticatedPluginPageURL(path: string): Promise<string> {
|
||||
const response = await this.instance.get<Blob>(path, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
if (!response.data.type.startsWith('text/html')) {
|
||||
return URL.createObjectURL(response.data);
|
||||
}
|
||||
|
||||
const html = await response.data.text();
|
||||
const pageSdkPattern =
|
||||
/<script\b[^>]*\bsrc=(['"])\/api\/v1\/plugins\/_sdk\/page-sdk\.js\1[^>]*>\s*<\/script>/i;
|
||||
if (!pageSdkPattern.test(html)) {
|
||||
return URL.createObjectURL(response.data);
|
||||
}
|
||||
|
||||
const sdkResponse = await this.instance.get<string>(
|
||||
'/api/v1/plugins/_sdk/page-sdk.js',
|
||||
{ responseType: 'text' },
|
||||
);
|
||||
const inlineSdk = sdkResponse.data.replace(/<\/script/gi, '<\\/script');
|
||||
const hydratedHtml = html.replace(
|
||||
pageSdkPattern,
|
||||
`<script>${inlineSdk}</script>`,
|
||||
);
|
||||
return URL.createObjectURL(
|
||||
new Blob([hydratedHtml], { type: response.data.type }),
|
||||
);
|
||||
}
|
||||
|
||||
public getAuthenticatedPluginIconURL(
|
||||
author: string,
|
||||
name: string,
|
||||
|
||||
@@ -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$/);
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user