mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-16 14:57:15 +00:00
fix(web): unify processor creation and entity detail loading states
This commit is contained in:
@@ -1012,6 +1012,7 @@ test.describe('agent runner resource selectors', () => {
|
||||
install_count: 12,
|
||||
latest_version: '1.0.0',
|
||||
components: { Runner: 1 },
|
||||
runner_usages: ['agent'],
|
||||
status: 'live',
|
||||
type: 'plugin',
|
||||
created_at: '2026-01-01T00:00:00Z',
|
||||
@@ -1128,6 +1129,7 @@ test.describe('agent runner resource selectors', () => {
|
||||
install_count: 9,
|
||||
latest_version: '1.0.0',
|
||||
components: { Runner: 1 },
|
||||
runner_usages: ['agent'],
|
||||
status: 'live',
|
||||
type: 'plugin',
|
||||
created_at: '2026-01-01T00:00:00Z',
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
import { expect, test, type Page } from '@playwright/test';
|
||||
import { installLangBotApiMocks } from './fixtures/langbot-api';
|
||||
|
||||
async function setup(page: Page) {
|
||||
await page.routeWebSocket('**/api/v1/pipelines/**/ws/connect**', (ws) => {
|
||||
ws.onMessage((raw) => {
|
||||
if (JSON.parse(String(raw)).type === 'authenticate')
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: 'connected',
|
||||
connection_id: 'loading-test',
|
||||
session_type: 'person',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
await installLangBotApiMocks(page, {
|
||||
authenticated: true,
|
||||
withAdapterEvents: true,
|
||||
withRunnerToolSelector: true,
|
||||
});
|
||||
await page.route('**/api/v1/plugins/qa/loading**', async (route) => {
|
||||
const path = new URL(route.request().url()).pathname;
|
||||
const data = path.endsWith('/config')
|
||||
? { config: {} }
|
||||
: path.endsWith('/readme')
|
||||
? { readme: '# Loaded documentation' }
|
||||
: {
|
||||
plugin: {
|
||||
manifest: {
|
||||
manifest: {
|
||||
metadata: {
|
||||
author: 'qa',
|
||||
name: 'loading',
|
||||
label: { en_US: 'Loading test plugin' },
|
||||
description: { en_US: 'Test' },
|
||||
},
|
||||
spec: { config: [] },
|
||||
},
|
||||
},
|
||||
components: [],
|
||||
},
|
||||
};
|
||||
await route.fulfill({ json: { code: 0, data } });
|
||||
});
|
||||
await page.route('**/api/v1/agents/processor-loading', (route) =>
|
||||
route.fulfill({
|
||||
json: {
|
||||
code: 0,
|
||||
data: {
|
||||
agent: {
|
||||
uuid: 'processor-loading',
|
||||
kind: 'event_processor',
|
||||
name: 'Loading test processor',
|
||||
config: {},
|
||||
supported_event_patterns: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
await page.route('**/api/v1/agents/processor-loading/runs**', (route) =>
|
||||
route.fulfill({
|
||||
json: {
|
||||
code: 0,
|
||||
data: { items: [], has_more: false, next_cursor: null },
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const cases = [
|
||||
{
|
||||
name: 'plugin documentation',
|
||||
url: '/home/extensions?id=qa/loading',
|
||||
endpoint: '/plugins/qa/loading/readme?**',
|
||||
form: '[data-slot="card-title"]',
|
||||
readyText: 'Loaded documentation',
|
||||
},
|
||||
{
|
||||
name: 'bot adapters',
|
||||
url: '/home/bots?id=bot-loading',
|
||||
endpoint: '/platform/adapters',
|
||||
form: '#bot-form',
|
||||
},
|
||||
{
|
||||
name: 'processor run history',
|
||||
url: '/home/agents?id=processor-loading',
|
||||
endpoint: '/agents/processor-loading/runs',
|
||||
form: '#event-processor-form',
|
||||
},
|
||||
{
|
||||
name: 'skill file list',
|
||||
url: '/home/skills?id=skill-loading',
|
||||
endpoint: '/skills/skill-loading/files?**',
|
||||
form: '#skill-form',
|
||||
readyText: 'SKILL.md',
|
||||
},
|
||||
|
||||
{
|
||||
name: 'bot details',
|
||||
url: '/home/bots?id=bot-loading',
|
||||
endpoint: '/platform/bots/bot-loading',
|
||||
form: '#bot-form',
|
||||
},
|
||||
{
|
||||
name: 'agent details',
|
||||
url: '/home/agents?id=agent-loading',
|
||||
endpoint: '/agents/agent-loading',
|
||||
form: '#agent-form',
|
||||
},
|
||||
{
|
||||
name: 'pipeline details',
|
||||
url: '/home/agents?id=pipeline-loading',
|
||||
endpoint: '/pipelines/pipeline-loading',
|
||||
form: '#pipeline-form',
|
||||
},
|
||||
{
|
||||
name: 'legacy pipeline route',
|
||||
url: '/home/pipelines?id=pipeline-loading',
|
||||
endpoint: '/pipelines/pipeline-loading',
|
||||
form: '#pipeline-form',
|
||||
},
|
||||
{
|
||||
name: 'plugin processor details',
|
||||
url: '/home/agents?id=processor-loading',
|
||||
endpoint: '/agents/processor-loading',
|
||||
form: '#event-processor-form',
|
||||
},
|
||||
{
|
||||
name: 'knowledge base details',
|
||||
url: '/home/knowledge?id=kb-loading',
|
||||
endpoint: '/knowledge/bases/kb-loading',
|
||||
form: '#kb-form',
|
||||
},
|
||||
{
|
||||
name: 'MCP details',
|
||||
url: '/home/mcp?id=mcp-loading',
|
||||
endpoint: '/mcp/servers/mcp-loading',
|
||||
form: '#mcp-form',
|
||||
},
|
||||
{
|
||||
name: 'skill details',
|
||||
url: '/home/skills?id=skill-loading',
|
||||
endpoint: '/skills/skill-loading',
|
||||
form: '#skill-form',
|
||||
},
|
||||
{
|
||||
name: 'plugin details',
|
||||
url: '/home/extensions?id=qa/loading',
|
||||
endpoint: '/plugins/qa/loading',
|
||||
form: '[data-slot="card-title"]',
|
||||
},
|
||||
{
|
||||
name: 'agent metadata after runtime health',
|
||||
url: '/home/agents?id=agent-loading',
|
||||
endpoint: '/agents/_/metadata',
|
||||
form: '#agent-form',
|
||||
},
|
||||
{
|
||||
name: 'pipeline metadata',
|
||||
url: '/home/agents?id=pipeline-loading',
|
||||
endpoint: '/pipelines/_/metadata',
|
||||
form: '#pipeline-form',
|
||||
},
|
||||
{
|
||||
name: 'processor metadata',
|
||||
url: '/home/agents?id=processor-loading',
|
||||
endpoint: '/agents/_/metadata',
|
||||
form: '#event-processor-form',
|
||||
},
|
||||
{
|
||||
name: 'knowledge engines',
|
||||
url: '/home/knowledge?id=kb-loading',
|
||||
endpoint: '/knowledge/engines',
|
||||
form: '#kb-form',
|
||||
},
|
||||
{
|
||||
name: 'plugin configuration',
|
||||
url: '/home/extensions?id=qa/loading',
|
||||
endpoint: '/plugins/qa/loading/config',
|
||||
form: '[data-slot="card-title"]',
|
||||
readyText: 'Plugin Configuration',
|
||||
},
|
||||
];
|
||||
|
||||
for (const scenario of cases) {
|
||||
test(`${scenario.name}: loading until response arrives`, async ({ page }) => {
|
||||
await setup(page);
|
||||
let release!: () => void;
|
||||
const pending = new Promise<void>((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
let requested = false;
|
||||
await page.route(`**/api/v1${scenario.endpoint}`, async (route) => {
|
||||
requested = true;
|
||||
await pending;
|
||||
await route.fallback();
|
||||
});
|
||||
try {
|
||||
await page.goto(scenario.url);
|
||||
await expect.poll(() => requested).toBe(true);
|
||||
await expect(
|
||||
page.getByRole('status').filter({ hasText: 'Loading...' }).first(),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText('No runners are available', { exact: true }),
|
||||
).toHaveCount(0);
|
||||
if (scenario.name === 'agent metadata after runtime health') {
|
||||
await page.screenshot({
|
||||
path: '../../.codex-run/entity-loading-agent.png',
|
||||
});
|
||||
}
|
||||
if (!scenario.readyText)
|
||||
await expect(page.locator(scenario.form)).toHaveCount(0);
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
await expect(page.locator(scenario.form).first()).toBeAttached();
|
||||
await expect(
|
||||
page.getByRole('status').filter({ hasText: 'Loading...' }),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
test(`${scenario.name}: failed request can be retried`, async ({ page }) => {
|
||||
await setup(page);
|
||||
let fail = true;
|
||||
await page.route(`**/api/v1${scenario.endpoint}`, async (route) => {
|
||||
if (fail)
|
||||
await route.fulfill({
|
||||
status: 503,
|
||||
json: {
|
||||
code: -1,
|
||||
msg: 'Temporarily unavailable',
|
||||
message: 'Temporarily unavailable',
|
||||
},
|
||||
});
|
||||
else await route.fallback();
|
||||
});
|
||||
await page.goto(scenario.url);
|
||||
const error = page
|
||||
.getByRole('alert')
|
||||
.filter({ hasText: 'Failed to load. Please try again.' });
|
||||
await expect(error).toBeVisible();
|
||||
fail = false;
|
||||
await error.getByRole('button', { name: 'Retry', exact: true }).click();
|
||||
await expect(error).toHaveCount(0);
|
||||
await expect(page.locator(scenario.form).first()).toBeAttached();
|
||||
await expect(
|
||||
page.getByRole('status').filter({ hasText: 'Loading...' }),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
}
|
||||
|
||||
test('completed empty metadata displays the genuine empty state', async ({
|
||||
page,
|
||||
}) => {
|
||||
await setup(page);
|
||||
await page.route('**/api/v1/agents/_/metadata', (route) =>
|
||||
route.fulfill({
|
||||
json: {
|
||||
code: 0,
|
||||
data: { runner_config: null, platform_tools: [], host_tools: [] },
|
||||
},
|
||||
}),
|
||||
);
|
||||
await page.goto('/home/agents?id=agent-empty');
|
||||
await expect(
|
||||
page.getByText('No runners are available', { exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('status').filter({ hasText: 'Loading...' }),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('switching bots resets the form and ignores an old response', async ({
|
||||
page,
|
||||
}) => {
|
||||
await setup(page);
|
||||
await page.route('**/api/v1/platform/bots', (route) =>
|
||||
route.fulfill({
|
||||
json: {
|
||||
code: 0,
|
||||
data: {
|
||||
bots: ['bot-first', 'bot-second'].map((uuid) => ({
|
||||
uuid,
|
||||
name: uuid,
|
||||
adapter: 'aiocqhttp',
|
||||
enable: true,
|
||||
})),
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
let release!: () => void;
|
||||
const pending = new Promise<void>((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
let requested = false;
|
||||
let responded = false;
|
||||
await page.route('**/api/v1/platform/bots/bot-second', async (route) => {
|
||||
requested = true;
|
||||
await pending;
|
||||
await route.fallback();
|
||||
responded = true;
|
||||
});
|
||||
await page.goto('/home/bots?id=bot-first');
|
||||
await expect(page.locator('#bot-form')).toBeVisible();
|
||||
try {
|
||||
await page.locator('a[href="/home/bots?id=bot-second"]').click();
|
||||
await expect.poll(() => requested).toBe(true);
|
||||
await expect(page.locator('#bot-form')).toHaveCount(0);
|
||||
await expect(
|
||||
page.getByRole('status').filter({ hasText: 'Loading...' }),
|
||||
).toBeVisible();
|
||||
await page.locator('a[href="/home/bots?id=bot-first"]').click();
|
||||
await expect(page.locator('#bot-form')).toBeVisible();
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
await expect.poll(() => responded).toBe(true);
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'bot-first', exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'bot-second', exact: true }),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
@@ -222,9 +222,7 @@ test('create first, select a plugin in the header, debug beside scrollable logs'
|
||||
await page
|
||||
.getByRole('textbox', { name: 'Name', exact: false })
|
||||
.fill('Welcome processor');
|
||||
await page
|
||||
.getByRole('button', { name: 'Create plugin processor', exact: true })
|
||||
.click();
|
||||
await page.getByRole('button', { name: 'Submit', exact: true }).click();
|
||||
await expect(page).toHaveURL(/id=processor-qa/);
|
||||
expect(creations).toHaveLength(1);
|
||||
expect(creations[0]).toMatchObject({
|
||||
|
||||
Reference in New Issue
Block a user