mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-17 01:46:07 +00:00
feat(agent-runner): enforce 4.x host-owned execution
This commit is contained in:
@@ -19,6 +19,52 @@ async function confirmDelete(page: Page) {
|
||||
.click();
|
||||
}
|
||||
|
||||
async function installDelayedFirstSave(page: Page, apiPath: string) {
|
||||
const payloads: Record<string, unknown>[] = [];
|
||||
let releaseFirstSave = () => {};
|
||||
const firstSaveGate = new Promise<void>((resolve) => {
|
||||
releaseFirstSave = resolve;
|
||||
});
|
||||
|
||||
await page.route(`**${apiPath}`, async (route) => {
|
||||
if (route.request().method() !== 'PUT') {
|
||||
await route.fallback();
|
||||
return;
|
||||
}
|
||||
|
||||
payloads.push(
|
||||
JSON.parse(route.request().postData() || '{}') as Record<string, unknown>,
|
||||
);
|
||||
if (payloads.length === 1) {
|
||||
await firstSaveGate;
|
||||
}
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
code: 0,
|
||||
message: 'ok',
|
||||
data: {},
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
return { payloads, releaseFirstSave };
|
||||
}
|
||||
|
||||
async function forceFormSubmit(page: Page, formSelector: string) {
|
||||
await page.locator(formSelector).evaluate((form) => {
|
||||
(form as HTMLFormElement).requestSubmit();
|
||||
});
|
||||
await page.evaluate(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
test.describe('frontend CRUD smoke flows', () => {
|
||||
test('creates, edits, and deletes a bot', async ({ page }) => {
|
||||
await installLangBotApiMocks(page, { authenticated: true });
|
||||
@@ -56,16 +102,17 @@ test.describe('frontend CRUD smoke flows', () => {
|
||||
test('creates, edits, and deletes a pipeline', async ({ page }) => {
|
||||
await installLangBotApiMocks(page, { authenticated: true });
|
||||
|
||||
await page.goto('/home/pipelines?id=new');
|
||||
await page.goto('/home/agents?id=new');
|
||||
await page.getByRole('button', { name: /^Pipeline/ }).click();
|
||||
|
||||
await expect(page.locator('input[name="basic.name"]')).toBeVisible();
|
||||
await page.locator('input[name="basic.name"]').fill('Escalation Pipeline');
|
||||
await expect(page.locator('input[name="name"]')).toBeVisible();
|
||||
await page.locator('input[name="name"]').fill('Escalation Pipeline');
|
||||
await page
|
||||
.locator('input[name="basic.description"]')
|
||||
.locator('input[name="description"]')
|
||||
.fill('Routes urgent customer issues.');
|
||||
await submit(page);
|
||||
|
||||
await expect(page).toHaveURL(/\/home\/pipelines\?id=pipeline-1$/);
|
||||
await expect(page).toHaveURL(/\/home\/agents\?id=pipeline-1$/);
|
||||
await page.reload();
|
||||
await expect(page.locator('input[name="basic.name"]')).toHaveValue(
|
||||
'Escalation Pipeline',
|
||||
@@ -82,9 +129,9 @@ test.describe('frontend CRUD smoke flows', () => {
|
||||
await page.getByRole('button', { name: /^Delete$/ }).click();
|
||||
await confirmDelete(page);
|
||||
|
||||
await expect(page).toHaveURL(/\/home\/pipelines$/);
|
||||
await expect(page).toHaveURL(/\/home\/agents$/);
|
||||
await expect(
|
||||
page.getByText('Select a pipeline from the sidebar'),
|
||||
page.getByText('Select an Agent or Pipeline from the sidebar'),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -93,7 +140,7 @@ test.describe('frontend CRUD smoke flows', () => {
|
||||
}) => {
|
||||
await installLangBotApiMocks(page, { authenticated: true });
|
||||
|
||||
await page.goto('/home/pipelines?id=pipeline-ai');
|
||||
await page.goto('/home/agents?id=pipeline-ai');
|
||||
|
||||
await expect(page.locator('input[name="basic.name"]')).toBeVisible();
|
||||
await page.getByRole('button', { name: /^AI$/ }).click();
|
||||
@@ -101,7 +148,7 @@ test.describe('frontend CRUD smoke flows', () => {
|
||||
await expect(page.getByText('Runtime')).toBeVisible();
|
||||
await expect(
|
||||
page.locator('[data-slot="card-title"]').filter({
|
||||
hasText: 'Built-in Agent',
|
||||
hasText: 'Local Agent',
|
||||
}),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
@@ -330,12 +377,48 @@ test.describe('bot advanced flows', () => {
|
||||
});
|
||||
|
||||
test.describe('pipeline advanced flows', () => {
|
||||
test('scopes runner tool catalogs to the edited pipeline', async ({
|
||||
page,
|
||||
}) => {
|
||||
await installLangBotApiMocks(page, {
|
||||
authenticated: true,
|
||||
withRunnerToolSelector: true,
|
||||
});
|
||||
const toolCatalogUrls: URL[] = [];
|
||||
const requestedPaths: string[] = [];
|
||||
page.on('request', (request) => {
|
||||
const url = new URL(request.url());
|
||||
if (url.pathname === '/api/v1/tools') toolCatalogUrls.push(url);
|
||||
requestedPaths.push(url.pathname);
|
||||
});
|
||||
|
||||
await page.goto('/home/agents?id=pipeline-scope');
|
||||
await page.getByRole('button', { name: /^AI$/ }).click();
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Edit tools' }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
toolCatalogUrls.some(
|
||||
(url) => url.searchParams.get('pipeline_uuid') === 'pipeline-scope',
|
||||
),
|
||||
)
|
||||
.toBe(true);
|
||||
await expect
|
||||
.poll(() =>
|
||||
requestedPaths.includes('/api/v1/pipelines/pipeline-scope/extensions'),
|
||||
)
|
||||
.toBe(true);
|
||||
});
|
||||
|
||||
test('switches to monitoring tab from pipeline detail', async ({ page }) => {
|
||||
await installLangBotApiMocks(page, { authenticated: true });
|
||||
|
||||
// Create a pipeline
|
||||
await page.goto('/home/pipelines?id=new');
|
||||
await page.locator('input[name="basic.name"]').fill('Tab Test Pipeline');
|
||||
await page.goto('/home/agents?id=new');
|
||||
await page.getByRole('button', { name: /^Pipeline/ }).click();
|
||||
await page.locator('input[name="name"]').fill('Tab Test Pipeline');
|
||||
await submit(page);
|
||||
|
||||
// Verify we're on the Configuration tab
|
||||
@@ -360,8 +443,9 @@ test.describe('pipeline advanced flows', () => {
|
||||
await installLangBotApiMocks(page, { authenticated: true });
|
||||
|
||||
// Create a pipeline
|
||||
await page.goto('/home/pipelines?id=new');
|
||||
await page.locator('input[name="basic.name"]').fill('Dirty Form Pipeline');
|
||||
await page.goto('/home/agents?id=new');
|
||||
await page.getByRole('button', { name: /^Pipeline/ }).click();
|
||||
await page.locator('input[name="name"]').fill('Dirty Form Pipeline');
|
||||
await submit(page);
|
||||
|
||||
// Wait for the page to fully load and form to reset
|
||||
@@ -385,14 +469,153 @@ test.describe('pipeline advanced flows', () => {
|
||||
}) => {
|
||||
await installLangBotApiMocks(page, { authenticated: true });
|
||||
|
||||
await page.goto('/home/pipelines?id=new');
|
||||
await page.goto('/home/agents?id=new');
|
||||
await page.getByRole('button', { name: /^Pipeline/ }).click();
|
||||
|
||||
// Submit without filling name
|
||||
await submit(page);
|
||||
|
||||
// Should show validation error for name (zod validation)
|
||||
await expect(page.getByText(/cannot be empty/i)).toBeVisible();
|
||||
await expect(page).toHaveURL(/\/home\/pipelines\?id=new$/);
|
||||
await expect(page).toHaveURL(/\/home\/agents\?id=new$/);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('agent runner resource selectors', () => {
|
||||
test('uses the global catalog and preserves temporarily unavailable tools', async ({
|
||||
page,
|
||||
}) => {
|
||||
await installLangBotApiMocks(page, { authenticated: true });
|
||||
const toolCatalogUrls: URL[] = [];
|
||||
const requestedPaths: string[] = [];
|
||||
let savedAgent: Record<string, unknown> | undefined;
|
||||
page.on('request', (request) => {
|
||||
const url = new URL(request.url());
|
||||
requestedPaths.push(url.pathname);
|
||||
if (url.pathname === '/api/v1/tools') toolCatalogUrls.push(url);
|
||||
if (
|
||||
url.pathname === '/api/v1/agents/agent-scope' &&
|
||||
request.method() === 'PUT'
|
||||
) {
|
||||
savedAgent = JSON.parse(request.postData() || '{}') as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto('/home/agents?id=agent-scope');
|
||||
await page.getByRole('button', { name: /^Runner$/ }).click();
|
||||
await page.getByRole('button', { name: 'Edit tools' }).click();
|
||||
|
||||
const dialog = page.getByRole('dialog');
|
||||
await expect(dialog.getByText('available_plugin_tool')).toBeVisible();
|
||||
await dialog
|
||||
.getByRole('checkbox', { name: 'Select available_plugin_tool' })
|
||||
.click();
|
||||
await dialog.getByRole('button', { name: /^Confirm$/ }).click();
|
||||
await save(page);
|
||||
|
||||
await expect.poll(() => savedAgent).toBeTruthy();
|
||||
expect(savedAgent).toMatchObject({
|
||||
config: {
|
||||
runner_config: {
|
||||
'plugin:langbot-team/LocalAgent/default': {
|
||||
tools: ['unavailable_plugin_tool', 'available_plugin_tool'],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(
|
||||
toolCatalogUrls.some((url) => url.searchParams.has('pipeline_uuid')),
|
||||
).toBe(false);
|
||||
expect(requestedPaths).not.toContain(
|
||||
'/api/v1/pipelines/agent-scope/extensions',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('agent and pipeline save concurrency', () => {
|
||||
test('agent save freezes its payload and keeps later edits dirty', async ({
|
||||
page,
|
||||
}) => {
|
||||
await installLangBotApiMocks(page, { authenticated: true });
|
||||
const delayedSave = await installDelayedFirstSave(
|
||||
page,
|
||||
'/api/v1/agents/agent-save-race',
|
||||
);
|
||||
|
||||
await page.goto('/home/agents?id=agent-save-race');
|
||||
const saveButton = page.getByRole('button', { name: /^Save$/ });
|
||||
const nameInput = page.locator('input[name="basic.name"]');
|
||||
const descriptionInput = page.locator('input[name="basic.description"]');
|
||||
await expect(nameInput).toBeVisible();
|
||||
|
||||
await nameInput.fill('Submitted Agent');
|
||||
await saveButton.click();
|
||||
await expect.poll(() => delayedSave.payloads.length).toBe(1);
|
||||
await expect(saveButton).toBeDisabled();
|
||||
|
||||
await descriptionInput.fill('Edited while the agent save is pending');
|
||||
await forceFormSubmit(page, '#agent-form');
|
||||
expect(delayedSave.payloads).toHaveLength(1);
|
||||
await expect(saveButton).toBeDisabled();
|
||||
|
||||
delayedSave.releaseFirstSave();
|
||||
await expect(saveButton).toBeEnabled();
|
||||
expect(delayedSave.payloads[0]).toMatchObject({
|
||||
name: 'Submitted Agent',
|
||||
description: '',
|
||||
});
|
||||
|
||||
await saveButton.click();
|
||||
await expect.poll(() => delayedSave.payloads.length).toBe(2);
|
||||
expect(delayedSave.payloads[1]).toMatchObject({
|
||||
name: 'Submitted Agent',
|
||||
description: 'Edited while the agent save is pending',
|
||||
});
|
||||
await expect(saveButton).toBeDisabled();
|
||||
});
|
||||
|
||||
test('pipeline save freezes its payload and keeps later edits dirty', async ({
|
||||
page,
|
||||
}) => {
|
||||
await installLangBotApiMocks(page, { authenticated: true });
|
||||
const delayedSave = await installDelayedFirstSave(
|
||||
page,
|
||||
'/api/v1/pipelines/pipeline-save-race',
|
||||
);
|
||||
|
||||
await page.goto('/home/agents?id=pipeline-save-race');
|
||||
const saveButton = page.getByRole('button', { name: /^Save$/ });
|
||||
const nameInput = page.locator('input[name="basic.name"]');
|
||||
const descriptionInput = page.locator('input[name="basic.description"]');
|
||||
await expect(nameInput).toBeVisible();
|
||||
|
||||
await nameInput.fill('Submitted Pipeline');
|
||||
await saveButton.click();
|
||||
await expect.poll(() => delayedSave.payloads.length).toBe(1);
|
||||
await expect(saveButton).toBeDisabled();
|
||||
|
||||
await descriptionInput.fill('Edited while the pipeline save is pending');
|
||||
await forceFormSubmit(page, '#pipeline-form');
|
||||
expect(delayedSave.payloads).toHaveLength(1);
|
||||
await expect(saveButton).toBeDisabled();
|
||||
|
||||
delayedSave.releaseFirstSave();
|
||||
await expect(saveButton).toBeEnabled();
|
||||
expect(delayedSave.payloads[0]).toMatchObject({
|
||||
name: 'Submitted Pipeline',
|
||||
description: '',
|
||||
});
|
||||
|
||||
await saveButton.click();
|
||||
await expect.poll(() => delayedSave.payloads.length).toBe(2);
|
||||
expect(delayedSave.payloads[1]).toMatchObject({
|
||||
name: 'Submitted Pipeline',
|
||||
description: 'Edited while the pipeline save is pending',
|
||||
});
|
||||
await expect(saveButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -401,10 +624,11 @@ test.describe('cross-resource flows', () => {
|
||||
await installLangBotApiMocks(page, { authenticated: true });
|
||||
|
||||
// Create a pipeline first
|
||||
await page.goto('/home/pipelines?id=new');
|
||||
await page.locator('input[name="basic.name"]').fill('Production Pipeline');
|
||||
await page.goto('/home/agents?id=new');
|
||||
await page.getByRole('button', { name: /^Pipeline/ }).click();
|
||||
await page.locator('input[name="name"]').fill('Production Pipeline');
|
||||
await submit(page);
|
||||
await expect(page).toHaveURL(/\/home\/pipelines\?id=pipeline-1$/);
|
||||
await expect(page).toHaveURL(/\/home\/agents\?id=pipeline-1$/);
|
||||
|
||||
// Create a bot
|
||||
await page.goto('/home/bots?id=new');
|
||||
@@ -417,17 +641,15 @@ test.describe('cross-resource flows', () => {
|
||||
// Wait for form to fully load
|
||||
await expect(page.locator('input[name="name"]')).toHaveValue('Bound Bot');
|
||||
|
||||
// Find the pipeline select by its label "Bind Pipeline"
|
||||
const pipelineCard = page.getByText('Bind Pipeline').locator('..');
|
||||
await expect(pipelineCard).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Click on the select trigger within the pipeline binding card
|
||||
// The select trigger shows "Select Pipeline" placeholder initially
|
||||
const pipelineSelectTrigger = page.getByText('Select Pipeline').first();
|
||||
await pipelineSelectTrigger.click();
|
||||
await page.getByRole('button', { name: 'Add behavior' }).click();
|
||||
await page.getByRole('menuitem', { name: /^Reply to messages/ }).click();
|
||||
await page
|
||||
.getByRole('combobox')
|
||||
.filter({ hasText: 'Select processor' })
|
||||
.click();
|
||||
|
||||
// Select the pipeline option
|
||||
await page.getByRole('option', { name: 'Production Pipeline' }).click();
|
||||
await page.getByRole('option', { name: /Production Pipeline/ }).click();
|
||||
|
||||
// Save the bot
|
||||
await save(page);
|
||||
@@ -436,9 +658,7 @@ test.describe('cross-resource flows', () => {
|
||||
await page.reload();
|
||||
// The pipeline name should appear in the select trigger (not in sidebar or options)
|
||||
await expect(
|
||||
page
|
||||
.locator('[data-slot="select-trigger"]')
|
||||
.filter({ hasText: 'Production Pipeline' }),
|
||||
page.getByRole('combobox').filter({ hasText: 'Production Pipeline' }),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -451,12 +671,12 @@ test.describe('empty states', () => {
|
||||
await expect(page.getByText('Select a bot from the sidebar')).toBeVisible();
|
||||
});
|
||||
|
||||
test('shows empty state when no pipelines exist', async ({ page }) => {
|
||||
test('shows empty state when no processors exist', async ({ page }) => {
|
||||
await installLangBotApiMocks(page, { authenticated: true });
|
||||
|
||||
await page.goto('/home/pipelines');
|
||||
await page.goto('/home/agents');
|
||||
await expect(
|
||||
page.getByText('Select a pipeline from the sidebar'),
|
||||
page.getByText('Select an Agent or Pipeline from the sidebar'),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
|
||||
@@ -21,6 +21,29 @@ interface PipelineMock {
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface AgentMock {
|
||||
uuid: string;
|
||||
name: string;
|
||||
description: string;
|
||||
emoji: string;
|
||||
kind: 'agent';
|
||||
component_ref: string;
|
||||
config: JsonRecord;
|
||||
enabled: boolean;
|
||||
supported_event_patterns: string[];
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface ToolMock {
|
||||
name: string;
|
||||
description: string;
|
||||
human_desc: string;
|
||||
parameters: JsonRecord;
|
||||
source: 'builtin' | 'plugin' | 'mcp' | 'skill';
|
||||
source_name?: string;
|
||||
source_id?: string;
|
||||
}
|
||||
|
||||
interface KnowledgeBaseMock {
|
||||
uuid: string;
|
||||
name: string;
|
||||
@@ -64,10 +87,12 @@ interface BotMock {
|
||||
use_pipeline_uuid?: string;
|
||||
pipeline_routing_rules: unknown[];
|
||||
adapter_runtime_values: JsonRecord;
|
||||
event_bindings: unknown[];
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface LangBotApiMockState {
|
||||
agents: AgentMock[];
|
||||
bots: BotMock[];
|
||||
counters: Record<string, number>;
|
||||
knowledgeBases: KnowledgeBaseMock[];
|
||||
@@ -78,6 +103,8 @@ interface LangBotApiMockState {
|
||||
sessionAnalyses: Record<string, unknown>;
|
||||
sessionMessages: Record<string, unknown[]>;
|
||||
skills: SkillMock[];
|
||||
tools: ToolMock[];
|
||||
withRunnerToolSelector: boolean;
|
||||
}
|
||||
|
||||
function ok(data: unknown) {
|
||||
@@ -183,7 +210,20 @@ function makePipeline(
|
||||
name: String(data.name || ''),
|
||||
description: String(data.description || ''),
|
||||
config: (data.config as JsonRecord | undefined) || {
|
||||
ai: {},
|
||||
ai: {
|
||||
runner: {
|
||||
id: 'plugin:langbot-team/LocalAgent/default',
|
||||
'expire-time': 0,
|
||||
},
|
||||
runner_config: {
|
||||
'plugin:langbot-team/LocalAgent/default': {
|
||||
model: {
|
||||
primary: 'llm-valid',
|
||||
fallbacks: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
trigger: {},
|
||||
safety: {},
|
||||
output: {},
|
||||
@@ -194,7 +234,41 @@ function makePipeline(
|
||||
};
|
||||
}
|
||||
|
||||
function pipelineMetadata() {
|
||||
function makeAgent(data: JsonRecord, uuid: string): AgentMock {
|
||||
return {
|
||||
uuid,
|
||||
name: String(data.name || uuid),
|
||||
description: String(data.description || ''),
|
||||
emoji: String(data.emoji || '🤖'),
|
||||
kind: 'agent',
|
||||
component_ref: String(
|
||||
data.component_ref || 'plugin:langbot-team/LocalAgent/default',
|
||||
),
|
||||
config: (data.config as JsonRecord | undefined) || {
|
||||
runner: {
|
||||
id: 'plugin:langbot-team/LocalAgent/default',
|
||||
'expire-time': 0,
|
||||
},
|
||||
runner_config: {
|
||||
'plugin:langbot-team/LocalAgent/default': {
|
||||
model: {
|
||||
primary: 'llm-valid',
|
||||
fallbacks: [],
|
||||
},
|
||||
'enable-all-tools': false,
|
||||
tools: ['unavailable_plugin_tool'],
|
||||
},
|
||||
},
|
||||
},
|
||||
enabled: data.enabled !== false,
|
||||
supported_event_patterns: (data.supported_event_patterns as
|
||||
| string[]
|
||||
| undefined) || ['*'],
|
||||
updated_at: now(),
|
||||
};
|
||||
}
|
||||
|
||||
function pipelineMetadata(withRunnerToolSelector = false) {
|
||||
return {
|
||||
configs: [
|
||||
{
|
||||
@@ -212,21 +286,21 @@ function pipelineMetadata() {
|
||||
},
|
||||
config: [
|
||||
{
|
||||
id: 'runner',
|
||||
name: 'runner',
|
||||
id: 'runner.id',
|
||||
name: 'id',
|
||||
label: {
|
||||
en_US: 'Runner',
|
||||
zh_Hans: '运行器',
|
||||
},
|
||||
type: 'select',
|
||||
required: true,
|
||||
default: 'local-agent',
|
||||
default: 'plugin:langbot-team/LocalAgent/default',
|
||||
options: [
|
||||
{
|
||||
name: 'local-agent',
|
||||
name: 'plugin:langbot-team/LocalAgent/default',
|
||||
label: {
|
||||
en_US: 'Built-in Agent',
|
||||
zh_Hans: '内置 Agent',
|
||||
en_US: 'Local Agent',
|
||||
zh_Hans: '本地 Agent',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -234,14 +308,14 @@ function pipelineMetadata() {
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'local-agent',
|
||||
name: 'plugin:langbot-team/LocalAgent/default',
|
||||
label: {
|
||||
en_US: 'Built-in Agent',
|
||||
zh_Hans: '内置 Agent',
|
||||
en_US: 'Local Agent',
|
||||
zh_Hans: '本地 Agent',
|
||||
},
|
||||
config: [
|
||||
{
|
||||
id: 'model',
|
||||
id: 'plugin:langbot-team/LocalAgent/default.model',
|
||||
name: 'model',
|
||||
label: {
|
||||
en_US: 'Model',
|
||||
@@ -254,6 +328,21 @@ function pipelineMetadata() {
|
||||
fallbacks: [],
|
||||
},
|
||||
},
|
||||
...(withRunnerToolSelector
|
||||
? [
|
||||
{
|
||||
id: 'plugin:langbot-team/LocalAgent/default.tools',
|
||||
name: 'tools',
|
||||
label: {
|
||||
en_US: 'Tools',
|
||||
zh_Hans: '工具',
|
||||
},
|
||||
type: 'rich-tools-selector',
|
||||
required: false,
|
||||
default: [],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -262,6 +351,19 @@ function pipelineMetadata() {
|
||||
};
|
||||
}
|
||||
|
||||
function agentMetadata() {
|
||||
return {
|
||||
runner_config: pipelineMetadata(true).configs[0],
|
||||
kinds: [
|
||||
{
|
||||
name: 'agent',
|
||||
supported_event_patterns: ['*'],
|
||||
message_only: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function providerModelList() {
|
||||
return {
|
||||
models: [
|
||||
@@ -366,6 +468,7 @@ function makeBot(
|
||||
: undefined,
|
||||
pipeline_routing_rules:
|
||||
(data.pipeline_routing_rules as unknown[] | undefined) || [],
|
||||
event_bindings: (data.event_bindings as unknown[] | undefined) || [],
|
||||
adapter_runtime_values: {
|
||||
webhook_full_url: `https://playwright.test/bots/${uuid}/webhook`,
|
||||
extra_webhook_full_url: '',
|
||||
@@ -503,8 +606,90 @@ async function handleBackendApi(route: Route, state: LangBotApiMockState) {
|
||||
return fulfillJson(route, { models: [] });
|
||||
}
|
||||
|
||||
if (path === '/api/v1/agents/_/metadata') {
|
||||
return fulfillJson(route, agentMetadata());
|
||||
}
|
||||
|
||||
if (path === '/api/v1/agents') {
|
||||
if (method === 'POST') {
|
||||
const data = parseJsonBody(route);
|
||||
if (data.kind === 'pipeline') {
|
||||
const pipeline = makePipeline(state, data);
|
||||
state.pipelines = [
|
||||
...state.pipelines.filter((item) => item.uuid !== pipeline.uuid),
|
||||
pipeline,
|
||||
];
|
||||
return fulfillJson(route, { uuid: pipeline.uuid, kind: 'pipeline' });
|
||||
}
|
||||
|
||||
const agentId = nextId(state, 'agent');
|
||||
const agent = makeAgent(data, agentId);
|
||||
state.agents = [
|
||||
...state.agents.filter((item) => item.uuid !== agentId),
|
||||
agent,
|
||||
];
|
||||
return fulfillJson(route, { uuid: agentId, kind: 'agent' });
|
||||
}
|
||||
|
||||
return fulfillJson(route, {
|
||||
agents: [
|
||||
...state.agents,
|
||||
...state.pipelines.map((pipeline) => ({
|
||||
...pipeline,
|
||||
kind: 'pipeline',
|
||||
component_ref: 'pipeline',
|
||||
enabled: true,
|
||||
supported_event_patterns: ['message.*'],
|
||||
})),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const agentMatch = path.match(/^\/api\/v1\/agents\/([^/]+)$/);
|
||||
if (agentMatch) {
|
||||
const agentId = decodeURIComponent(agentMatch[1]);
|
||||
|
||||
if (method === 'PUT') {
|
||||
const agent = makeAgent(parseJsonBody(route), agentId);
|
||||
state.agents = [
|
||||
...state.agents.filter((item) => item.uuid !== agentId),
|
||||
agent,
|
||||
];
|
||||
return fulfillJson(route, {});
|
||||
}
|
||||
|
||||
if (method === 'DELETE') {
|
||||
state.agents = state.agents.filter((item) => item.uuid !== agentId);
|
||||
return fulfillJson(route, {});
|
||||
}
|
||||
|
||||
if (agentId.startsWith('pipeline-')) {
|
||||
let pipeline = state.pipelines.find((item) => item.uuid === agentId);
|
||||
if (!pipeline) {
|
||||
pipeline = makePipeline(state, { name: agentId }, agentId);
|
||||
state.pipelines = [...state.pipelines, pipeline];
|
||||
}
|
||||
return fulfillJson(route, {
|
||||
agent: {
|
||||
...pipeline,
|
||||
kind: 'pipeline',
|
||||
component_ref: 'pipeline',
|
||||
enabled: true,
|
||||
supported_event_patterns: ['message.*'],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let agent = state.agents.find((item) => item.uuid === agentId);
|
||||
if (!agent) {
|
||||
agent = makeAgent({ name: agentId }, agentId);
|
||||
state.agents = [...state.agents, agent];
|
||||
}
|
||||
return fulfillJson(route, { agent });
|
||||
}
|
||||
|
||||
if (path === '/api/v1/pipelines/_/metadata') {
|
||||
return fulfillJson(route, pipelineMetadata());
|
||||
return fulfillJson(route, pipelineMetadata(state.withRunnerToolSelector));
|
||||
}
|
||||
|
||||
if (path === '/api/v1/pipelines') {
|
||||
@@ -559,6 +744,8 @@ async function handleBackendApi(route: Route, state: LangBotApiMockState) {
|
||||
available_plugins: [],
|
||||
bound_mcp_servers: [],
|
||||
available_mcp_servers: state.mcpServers,
|
||||
bound_mcp_resources: [],
|
||||
mcp_resource_agent_read_enabled: true,
|
||||
bound_skills: [],
|
||||
available_skills: state.skills,
|
||||
});
|
||||
@@ -624,6 +811,10 @@ async function handleBackendApi(route: Route, state: LangBotApiMockState) {
|
||||
});
|
||||
}
|
||||
|
||||
if (path === '/api/v1/tools') {
|
||||
return fulfillJson(route, { tools: state.tools });
|
||||
}
|
||||
|
||||
if (path === '/api/v1/plugins') {
|
||||
return fulfillJson(route, { plugins: [] });
|
||||
}
|
||||
@@ -951,6 +1142,7 @@ export async function installLangBotApiMocks(
|
||||
sessionAnalyses?: Record<string, unknown>;
|
||||
sessionMessages?: Record<string, unknown[]>;
|
||||
storage?: JsonRecord;
|
||||
withRunnerToolSelector?: boolean;
|
||||
} = {},
|
||||
) {
|
||||
const {
|
||||
@@ -960,8 +1152,10 @@ export async function installLangBotApiMocks(
|
||||
sessionAnalyses,
|
||||
sessionMessages,
|
||||
storage = {},
|
||||
withRunnerToolSelector = false,
|
||||
} = options;
|
||||
const state: LangBotApiMockState = {
|
||||
agents: [],
|
||||
bots: [],
|
||||
counters: {},
|
||||
knowledgeBases: [],
|
||||
@@ -972,6 +1166,18 @@ export async function installLangBotApiMocks(
|
||||
sessionAnalyses: sessionAnalyses || {},
|
||||
sessionMessages: sessionMessages || {},
|
||||
skills: [],
|
||||
tools: [
|
||||
{
|
||||
name: 'available_plugin_tool',
|
||||
description: 'Available plugin tool',
|
||||
human_desc: 'Available plugin tool',
|
||||
parameters: {},
|
||||
source: 'plugin',
|
||||
source_name: 'langbot-app/TestTools',
|
||||
source_id: 'langbot-app/TestTools',
|
||||
},
|
||||
],
|
||||
withRunnerToolSelector,
|
||||
};
|
||||
|
||||
await page.addInitScript(
|
||||
|
||||
Reference in New Issue
Block a user