mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-27 02:45:24 +08:00
fix: bot routing setup and busy-session response checks (#2728)
Apply group response rules before busy-session steering. Initialize usable private-message and group-mention routes for new bots, including Wizard-created pipelines, and preserve member-configured rules across saves and retries. Fix empty pipeline configuration, refresh the processor sidebar after automatic creation, and show creation feedback. Align routing regressions and use Node 24 for the browser CI job. Correct the task's erroneous agent attribution while preserving every code tree and the original human contribution. Co-authored-by: multica-agent <[email protected]>
This commit is contained in:
co-authored by
multica-agent
parent
f8e32ef14f
commit
d13ae56ea5
@@ -852,18 +852,16 @@ test.describe('bot advanced flows', () => {
|
||||
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(
|
||||
page.getByRole('combobox').filter({ hasText: 'Dirty state target' }),
|
||||
).toHaveCount(2);
|
||||
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();
|
||||
).toHaveCount(2);
|
||||
});
|
||||
|
||||
test('shows validation error when bot name is empty', async ({ page }) => {
|
||||
@@ -1587,9 +1585,10 @@ test.describe('cross-resource flows', () => {
|
||||
await page.getByRole('menuitem', { name: /^Welcome new members/ }).click();
|
||||
|
||||
const routeCards = page.locator('[data-testid^="event-route-"]');
|
||||
await expect(routeCards).toHaveCount(2);
|
||||
await expect(routeCards).toHaveCount(3);
|
||||
await expect(routeCards.nth(0)).toContainText('Message received');
|
||||
await expect(routeCards.nth(1)).toContainText('Member joined group');
|
||||
await expect(routeCards.nth(1)).toContainText('Message received');
|
||||
await expect(routeCards.nth(2)).toContainText('Member joined group');
|
||||
|
||||
await page.getByRole('button', { name: 'Add behavior' }).click();
|
||||
await page
|
||||
@@ -1607,14 +1606,14 @@ test.describe('cross-resource flows', () => {
|
||||
await eventSubmenu
|
||||
.getByRole('menuitem', { name: /^Message edited/ })
|
||||
.click();
|
||||
await expect(routeCards).toHaveCount(3);
|
||||
await expect(routeCards.nth(2)).toContainText('Message edited');
|
||||
await expect(routeCards).toHaveCount(4);
|
||||
await expect(routeCards.nth(3)).toContainText('Message edited');
|
||||
|
||||
const firstHandle = page.getByRole('button', { name: 'Drag route 1' });
|
||||
const secondCard = routeCards.nth(1);
|
||||
await firstHandle.scrollIntoViewIfNeeded();
|
||||
const handleBox = await firstHandle.boundingBox();
|
||||
const targetBox = await secondCard.boundingBox();
|
||||
const memberHandle = page.getByRole('button', { name: 'Drag route 3' });
|
||||
const firstCard = routeCards.nth(0);
|
||||
await memberHandle.scrollIntoViewIfNeeded();
|
||||
const handleBox = await memberHandle.boundingBox();
|
||||
const targetBox = await firstCard.boundingBox();
|
||||
expect(handleBox).not.toBeNull();
|
||||
expect(targetBox).not.toBeNull();
|
||||
|
||||
@@ -1645,7 +1644,8 @@ test.describe('cross-resource flows', () => {
|
||||
const savedRouteCards = page.locator('[data-testid^="event-route-"]');
|
||||
await expect(savedRouteCards.nth(0)).toContainText('Member joined group');
|
||||
await expect(savedRouteCards.nth(1)).toContainText('Message received');
|
||||
await expect(savedRouteCards.nth(2)).toContainText('Message edited');
|
||||
await expect(savedRouteCards.nth(2)).toContainText('Message received');
|
||||
await expect(savedRouteCards.nth(3)).toContainText('Message edited');
|
||||
});
|
||||
|
||||
test('creates a pipeline then binds it to a bot', async ({ page }) => {
|
||||
@@ -1672,13 +1672,9 @@ test.describe('cross-resource flows', () => {
|
||||
|
||||
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 expect(
|
||||
page.getByRole('combobox').filter({ hasText: 'Production Pipeline' }),
|
||||
).toHaveCount(2);
|
||||
|
||||
// Save the bot
|
||||
await save(page);
|
||||
@@ -1688,6 +1684,6 @@ test.describe('cross-resource flows', () => {
|
||||
// The pipeline name should appear in the select trigger (not in sidebar or options)
|
||||
await expect(
|
||||
page.getByRole('combobox').filter({ hasText: 'Production Pipeline' }),
|
||||
).toBeVisible();
|
||||
).toHaveCount(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -212,8 +212,10 @@ test.describe('wizard and QR platform regressions', () => {
|
||||
pipelineCreateCount += 1;
|
||||
}
|
||||
if (
|
||||
request.method() === 'PUT' &&
|
||||
url.pathname === '/api/v1/platform/bots/bot-1'
|
||||
(request.method() === 'POST' &&
|
||||
url.pathname === '/api/v1/platform/bots') ||
|
||||
(request.method() === 'PUT' &&
|
||||
url.pathname === '/api/v1/platform/bots/bot-1')
|
||||
) {
|
||||
const body = request.postDataJSON() as {
|
||||
event_bindings?: Array<{
|
||||
@@ -227,7 +229,9 @@ test.describe('wizard and QR platform regressions', () => {
|
||||
item.event_pattern === 'message.received' &&
|
||||
item.target_type === 'pipeline',
|
||||
);
|
||||
boundPipelineUuid = binding?.target_uuid ?? null;
|
||||
if (body.event_bindings) {
|
||||
boundPipelineUuid = binding?.target_uuid ?? null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -413,8 +417,10 @@ test.describe('wizard and QR platform regressions', () => {
|
||||
pipelineCreateCount += 1;
|
||||
}
|
||||
if (
|
||||
request.method() === 'PUT' &&
|
||||
url.pathname === '/api/v1/platform/bots/bot-1'
|
||||
(request.method() === 'POST' &&
|
||||
url.pathname === '/api/v1/platform/bots') ||
|
||||
(request.method() === 'PUT' &&
|
||||
url.pathname === '/api/v1/platform/bots/bot-1')
|
||||
) {
|
||||
const body = request.postDataJSON() as {
|
||||
adapter_config?: { inbound_secret?: string };
|
||||
@@ -430,7 +436,9 @@ test.describe('wizard and QR platform regressions', () => {
|
||||
item.event_pattern === 'message.received' &&
|
||||
item.target_type === 'pipeline',
|
||||
);
|
||||
boundPipelineUuid = binding?.target_uuid ?? null;
|
||||
if (body.event_bindings) {
|
||||
boundPipelineUuid = binding?.target_uuid ?? null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import test from 'node:test';
|
||||
import ts from 'typescript';
|
||||
|
||||
const source = fs.readFileSync(
|
||||
new URL(
|
||||
'../../src/app/home/bots/components/bot-form/message-routing-template.ts',
|
||||
import.meta.url,
|
||||
),
|
||||
'utf8',
|
||||
);
|
||||
const exports = {};
|
||||
new Function(
|
||||
'exports',
|
||||
ts.transpileModule(source, {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS },
|
||||
}).outputText,
|
||||
)(exports);
|
||||
const append = exports.appendMessageRoutingTemplate;
|
||||
|
||||
test('message template explicitly separates private messages and mentions of this bot', () => {
|
||||
const routes = append([], { target_type: 'pipeline', target_uuid: 'p' });
|
||||
assert.equal(routes.length, 2);
|
||||
assert.deepEqual(routes[0].filters, [
|
||||
{ field: 'chat_type', operator: 'eq', value: 'person' },
|
||||
]);
|
||||
assert.deepEqual(routes[1].filters, [
|
||||
{ field: 'chat_type', operator: 'eq', value: 'group' },
|
||||
{ field: 'is_at_bot', operator: 'eq', value: true },
|
||||
]);
|
||||
assert.ok(routes.every((r) => r.target_uuid === 'p' && r.enabled));
|
||||
});
|
||||
|
||||
test('reapplying preserves manual destinations, disabled rules and filter order without duplication', () => {
|
||||
const routes = append([], { target_type: 'pipeline', target_uuid: 'p' });
|
||||
routes[0].target_uuid = 'manual';
|
||||
routes[1].filters.reverse();
|
||||
routes[1].enabled = false;
|
||||
const manual = {
|
||||
event_pattern: '*',
|
||||
target_type: 'discard',
|
||||
target_uuid: '',
|
||||
filters: [],
|
||||
};
|
||||
routes.unshift(manual);
|
||||
const before = structuredClone(routes);
|
||||
assert.deepEqual(
|
||||
append(routes, { target_type: 'agent', target_uuid: 'other' }),
|
||||
before,
|
||||
);
|
||||
assert.deepEqual(routes, before);
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import test from 'node:test';
|
||||
import ts from 'typescript';
|
||||
function load(path, deps = {}) {
|
||||
const mod = { exports: {} };
|
||||
new Function(
|
||||
'require',
|
||||
'exports',
|
||||
ts.transpileModule(
|
||||
fs.readFileSync(new URL('../../src/' + path, import.meta.url), 'utf8'),
|
||||
{ compilerOptions: { module: ts.ModuleKind.CommonJS } },
|
||||
).outputText,
|
||||
)((id) => deps[id], mod.exports);
|
||||
return mod.exports;
|
||||
}
|
||||
const template = load(
|
||||
'app/home/bots/components/bot-form/message-routing-template.ts',
|
||||
);
|
||||
const { prepareWizardBot, wizardBindingsIfMissing } = load(
|
||||
'app/wizard/bot-setup.ts',
|
||||
{ '@/app/home/bots/components/bot-form/message-routing-template': template },
|
||||
);
|
||||
const bot = {
|
||||
name: 'Wizard QA',
|
||||
description: '',
|
||||
adapter: 'onebot',
|
||||
adapter_config: {},
|
||||
enable: false,
|
||||
};
|
||||
test('fresh wizard binds both rules to its own pipeline and back navigation reuses the bot', async () => {
|
||||
const calls = [];
|
||||
let saved;
|
||||
const client = {
|
||||
createPipeline: async (p) => {
|
||||
calls.push('pipeline');
|
||||
assert.deepEqual(p.config, {});
|
||||
return { uuid: 'p' };
|
||||
},
|
||||
createBot: async (b) => {
|
||||
calls.push('bot');
|
||||
saved = b;
|
||||
return { uuid: 'b' };
|
||||
},
|
||||
getBot: async () => ({ bot: saved }),
|
||||
};
|
||||
const draft = { adapter: 'onebot', botUuid: null, pipelineUuid: null };
|
||||
await prepareWizardBot(client, draft, bot, 'QA pipeline');
|
||||
assert.deepEqual(calls, ['pipeline', 'bot']);
|
||||
assert.equal(saved.event_bindings.length, 2);
|
||||
assert.ok(saved.event_bindings.every((b) => b.target_uuid === 'p'));
|
||||
assert.deepEqual(saved.event_bindings[1].filters[1], {
|
||||
field: 'is_at_bot',
|
||||
operator: 'eq',
|
||||
value: true,
|
||||
});
|
||||
saved.adapter_config = { token: 'edited' };
|
||||
assert.equal(
|
||||
(await prepareWizardBot(client, draft, bot, 'QA pipeline')).bot
|
||||
.adapter_config.token,
|
||||
'edited',
|
||||
);
|
||||
assert.deepEqual(calls, ['pipeline', 'bot']);
|
||||
});
|
||||
test('retry after bot failure reuses the successful pipeline', async () => {
|
||||
let pipelines = 0,
|
||||
bots = 0;
|
||||
const client = {
|
||||
createPipeline: async () => ({ uuid: 'p' + ++pipelines }),
|
||||
createBot: async () => {
|
||||
if (++bots === 1) throw new Error('temporary failure');
|
||||
return { uuid: 'b' };
|
||||
},
|
||||
};
|
||||
const draft = { adapter: 'onebot', botUuid: null, pipelineUuid: null };
|
||||
await assert.rejects(prepareWizardBot(client, draft, bot, 'p'));
|
||||
await prepareWizardBot(client, draft, bot, 'p');
|
||||
assert.equal(pipelines, 1);
|
||||
assert.equal(bots, 2);
|
||||
});
|
||||
test('existing pipeline rules preserve disabled flags, filters and manual destinations', () => {
|
||||
const custom = {
|
||||
...bot,
|
||||
event_bindings: [
|
||||
{
|
||||
id: 'manual',
|
||||
event_pattern: '*',
|
||||
target_type: 'pipeline',
|
||||
target_uuid: 'existing',
|
||||
filters: [],
|
||||
enabled: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
const before = structuredClone(custom);
|
||||
assert.deepEqual(wizardBindingsIfMissing(custom, 'new'), {});
|
||||
assert.deepEqual(custom, before);
|
||||
const patch = wizardBindingsIfMissing(bot, 'p');
|
||||
assert.equal(patch.event_bindings.length, 2);
|
||||
assert.deepEqual(wizardBindingsIfMissing({ ...bot, ...patch }, 'p'), {});
|
||||
});
|
||||
Reference in New Issue
Block a user