mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-24 11:07:14 +00:00
fix(wizard): repair HTTP bot inbound test setup
This commit is contained in:
@@ -48,6 +48,11 @@ import {
|
|||||||
import { getAdapterDocUrl } from '@/app/infra/entities/adapter-docs';
|
import { getAdapterDocUrl } from '@/app/infra/entities/adapter-docs';
|
||||||
import i18n from 'i18next';
|
import i18n from 'i18next';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ensureHttpBotSigningSecret,
|
||||||
|
getErrorMessage,
|
||||||
|
} from '@/app/wizard/utils';
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import {
|
import {
|
||||||
@@ -177,15 +182,30 @@ export default function WizardPage() {
|
|||||||
const botData = await httpClient.getBot(progress.created_bot_uuid);
|
const botData = await httpClient.getBot(progress.created_bot_uuid);
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
|
|
||||||
setSelectedAdapter(progress.selected_adapter);
|
const restoredAdapter =
|
||||||
|
progress.selected_adapter ?? botData.bot.adapter;
|
||||||
|
const restoredConfig = (botData.bot.adapter_config ?? {}) as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
|
const configToRestore = ensureHttpBotSigningSecret(
|
||||||
|
restoredAdapter,
|
||||||
|
restoredConfig,
|
||||||
|
);
|
||||||
|
const configNeedsSave = configToRestore !== restoredConfig;
|
||||||
|
|
||||||
|
setSelectedAdapter(restoredAdapter);
|
||||||
setCreatedBotUuid(progress.created_bot_uuid);
|
setCreatedBotUuid(progress.created_bot_uuid);
|
||||||
setCreatedPipelineUuid(progress.created_pipeline_uuid ?? null);
|
setCreatedPipelineUuid(progress.created_pipeline_uuid ?? null);
|
||||||
setBotSaved(progress.bot_saved ?? false);
|
setBotSaved(
|
||||||
|
configNeedsSave ? false : (progress.bot_saved ?? false),
|
||||||
|
);
|
||||||
setMessageReceived(progress.message_received ?? false);
|
setMessageReceived(progress.message_received ?? false);
|
||||||
setSelectedRunner(progress.selected_runner);
|
setSelectedRunner(progress.selected_runner);
|
||||||
|
|
||||||
// Restore bot name from fetched bot data
|
// Restore bot name from fetched bot data
|
||||||
setBotName(botData.bot.name);
|
setBotName(botData.bot.name);
|
||||||
|
setAdapterConfig(configToRestore);
|
||||||
|
|
||||||
// Restore webhook URLs
|
// Restore webhook URLs
|
||||||
const runtimeValues = botData.bot.adapter_runtime_values as
|
const runtimeValues = botData.bot.adapter_runtime_values as
|
||||||
@@ -360,12 +380,17 @@ export default function WizardPage() {
|
|||||||
const defaultConfig = adapter
|
const defaultConfig = adapter
|
||||||
? getDefaultValues(adapter.spec.config)
|
? getDefaultValues(adapter.spec.config)
|
||||||
: {};
|
: {};
|
||||||
|
const initialConfig = ensureHttpBotSigningSecret(
|
||||||
|
selectedAdapter,
|
||||||
|
defaultConfig,
|
||||||
|
);
|
||||||
|
setAdapterConfig(initialConfig);
|
||||||
|
|
||||||
const bot: Bot = {
|
const bot: Bot = {
|
||||||
name: defaultName,
|
name: defaultName,
|
||||||
description: '',
|
description: '',
|
||||||
adapter: selectedAdapter,
|
adapter: selectedAdapter,
|
||||||
adapter_config: defaultConfig,
|
adapter_config: initialConfig,
|
||||||
enable: false,
|
enable: false,
|
||||||
};
|
};
|
||||||
const resp = await httpClient.createBot(bot);
|
const resp = await httpClient.createBot(bot);
|
||||||
@@ -454,11 +479,17 @@ export default function WizardPage() {
|
|||||||
setCreatedPipelineUuid(pipelineUuid);
|
setCreatedPipelineUuid(pipelineUuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const configToSave = ensureHttpBotSigningSecret(
|
||||||
|
selectedAdapter,
|
||||||
|
adapterConfig,
|
||||||
|
);
|
||||||
|
setAdapterConfig(configToSave);
|
||||||
|
|
||||||
await httpClient.updateBot(createdBotUuid, {
|
await httpClient.updateBot(createdBotUuid, {
|
||||||
name: botName,
|
name: botName,
|
||||||
description: botDescription || '',
|
description: botDescription || '',
|
||||||
adapter: selectedAdapter,
|
adapter: selectedAdapter,
|
||||||
adapter_config: adapterConfig,
|
adapter_config: configToSave,
|
||||||
enable: true,
|
enable: true,
|
||||||
use_pipeline_uuid: pipelineUuid,
|
use_pipeline_uuid: pipelineUuid,
|
||||||
});
|
});
|
||||||
@@ -1063,7 +1094,7 @@ function StepBotConfig({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(
|
toast.error(
|
||||||
t('wizard.botConfig.httpTestFailed', {
|
t('wizard.botConfig.httpTestFailed', {
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error: getErrorMessage(error),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
export function getErrorMessage(error: unknown): string {
|
||||||
|
if (error instanceof Error) return error.message;
|
||||||
|
|
||||||
|
if (typeof error === 'object' && error !== null && 'msg' in error) {
|
||||||
|
const message = (error as { msg?: unknown }).msg;
|
||||||
|
if (typeof message === 'string') return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSigningSecret(): string {
|
||||||
|
const bytes = new Uint8Array(32);
|
||||||
|
crypto.getRandomValues(bytes);
|
||||||
|
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join(
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ensureHttpBotSigningSecret(
|
||||||
|
adapterName: string,
|
||||||
|
config: Record<string, unknown>,
|
||||||
|
): Record<string, unknown> {
|
||||||
|
if (
|
||||||
|
adapterName !== 'http_bot' ||
|
||||||
|
config.signature_required === false ||
|
||||||
|
(typeof config.inbound_secret === 'string' && config.inbound_secret)
|
||||||
|
) {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...config,
|
||||||
|
inbound_secret: createSigningSecret(),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import test from 'node:test';
|
||||||
|
import ts from 'typescript';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const currentDirectory = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const sourcePath = path.resolve(
|
||||||
|
currentDirectory,
|
||||||
|
'../../src/app/wizard/utils.ts',
|
||||||
|
);
|
||||||
|
|
||||||
|
function loadWizardUtils() {
|
||||||
|
const source = fs.readFileSync(sourcePath, 'utf8');
|
||||||
|
const compiled = ts.transpileModule(source, {
|
||||||
|
compilerOptions: { module: ts.ModuleKind.CommonJS },
|
||||||
|
}).outputText;
|
||||||
|
const loadedModule = { exports: {} };
|
||||||
|
new Function('require', 'module', 'exports', compiled)(
|
||||||
|
() => {
|
||||||
|
throw new Error('Wizard utils must not have runtime imports');
|
||||||
|
},
|
||||||
|
loadedModule,
|
||||||
|
loadedModule.exports,
|
||||||
|
);
|
||||||
|
return loadedModule.exports;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { ensureHttpBotSigningSecret, getErrorMessage } = loadWizardUtils();
|
||||||
|
|
||||||
|
test('generates an HTTP Bot signing secret when signatures are enabled', () => {
|
||||||
|
const config = ensureHttpBotSigningSecret('http_bot', {
|
||||||
|
signature_required: true,
|
||||||
|
inbound_secret: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.match(config.inbound_secret, /^[a-f0-9]{64}$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('preserves existing or intentionally disabled HTTP Bot signing config', () => {
|
||||||
|
const existing = { signature_required: true, inbound_secret: 'keep-me' };
|
||||||
|
const disabled = { signature_required: false, inbound_secret: '' };
|
||||||
|
|
||||||
|
assert.equal(ensureHttpBotSigningSecret('http_bot', existing), existing);
|
||||||
|
assert.equal(ensureHttpBotSigningSecret('http_bot', disabled), disabled);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not add signing config to other adapters', () => {
|
||||||
|
const config = {};
|
||||||
|
|
||||||
|
assert.equal(ensureHttpBotSigningSecret('web_page_bot', config), config);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('extracts the backend message from structured API errors', () => {
|
||||||
|
assert.equal(
|
||||||
|
getErrorMessage({ code: 400, msg: 'Signing secret is required' }),
|
||||||
|
'Signing secret is required',
|
||||||
|
);
|
||||||
|
assert.equal(getErrorMessage(new Error('Network failed')), 'Network failed');
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user