From 6b60ad16789f7b1a44916a5e8f79b00691a16a2e Mon Sep 17 00:00:00 2001 From: fdc310 <2213070223@qq.com> Date: Sun, 23 Aug 2026 20:41:46 +0800 Subject: [PATCH] fix(wizard): repair HTTP bot inbound test setup --- web/src/app/wizard/page.tsx | 41 +++++++++++++++-- web/src/app/wizard/utils.ts | 36 +++++++++++++++ web/tests/unit/wizard-http-bot.test.mjs | 61 +++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 5 deletions(-) create mode 100644 web/src/app/wizard/utils.ts create mode 100644 web/tests/unit/wizard-http-bot.test.mjs diff --git a/web/src/app/wizard/page.tsx b/web/src/app/wizard/page.tsx index f2a2b13bd..9dc1d0140 100644 --- a/web/src/app/wizard/page.tsx +++ b/web/src/app/wizard/page.tsx @@ -48,6 +48,11 @@ import { import { getAdapterDocUrl } from '@/app/infra/entities/adapter-docs'; import i18n from 'i18next'; +import { + ensureHttpBotSigningSecret, + getErrorMessage, +} from '@/app/wizard/utils'; + import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { @@ -177,15 +182,30 @@ export default function WizardPage() { const botData = await httpClient.getBot(progress.created_bot_uuid); 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); setCreatedPipelineUuid(progress.created_pipeline_uuid ?? null); - setBotSaved(progress.bot_saved ?? false); + setBotSaved( + configNeedsSave ? false : (progress.bot_saved ?? false), + ); setMessageReceived(progress.message_received ?? false); setSelectedRunner(progress.selected_runner); // Restore bot name from fetched bot data setBotName(botData.bot.name); + setAdapterConfig(configToRestore); // Restore webhook URLs const runtimeValues = botData.bot.adapter_runtime_values as @@ -360,12 +380,17 @@ export default function WizardPage() { const defaultConfig = adapter ? getDefaultValues(adapter.spec.config) : {}; + const initialConfig = ensureHttpBotSigningSecret( + selectedAdapter, + defaultConfig, + ); + setAdapterConfig(initialConfig); const bot: Bot = { name: defaultName, description: '', adapter: selectedAdapter, - adapter_config: defaultConfig, + adapter_config: initialConfig, enable: false, }; const resp = await httpClient.createBot(bot); @@ -454,11 +479,17 @@ export default function WizardPage() { setCreatedPipelineUuid(pipelineUuid); } + const configToSave = ensureHttpBotSigningSecret( + selectedAdapter, + adapterConfig, + ); + setAdapterConfig(configToSave); + await httpClient.updateBot(createdBotUuid, { name: botName, description: botDescription || '', adapter: selectedAdapter, - adapter_config: adapterConfig, + adapter_config: configToSave, enable: true, use_pipeline_uuid: pipelineUuid, }); @@ -1063,7 +1094,7 @@ function StepBotConfig({ } catch (error) { toast.error( t('wizard.botConfig.httpTestFailed', { - error: error instanceof Error ? error.message : String(error), + error: getErrorMessage(error), }), ); } finally { diff --git a/web/src/app/wizard/utils.ts b/web/src/app/wizard/utils.ts new file mode 100644 index 000000000..1841d50b6 --- /dev/null +++ b/web/src/app/wizard/utils.ts @@ -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, +): Record { + if ( + adapterName !== 'http_bot' || + config.signature_required === false || + (typeof config.inbound_secret === 'string' && config.inbound_secret) + ) { + return config; + } + + return { + ...config, + inbound_secret: createSigningSecret(), + }; +} diff --git a/web/tests/unit/wizard-http-bot.test.mjs b/web/tests/unit/wizard-http-bot.test.mjs new file mode 100644 index 000000000..4a6edf936 --- /dev/null +++ b/web/tests/unit/wizard-http-bot.test.mjs @@ -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'); +});