mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-22 16:35:20 +08:00
test(web): verify resource writes and recovery through behavior
This commit is contained in:
@@ -3,6 +3,7 @@ import { expect, Page, test } from '@playwright/test';
|
||||
import {
|
||||
installLangBotApiMocks,
|
||||
makeWorkspaceEntry,
|
||||
pipelineMetadata,
|
||||
} from './fixtures/langbot-api';
|
||||
|
||||
async function save(page: Page) {
|
||||
@@ -31,11 +32,20 @@ async function createPlaywrightBot(page: Page, name: string, description = '') {
|
||||
await submit(page);
|
||||
}
|
||||
|
||||
async function confirmDelete(page: Page) {
|
||||
async function confirmDelete(page: Page, apiPath: string) {
|
||||
const responses: boolean[] = [];
|
||||
page.on('response', (response) => {
|
||||
if (
|
||||
response.request().method() === 'DELETE' &&
|
||||
new URL(response.url()).pathname === apiPath
|
||||
)
|
||||
responses.push(response.ok());
|
||||
});
|
||||
await page
|
||||
.getByRole('dialog')
|
||||
.getByRole('button', { name: /^Confirm Delete$/ })
|
||||
.click();
|
||||
await expect.poll(() => responses).toEqual([true]);
|
||||
}
|
||||
|
||||
async function installDelayedFirstSave(page: Page, apiPath: string) {
|
||||
@@ -198,10 +208,14 @@ test.describe('frontend CRUD smoke flows', () => {
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: /^Delete$/ }).click();
|
||||
await confirmDelete(page);
|
||||
await confirmDelete(page, '/api/v1/platform/bots/bot-1');
|
||||
|
||||
await expect(page).toHaveURL(/\/home\/bots$/);
|
||||
await expect(page.getByText('Select a bot from the sidebar')).toBeVisible();
|
||||
await page.reload();
|
||||
await expect(
|
||||
page.getByText('Support Bot Updated', { exact: true }),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('creates, edits, and deletes a pipeline', async ({ page }) => {
|
||||
@@ -264,15 +278,26 @@ test.describe('frontend CRUD smoke flows', () => {
|
||||
.getByLabel('Description')
|
||||
.fill('Routes urgent customer issues to operators.');
|
||||
await pipelineInfoDialog.getByRole('button', { name: 'Save' }).click();
|
||||
await expect(pipelineInfoDialog).toHaveCount(0);
|
||||
await page.reload();
|
||||
await page.getByRole('button', { name: 'Edit basic information' }).click();
|
||||
await expect(
|
||||
page.getByRole('dialog').getByLabel('Description'),
|
||||
).toHaveValue('Routes urgent customer issues to operators.');
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
await page.getByRole('button', { name: 'Management' }).click();
|
||||
await page.getByRole('button', { name: /^Delete$/ }).click();
|
||||
await confirmDelete(page);
|
||||
await confirmDelete(page, '/api/v1/pipelines/pipeline-1');
|
||||
|
||||
await expect(page).toHaveURL(/\/home\/agents$/);
|
||||
await expect(
|
||||
page.getByText('Select a processor from the sidebar'),
|
||||
).toBeVisible();
|
||||
await page.reload();
|
||||
await expect(
|
||||
page.getByText('Escalation Pipeline', { exact: true }),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('opens pipeline AI capabilities with malformed model options', async ({
|
||||
@@ -337,12 +362,16 @@ test.describe('frontend CRUD smoke flows', () => {
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: /^Delete$/ }).click();
|
||||
await confirmDelete(page);
|
||||
await confirmDelete(page, '/api/v1/knowledge/bases/knowledge-1');
|
||||
|
||||
await expect(page).toHaveURL(/\/home\/knowledge$/);
|
||||
await expect(
|
||||
page.getByText('Select a knowledge base from the sidebar'),
|
||||
).toBeVisible();
|
||||
await page.reload();
|
||||
await expect(
|
||||
page.getByText('Support Knowledge Updated', { exact: true }),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('creates, edits, and deletes an MCP server', async ({ page }) => {
|
||||
@@ -366,18 +395,33 @@ test.describe('frontend CRUD smoke flows', () => {
|
||||
await page
|
||||
.locator('input[name="url"]')
|
||||
.fill('https://mcp.example.test/updated-sse');
|
||||
const update = page.waitForResponse(
|
||||
(response) =>
|
||||
response.request().method() === 'PUT' &&
|
||||
response.url().endsWith('/api/v1/mcp/servers/playwright-mcp'),
|
||||
);
|
||||
await save(page);
|
||||
expect((await update).request().postDataJSON()).toMatchObject({
|
||||
name: 'playwright-mcp',
|
||||
mode: 'remote',
|
||||
extra_args: { url: 'https://mcp.example.test/updated-sse' },
|
||||
});
|
||||
await page.reload();
|
||||
await expect(page.locator('input[name="url"]')).toHaveValue(
|
||||
'https://mcp.example.test/updated-sse',
|
||||
);
|
||||
|
||||
await page.getByRole('button', { name: /^Delete$/ }).click();
|
||||
await confirmDelete(page);
|
||||
await confirmDelete(page, '/api/v1/mcp/servers/playwright-mcp');
|
||||
|
||||
await expect(page).toHaveURL(/\/home\/mcp$/);
|
||||
await expect(
|
||||
page.getByText('Select an MCP server from the sidebar'),
|
||||
).toBeVisible();
|
||||
await page.reload();
|
||||
await expect(page.getByText('playwright-mcp', { exact: true })).toHaveCount(
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
||||
test('updates and deletes a manually-created skill', async ({ page }) => {
|
||||
@@ -415,9 +459,13 @@ test.describe('frontend CRUD smoke flows', () => {
|
||||
);
|
||||
|
||||
await page.getByRole('button', { name: /^Delete$/ }).click();
|
||||
await confirmDelete(page);
|
||||
await confirmDelete(page, '/api/v1/skills/release_notes');
|
||||
|
||||
await expect(page).toHaveURL(/\/home\/add-extension$/);
|
||||
await page.goto('/home/skills');
|
||||
await expect(page.getByText('Release Notes', { exact: true })).toHaveCount(
|
||||
0,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -679,6 +727,54 @@ test.describe('bot advanced flows', () => {
|
||||
|
||||
test('switches between bot detail tabs', async ({ page }) => {
|
||||
await installLangBotApiMocks(page, { authenticated: true });
|
||||
await page.route('**/api/v1/platform/bots/bot-1/logs', (route) =>
|
||||
route.fulfill({
|
||||
json: {
|
||||
code: 0,
|
||||
data: {
|
||||
logs: [
|
||||
{
|
||||
seq_id: 1,
|
||||
level: 'info',
|
||||
text: 'Bot tab log response',
|
||||
images: [],
|
||||
message_session_id: '',
|
||||
timestamp: 1792200000,
|
||||
},
|
||||
],
|
||||
total_count: 1,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
await page.route('**/api/v1/monitoring/sessions?**', (route) => {
|
||||
expect(new URL(route.request().url()).searchParams.get('botId')).toBe(
|
||||
'bot-1',
|
||||
);
|
||||
return route.fulfill({
|
||||
json: {
|
||||
code: 0,
|
||||
data: {
|
||||
sessions: [
|
||||
{
|
||||
session_id: 'person_tab-user',
|
||||
bot_id: 'bot-1',
|
||||
bot_name: 'Tab Test Bot',
|
||||
pipeline_id: 'pipeline-tab',
|
||||
pipeline_name: 'Tab pipeline',
|
||||
message_count: 1,
|
||||
start_time: '2026-10-17T00:00:00Z',
|
||||
last_activity: '2026-10-17T00:01:00Z',
|
||||
is_active: true,
|
||||
user_id: 'tab-user',
|
||||
user_name: 'Tab Session User',
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// Create a bot
|
||||
await page.goto('/home/bots?id=new');
|
||||
@@ -699,6 +795,10 @@ test.describe('bot advanced flows', () => {
|
||||
'active',
|
||||
);
|
||||
|
||||
await expect(
|
||||
page.getByText('Bot tab log response', { exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
// Switch to Sessions tab
|
||||
await page.getByRole('tab', { name: /Sessions/ }).click();
|
||||
await expect(page.getByRole('tab', { name: /Sessions/ })).toHaveAttribute(
|
||||
@@ -706,6 +806,10 @@ test.describe('bot advanced flows', () => {
|
||||
'active',
|
||||
);
|
||||
|
||||
await expect(
|
||||
page.getByText('Tab Session User', { exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
// Switch back to Configuration
|
||||
await page.getByRole('tab', { name: /Configuration/ }).click();
|
||||
await expect(
|
||||
@@ -716,6 +820,14 @@ test.describe('bot advanced flows', () => {
|
||||
test('save button is disabled when form is clean', async ({ page }) => {
|
||||
await installLangBotApiMocks(page, { authenticated: true });
|
||||
|
||||
await page.goto('/home/agents?id=new');
|
||||
await page.getByRole('radio', { name: /^Pipeline/ }).click();
|
||||
await page.locator('#agent-create-form').getByRole('combobox').click();
|
||||
await page.getByRole('option', { name: /Local Agent/ }).click();
|
||||
await page.locator('input[name="name"]').fill('Dirty state target');
|
||||
await submit(page);
|
||||
await expect(page).toHaveURL(/\/home\/agents\?id=pipeline-1$/);
|
||||
|
||||
// Create a bot
|
||||
await page.goto('/home/bots?id=new');
|
||||
await createPlaywrightBot(page, 'Clean Form Bot');
|
||||
@@ -728,7 +840,9 @@ test.describe('bot advanced flows', () => {
|
||||
).toBeVisible();
|
||||
|
||||
// After loading, save button should be disabled (form is clean)
|
||||
const saveButton = page.getByRole('button', { name: /^Save$/ });
|
||||
const saveButton = page.getByRole('button', {
|
||||
name: /^Save(?: and enable)?$/,
|
||||
});
|
||||
await expect(saveButton).toBeDisabled();
|
||||
|
||||
await page.getByRole('button', { name: 'Edit basic information' }).click();
|
||||
@@ -736,6 +850,20 @@ test.describe('bot advanced flows', () => {
|
||||
await infoDialog.getByLabel('Description').fill('New description');
|
||||
await infoDialog.getByRole('button', { name: 'Save' }).click();
|
||||
await expect(saveButton).toBeDisabled();
|
||||
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();
|
||||
await page.getByRole('option', { name: /Dirty state target/ }).click();
|
||||
await expect(saveButton).toBeEnabled();
|
||||
await save(page);
|
||||
await expect(saveButton).toBeDisabled();
|
||||
await page.reload();
|
||||
await expect(
|
||||
page.getByRole('combobox').filter({ hasText: 'Dirty state target' }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('shows validation error when bot name is empty', async ({ page }) => {
|
||||
@@ -823,6 +951,17 @@ test.describe('pipeline advanced flows', () => {
|
||||
|
||||
test('save button reflects form dirty state', async ({ page }) => {
|
||||
await installLangBotApiMocks(page, { authenticated: true });
|
||||
const metadata = pipelineMetadata();
|
||||
metadata.configs[0].stages[1].config.push({
|
||||
name: 'instruction',
|
||||
label: { en_US: 'Instruction', zh_Hans: 'Instruction' },
|
||||
type: 'text',
|
||||
required: true,
|
||||
default: 'Original instruction',
|
||||
} as any);
|
||||
await page.route('**/api/v1/pipelines/_/metadata', (route) =>
|
||||
route.fulfill({ json: { code: 0, data: metadata } }),
|
||||
);
|
||||
|
||||
// Create a pipeline
|
||||
await page.goto('/home/agents?id=new');
|
||||
@@ -842,6 +981,18 @@ test.describe('pipeline advanced flows', () => {
|
||||
page.getByRole('heading', { name: /Dirty Form Pipeline Updated/ }),
|
||||
).toBeVisible();
|
||||
await expect(saveButton).toBeDisabled();
|
||||
await page.getByRole('tab', { name: /^AI$/ }).click();
|
||||
const instruction = page.locator('textarea[name="instruction"]');
|
||||
await expect(instruction).toHaveValue('Original instruction');
|
||||
await instruction.fill('Updated instruction');
|
||||
await expect(saveButton).toBeEnabled();
|
||||
await save(page);
|
||||
await expect(saveButton).toBeDisabled();
|
||||
await page.reload();
|
||||
await page.getByRole('tab', { name: /^AI$/ }).click();
|
||||
await expect(page.locator('textarea[name="instruction"]')).toHaveValue(
|
||||
'Updated instruction',
|
||||
);
|
||||
});
|
||||
|
||||
test('shows validation error when pipeline name is empty', async ({
|
||||
|
||||
@@ -22,7 +22,7 @@ async function setup(page: Page) {
|
||||
await page.route('**/api/v1/plugins/qa/loading**', async (route) => {
|
||||
const path = new URL(route.request().url()).pathname;
|
||||
const data = path.endsWith('/config')
|
||||
? { config: {} }
|
||||
? { config: { support_text: 'Restored plugin configuration' } }
|
||||
: path.endsWith('/readme')
|
||||
? { readme: '# Loaded documentation' }
|
||||
: {
|
||||
@@ -35,7 +35,16 @@ async function setup(page: Page) {
|
||||
label: { en_US: 'Loading test plugin' },
|
||||
description: { en_US: 'Test' },
|
||||
},
|
||||
spec: { config: [] },
|
||||
spec: {
|
||||
config: [
|
||||
{
|
||||
name: 'support_text',
|
||||
label: { en_US: 'Plugin greeting' },
|
||||
type: 'text',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
components: [],
|
||||
@@ -179,8 +188,8 @@ const cases = [
|
||||
name: 'plugin configuration',
|
||||
url: '/home/extensions?id=qa/loading',
|
||||
endpoint: '/plugins/qa/loading/config',
|
||||
form: '[data-slot="card-title"]',
|
||||
readyText: 'Plugin Configuration',
|
||||
form: 'textarea[name="support_text"]',
|
||||
readyValue: 'Restored plugin configuration',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -225,7 +234,9 @@ for (const scenario of cases) {
|
||||
test(`${scenario.name}: failed request can be retried`, async ({ page }) => {
|
||||
await setup(page);
|
||||
let fail = true;
|
||||
let requests = 0;
|
||||
await page.route(`**/api/v1${scenario.endpoint}`, async (route) => {
|
||||
requests++;
|
||||
if (fail)
|
||||
await route.fulfill({
|
||||
status: 503,
|
||||
@@ -242,9 +253,19 @@ for (const scenario of cases) {
|
||||
.getByRole('alert')
|
||||
.filter({ hasText: 'Unable to load this content' });
|
||||
await expect(error).toBeVisible();
|
||||
const requestsBeforeRetry = requests;
|
||||
fail = false;
|
||||
await error.getByRole('button', { name: 'Retry', exact: true }).click();
|
||||
await expect.poll(() => requests).toBeGreaterThan(requestsBeforeRetry);
|
||||
await expect(error).toHaveCount(0);
|
||||
if (scenario.readyText)
|
||||
await expect(
|
||||
page.getByText(scenario.readyText, { exact: true }).first(),
|
||||
).toBeVisible();
|
||||
if (scenario.readyValue)
|
||||
await expect(page.locator(scenario.form)).toHaveValue(
|
||||
scenario.readyValue,
|
||||
);
|
||||
await expect(page.locator(scenario.form).first()).toBeAttached();
|
||||
await expect(
|
||||
page.getByRole('status').filter({ hasText: 'Loading...' }),
|
||||
|
||||
@@ -138,3 +138,37 @@ test.describe('authenticated app shell', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('token monitoring displays structured API failures and retries the request', async ({
|
||||
page,
|
||||
}) => {
|
||||
await installLangBotApiMocks(page, { authenticated: true });
|
||||
let failed = true;
|
||||
let requests = 0;
|
||||
await page.route(
|
||||
'**/api/v1/monitoring/token-statistics?**',
|
||||
async (route) => {
|
||||
requests++;
|
||||
if (failed)
|
||||
return route.fulfill({
|
||||
status: 503,
|
||||
json: { code: 1, msg: 'Token aggregation unavailable' },
|
||||
});
|
||||
await route.fallback();
|
||||
},
|
||||
);
|
||||
await page.goto('/home/monitoring');
|
||||
await page.getByRole('tab', { name: 'Token Monitoring' }).click();
|
||||
const error = page
|
||||
.getByRole('alert')
|
||||
.filter({ hasText: 'Token aggregation unavailable' });
|
||||
await expect(error).toBeVisible();
|
||||
const beforeRetry = requests;
|
||||
failed = false;
|
||||
await error.getByRole('button', { name: 'Retry', exact: true }).click();
|
||||
await expect.poll(() => requests).toBeGreaterThan(beforeRetry);
|
||||
await expect(error).toHaveCount(0);
|
||||
await expect(
|
||||
page.getByText('No token usage in the selected time range'),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -419,7 +419,9 @@ for (const failure of [
|
||||
test('workspace changes discard previous task results and stop polling', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.clock.install();
|
||||
const state = await setup(page);
|
||||
state.done = false;
|
||||
let release!: () => void;
|
||||
state.holdPoll = new Promise((resolve) => {
|
||||
release = resolve;
|
||||
@@ -437,6 +439,8 @@ test('workspace changes discard previous task results and stop polling', async (
|
||||
await expect(
|
||||
page.getByRole('dialog', { name: 'Pipeline migration' }),
|
||||
).toHaveCount(0);
|
||||
await page.clock.runFor(3000);
|
||||
expect(state.polls).toBe(1);
|
||||
expect(state.posts).toHaveLength(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -77,6 +77,16 @@ test.describe('processor detail workbench', () => {
|
||||
expect(debugBox!.y).toBeGreaterThanOrEqual(0);
|
||||
|
||||
const flow = configPanel.getByRole('tablist');
|
||||
await expect(flow.getByRole('tab')).toHaveCount(2);
|
||||
const tabs = await flow.getByRole('tab').all();
|
||||
const positions = await Promise.all(tabs.map((tab) => tab.boundingBox()));
|
||||
expect(positions.every((position) => position !== null)).toBe(true);
|
||||
for (let index = 1; index < positions.length; index++) {
|
||||
expect(
|
||||
Math.abs(positions[index]!.y - positions[0]!.y),
|
||||
).toBeLessThanOrEqual(1);
|
||||
expect(positions[index]!.x).toBeGreaterThan(positions[index - 1]!.x);
|
||||
}
|
||||
await expect(flow.getByRole('tab').nth(0)).toContainText('Runner');
|
||||
await expect(flow.getByRole('tab').nth(1)).toContainText('Events & tools');
|
||||
await expect(flow.getByRole('tab')).toHaveCount(2);
|
||||
@@ -490,6 +500,16 @@ test.describe('processor detail workbench', () => {
|
||||
expect(debugBox!.y).toBeGreaterThanOrEqual(0);
|
||||
|
||||
const flow = configPanel.getByRole('tablist');
|
||||
await expect(flow.getByRole('tab')).toHaveCount(3);
|
||||
const tabs = await flow.getByRole('tab').all();
|
||||
const positions = await Promise.all(tabs.map((tab) => tab.boundingBox()));
|
||||
expect(positions.every((position) => position !== null)).toBe(true);
|
||||
for (let index = 1; index < positions.length; index++) {
|
||||
expect(
|
||||
Math.abs(positions[index]!.y - positions[0]!.y),
|
||||
).toBeLessThanOrEqual(1);
|
||||
expect(positions[index]!.x).toBeGreaterThan(positions[index - 1]!.x);
|
||||
}
|
||||
await expect(flow.getByRole('tab').nth(0)).toContainText('Trigger');
|
||||
await expect(flow.getByRole('tab').nth(1)).toContainText('AI');
|
||||
await expect(flow.getByRole('tab').nth(2)).toContainText('Output');
|
||||
|
||||
@@ -570,16 +570,34 @@ test('setup login starts the existing LangBot Account authorization flow', async
|
||||
await page.route('**/api/v1/provider/models/llm', (route) =>
|
||||
route.fulfill({ json: { code: 0, data: { models: [] } } }),
|
||||
);
|
||||
const authorizationUrl =
|
||||
'https://space.langbot.app/authorize?state=assistant-bind-state';
|
||||
await page.route('**/api/v1/user/space/bind-authorize-url?**', (route) =>
|
||||
route.fulfill({
|
||||
json: { code: 0, data: { authorize_url: authorizationUrl } },
|
||||
}),
|
||||
);
|
||||
await page.goto('/home/bots');
|
||||
const origin = new URL(page.url()).origin;
|
||||
const request = page.waitForRequest(
|
||||
(request) =>
|
||||
request.url().includes('/space/') && request.url().includes('authorize'),
|
||||
new URL(request.url()).pathname ===
|
||||
'/api/v1/user/space/bind-authorize-url',
|
||||
);
|
||||
await page
|
||||
.getByRole('dialog', { name: 'Workspace assistant' })
|
||||
.getByRole('button', { name: 'Sign in to LangBot Account' })
|
||||
.click();
|
||||
expect((await request).url()).toContain('authorize');
|
||||
const authorization = await request;
|
||||
expect(authorization.method()).toBe('GET');
|
||||
expect(new URL(authorization.url()).searchParams.get('redirect_uri')).toBe(
|
||||
`${origin}/auth/space/callback?mode=bind`,
|
||||
);
|
||||
expect(authorization.headers()['authorization']).toBe(
|
||||
'Bearer playwright-token',
|
||||
);
|
||||
expect(authorization.headers()['x-workspace-id']).toBeUndefined();
|
||||
await expect(page).toHaveURL(authorizationUrl);
|
||||
});
|
||||
|
||||
test('switches conversations, restores background progress after reload and stops only the selected task', async ({
|
||||
|
||||
@@ -1,97 +1,33 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const testDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const webRoot = path.resolve(testDir, '../..');
|
||||
|
||||
function readSource(relativePath) {
|
||||
return fs.readFileSync(path.join(webRoot, relativePath), 'utf8');
|
||||
}
|
||||
|
||||
test('agent and pipeline details share the split processor workbench', () => {
|
||||
const workbench = readSource(
|
||||
'src/app/home/components/processor-detail/ProcessorDetailWorkbench.tsx',
|
||||
);
|
||||
const agentDetail = readSource('src/app/home/agents/AgentDetailContent.tsx');
|
||||
const pipelineDetail = readSource(
|
||||
'src/app/home/pipelines/PipelineDetailContent.tsx',
|
||||
);
|
||||
const websocketClient = readSource(
|
||||
'src/app/infra/websocket/WebSocketClient.ts',
|
||||
);
|
||||
const pipelineDebug = readSource(
|
||||
'src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx',
|
||||
);
|
||||
const source = fs.readFileSync(
|
||||
new URL(
|
||||
'../../src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx',
|
||||
import.meta.url,
|
||||
),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
// Shared WebSocket tests do not mount this component's socket replacement effect.
|
||||
test('pipeline debug retains ownership guards before callback and connection-error updates', () => {
|
||||
for (const callback of [
|
||||
'onConnected',
|
||||
'onMessage',
|
||||
'onError',
|
||||
'onClose',
|
||||
'onBroadcast',
|
||||
]) {
|
||||
assert.match(
|
||||
source,
|
||||
new RegExp(
|
||||
`\\.${callback}\\(\\([^)]*\\) => \\{\\s*if \\(wsClientRef\\.current !== wsClient\\) return;`,
|
||||
),
|
||||
);
|
||||
}
|
||||
assert.match(
|
||||
workbench,
|
||||
/lg:grid-cols-\[minmax\(20rem,0\.72fr\)_minmax\(0,1\.28fr\)\]/,
|
||||
source,
|
||||
/await wsClient\.connect\(\);\s*\} catch \(error\) \{\s*if \(!wsClient \|\| wsClientRef\.current !== wsClient\) return;/,
|
||||
);
|
||||
assert.ok(
|
||||
workbench.indexOf('{debugContent}') <
|
||||
workbench.lastIndexOf('{configPanel}'),
|
||||
);
|
||||
assert.match(agentDetail, /<ProcessorDetailWorkbench/);
|
||||
assert.match(agentDetail, /debugContent=/);
|
||||
assert.match(pipelineDetail, /<ProcessorDetailWorkbench/);
|
||||
assert.match(pipelineDetail, /compact=\{true\}/);
|
||||
assert.doesNotMatch(
|
||||
websocketClient,
|
||||
/this\.ws\.onopen = \(\) => \{\s*this\.reconnectAttempts = 0/,
|
||||
);
|
||||
assert.match(
|
||||
websocketClient,
|
||||
/data\.type === 'connected'[\s\S]*this\.reconnectAttempts = 0/,
|
||||
);
|
||||
assert.match(websocketClient, /private reconnectTimeout:/);
|
||||
assert.match(websocketClient, /private disconnectedByUser = false/);
|
||||
assert.match(
|
||||
websocketClient,
|
||||
/if \(!this\.connectionId\)[\s\S]*this\.shouldReconnect = false/,
|
||||
);
|
||||
assert.match(
|
||||
pipelineDebug,
|
||||
/wsClientRef\.current = wsClient;[\s\S]*await wsClient\.connect\(\)/,
|
||||
);
|
||||
assert.match(
|
||||
pipelineDebug,
|
||||
/if \(wsClientRef\.current !== wsClient\) return;/,
|
||||
);
|
||||
assert.match(pipelineDebug, /data-slot="scroll-area-viewport"/);
|
||||
assert.doesNotMatch(pipelineDebug, /scrollIntoView/);
|
||||
});
|
||||
|
||||
test('processor forms expose their primary orchestration flow horizontally', () => {
|
||||
const agentForm = readSource(
|
||||
'src/app/home/agents/components/AgentFormComponent.tsx',
|
||||
);
|
||||
const pipelineForm = readSource(
|
||||
'src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx',
|
||||
);
|
||||
|
||||
const agentSections = agentForm.slice(
|
||||
agentForm.indexOf('const primarySections:'),
|
||||
agentForm.indexOf('const runnerStatus ='),
|
||||
);
|
||||
assert.deepEqual(
|
||||
[...agentSections.matchAll(/name: '([^']+)'/g)].map((match) => match[1]),
|
||||
['runner', 'events_and_tools'],
|
||||
);
|
||||
const runnerPanel = agentForm.slice(
|
||||
agentForm.indexOf("{activeSection === 'runner' &&"),
|
||||
agentForm.indexOf("{activeSection === 'events_and_tools' &&"),
|
||||
);
|
||||
assert.match(runnerPanel, /renderDynamicStage\(runnerSelectorStage\)/);
|
||||
assert.match(runnerPanel, /renderDynamicStage\(activeRunnerStage\)/);
|
||||
assert.match(
|
||||
pipelineForm,
|
||||
/const primarySectionNames = \['trigger', 'ai', 'output'\]/,
|
||||
);
|
||||
assert.match(agentForm, /<TabsList[^>]*grid-cols-2/);
|
||||
assert.match(pipelineForm, /<TabsList[^>]*grid-cols-3/);
|
||||
assert.doesNotMatch(agentForm, /<ol className=/);
|
||||
assert.doesNotMatch(pipelineForm, /<ol className=/);
|
||||
});
|
||||
|
||||
@@ -10,10 +10,6 @@ const utilsPath = path.resolve(
|
||||
currentDirectory,
|
||||
'../../src/app/home/monitoring/utils.ts',
|
||||
);
|
||||
const componentPath = path.resolve(
|
||||
currentDirectory,
|
||||
'../../src/app/home/monitoring/components/TokenMonitoring.tsx',
|
||||
);
|
||||
|
||||
function loadMonitoringUtils() {
|
||||
const source = fs.readFileSync(utilsPath, 'utf8');
|
||||
@@ -45,9 +41,3 @@ test('token monitoring extracts messages from structured API errors', () => {
|
||||
assert.equal(getErrorMessage(new Error('Network failed')), 'Network failed');
|
||||
assert.equal(getErrorMessage('Request failed'), 'Request failed');
|
||||
});
|
||||
|
||||
test('token monitoring uses the structured API error helper', () => {
|
||||
const source = fs.readFileSync(componentPath, 'utf8');
|
||||
assert.match(source, /import \{ getErrorMessage \} from '\.\.\/utils';/);
|
||||
assert.match(source, /setError\(getErrorMessage\(e\)\)/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user