test(web): exercise marketplace installs and wizard contracts

This commit is contained in:
huanghuoguoguo
2026-10-17 16:09:23 +08:00
parent c05aac538f
commit 8e447dae09
8 changed files with 1400 additions and 685 deletions
@@ -1,146 +1,264 @@
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';
import ts from 'typescript';
const currentDirectory = path.dirname(fileURLToPath(import.meta.url));
const webRoot = path.resolve(currentDirectory, '../..');
function readSource(relativePath) {
return fs.readFileSync(path.join(webRoot, relativePath), 'utf8');
function setup({
tasks = [{ runtime: { done: true } }],
engines = [{ engines: [] }],
cloud,
} = {}) {
const source = fs.readFileSync(
new URL(
'../../src/app/home/knowledge/components/kb-form/knowledge-engine-marketplace.ts',
import.meta.url,
),
'utf8',
);
const code = ts.transpileModule(source, {
compilerOptions: {
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2022,
},
}).outputText;
const storage = new Map(),
calls = [];
let workspace = 'alpha',
now = 0,
taskIndex = 0,
engineIndex = 0;
const http = {
getPlugins: async () => ({
plugins: [
{
manifest: {
manifest: { metadata: { author: 'qa', name: 'existing' } },
},
},
],
}),
installPluginFromMarketplace: async (...args) => {
calls.push(['install', ...args]);
return { task_id: 42 };
},
getAsyncTask: async (id) => {
calls.push(['task', id]);
return tasks[Math.min(taskIndex++, tasks.length - 1)];
},
getKnowledgeEngines: async () => {
calls.push(['engines']);
return engines[Math.min(engineIndex++, engines.length - 1)];
},
};
const deps = {
'@/app/infra/http': { getCloudServiceClient: async () => cloud },
'@/app/infra/http/HttpClient': { httpClient: http },
'@/app/infra/http/workspaceContext': {
getActiveWorkspaceUuid: () => workspace,
},
};
const mod = { exports: {} };
new Function(
'require',
'module',
'exports',
'window',
'sessionStorage',
'CustomEvent',
'Date',
'setTimeout',
code,
)(
(name) => {
assert.ok(name in deps, name);
return deps[name];
},
mod,
mod.exports,
new EventTarget(),
{
getItem: (key) => storage.get(key) ?? null,
setItem: (key, value) => storage.set(key, value),
removeItem: (key) => storage.delete(key),
},
CustomEvent,
{ now: () => now },
(callback, ms) => {
now += ms;
queueMicrotask(callback);
},
);
return {
api: mod.exports,
storage,
calls,
setWorkspace: (value) => {
workspace = value;
},
};
}
const plugin = {
author: 'qa',
name: 'engine',
label: { en_US: 'QA Engine' },
latest_version: '2.0.0',
};
const engine = { plugin_id: 'qa/engine', name: 'Registered QA Engine' };
const kbFormSource = readSource(
'src/app/home/knowledge/components/kb-form/KBForm.tsx',
);
const selectSource = readSource(
'src/app/home/knowledge/components/kb-form/KnowledgeEngineSelect.tsx',
);
const runnerSelectSource = readSource(
'src/app/home/agents/components/RunnerSelect.tsx',
);
const agentFormSource = readSource(
'src/app/home/agents/components/AgentFormComponent.tsx',
);
const pipelineFormSource = readSource(
'src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx',
);
const runnerMarketplaceSource = readSource(
'src/app/home/agents/runner-marketplace.ts',
);
const marketplaceInstallButtonSource = readSource(
'src/app/home/components/MarketplaceInstallButton.tsx',
);
const marketplaceSource = readSource(
'src/app/home/knowledge/components/kb-form/knowledge-engine-marketplace.ts',
);
const taskContextSource = readSource(
'src/app/home/plugins/components/plugin-install-task/PluginInstallTaskContext.tsx',
);
const progressDialogSource = readSource(
'src/app/home/plugins/components/plugin-install-task/PluginInstallProgressDialog.tsx',
);
const installedPluginsSource = readSource(
'src/app/home/plugins/components/plugin-installed/PluginInstalledComponent.tsx',
);
const homeSidebarSource = readSource(
'src/app/home/components/home-sidebar/HomeSidebar.tsx',
);
test('keeps the knowledge-base fields visible without an installed engine', () => {
assert.match(kbFormSource, /<KnowledgeEngineSelect/);
assert.match(kbFormSource, /name="name"/);
assert.match(kbFormSource, /name="description"/);
assert.doesNotMatch(kbFormSource, /if \(ragEngines\.length === 0\)/);
});
test('offers KnowledgeEngine marketplace plugins inside the selector', () => {
assert.match(
marketplaceSource,
/KNOWLEDGE_ENGINE_COMPONENT_FILTER = 'KnowledgeEngine'/,
test('catalog loads every page with the engine filter and excludes unrelated components', async () => {
const searches = [];
const { api } = setup({
cloud: {
searchMarketplaceExtensions: async (args) => {
searches.push(args);
return {
total: 201,
plugins:
args.page === 1
? [
{
...plugin,
components: { KnowledgeEngine: 1 },
install_count: 5,
},
]
: args.page === 2
? [
{
name: 'runner',
components: { Runner: 1 },
install_count: 999,
},
]
: [
{
...plugin,
name: 'popular',
components: { KnowledgeEngine: 1 },
install_count: 20,
},
],
};
},
},
});
const result = await api.loadKnowledgeEngineCatalog();
assert.deepEqual(
searches.map((value) => value.page),
[1, 2, 3],
);
assert.match(marketplaceSource, /installPluginFromMarketplace\(/);
assert.match(marketplaceSource, /getKnowledgeEngines\(\)/);
assert.match(marketplaceSource, /sessionStorage\.setItem\(/);
assert.match(selectSource, /resumePendingKnowledgeEngineInstall\(/);
assert.match(selectSource, /onInstalled\(installed\)/);
assert.match(selectSource, /component=KnowledgeEngine/);
});
test('tracks plugin upgrades as recoverable multistep async tasks', () => {
assert.match(taskContextSource, /name\.startsWith\('plugin-upgrade-'\)/);
assert.match(taskContextSource, /operation: PluginTaskOperation/);
assert.match(taskContextSource, /computeStageProgress/);
assert.match(taskContextSource, /INSTALL_PROGRESS_CAP/);
assert.match(progressDialogSource, /InstallStage\.CHECKING/);
assert.match(progressDialogSource, /InstallStage\.VALIDATING/);
assert.match(progressDialogSource, /InstallStage\.LAUNCHING/);
assert.match(progressDialogSource, /plugins\.installProgress\.updateTitle/);
for (const source of [installedPluginsSource, homeSidebarSource]) {
assert.match(
source,
/upgradePlugin\([\s\S]*?add(?:Plugin)?Task\([\s\S]*?operation: 'upgrade'/,
);
assert.match(
source,
/pluginTaskKey\(res\.task_id, 'marketplace', 'upgrade'\)/,
);
}
});
test('keeps marketplace install actions on one fixed vertical column', () => {
for (const source of [selectSource, runnerSelectSource]) {
assert.match(source, /grid-cols-\[1\.75rem_minmax\(0,1fr\)_4rem\]/);
assert.match(source, /<MarketplaceInstallButton/);
}
assert.match(marketplaceInstallButtonSource, /justify-self-end/);
});
test('uses a compact selected engine layout and clears stale required errors', () => {
assert.match(selectSource, /function SelectedEngineContent/);
assert.match(
selectSource,
/selectedEngine \? \([\s\S]*?<SelectedEngineContent engine=\{selectedEngine\}/,
for (const args of searches)
assert.deepEqual(args, {
query: '',
page: args.page,
page_size: 100,
type_filter: 'plugin',
component_filter: 'KnowledgeEngine',
});
assert.deepEqual(
result.marketplaceEngines.map((value) => value.name),
['popular', 'engine'],
);
assert.match(
selectSource,
/flex min-w-0 flex-1 items-center gap-2 text-left/,
);
assert.match(kbFormSource, /form\.clearErrors\('ragEngineId'\)/);
assert.match(kbFormSource, /form\.trigger\('ragEngineId'\)/);
assert.doesNotMatch(kbFormSource, /field\.onChange\(value\)/);
assert.deepEqual(result.installedPluginIds, ['qa/existing']);
});
test('installs marketplace components from inline progress buttons without selecting them', () => {
for (const source of [selectSource, runnerSelectSource]) {
assert.match(source, /const handleInstall = useCallback/);
assert.match(source, /installing=\{activePluginId === pluginId\}/);
assert.match(source, /progress=\{installProgress\}/);
assert.doesNotMatch(source, /MARKETPLACE_VALUE_PREFIX/);
}
assert.match(marketplaceInstallButtonSource, /<Progress/);
assert.match(marketplaceInstallButtonSource, /onPointerDown=/);
assert.match(kbFormSource, /suppressNextAutoSelectRef\.current = true/);
assert.doesNotMatch(
kbFormSource,
/handleEngineInstalled[\s\S]*?handleEngineChange\(engine\.plugin_id/,
);
for (const source of [agentFormSource, pipelineFormSource]) {
const callback = source.match(
/const applyInstalledRunner = useCallback\(([\s\S]*?)\n \);/,
);
assert.ok(callback);
assert.doesNotMatch(callback[1], /form\.setValue/);
}
test('installation persists a scoped task before polling and waits for component registration', async () => {
const { api, storage, calls } = setup({
tasks: [{ runtime: { done: false } }, { runtime: { done: true } }],
engines: [{ engines: [{ plugin_id: 'qa/other' }] }, { engines: [engine] }],
});
let created;
const installed = await api.installMarketplaceKnowledgeEngine(plugin, {
scope: 'kb:new',
onTaskCreated: (id) => {
created = id;
assert.equal(api.readPendingKnowledgeEngineInstall('kb:new').taskId, 42);
},
});
assert.equal(installed, engine);
assert.equal(created, 42);
assert.deepEqual(calls, [
['install', 'qa', 'engine', '2.0.0'],
['task', 42],
['task', 42],
['engines'],
['engines'],
]);
assert.equal(storage.size, 0);
});
test('shows plugin descriptions for installed Runner entries', () => {
assert.match(runnerMarketplaceSource, /installedPluginDescriptions/);
assert.match(runnerMarketplaceSource, /metadata\.description/);
assert.match(runnerSelectSource, /installedRunnerDescription\(/);
assert.match(runnerSelectSource, /function InstalledRunnerOptionContent/);
assert.match(runnerSelectSource, /marketplacePlugin\?\.description/);
assert.match(runnerSelectSource, /grid-cols-\[1\.75rem_minmax\(0,1fr\)\]/);
assert.doesNotMatch(runnerSelectSource, /description=\{option\.name\}/);
test('a pending engine install resumes only in its original workspace and form scope', async () => {
const { api, storage, calls, setWorkspace } = setup({
engines: [{ engines: [engine] }],
});
storage.set(
'langbot-knowledge-engine-install:alpha:kb:new',
JSON.stringify({
taskId: 42,
pluginId: 'qa/engine',
pluginAuthor: 'qa',
pluginName: 'engine',
pluginLabel: 'QA Engine',
scope: 'kb:new',
startedAt: 0,
}),
);
setWorkspace('beta');
assert.equal(await api.resumePendingKnowledgeEngineInstall('kb:new'), null);
setWorkspace('alpha');
assert.equal(await api.resumePendingKnowledgeEngineInstall('kb:other'), null);
assert.equal(await api.resumePendingKnowledgeEngineInstall('kb:new'), engine);
assert.deepEqual(calls, [['task', 42], ['engines']]);
assert.equal(storage.size, 0);
});
test('failed tasks clear their intent and never query component registration', async () => {
const { api, calls, storage } = setup({
tasks: [{ runtime: { done: true, exception: 'Package invalid' } }],
});
await assert.rejects(
api.installMarketplaceKnowledgeEngine(plugin, { scope: 'kb:new' }),
/Package invalid/,
);
assert.deepEqual(calls, [
['install', 'qa', 'engine', '2.0.0'],
['task', 42],
]);
assert.equal(storage.size, 0);
});
test('an unfinished task times out with a recoverable intent, while missing registration clears it', async () => {
const pending = setup({ tasks: [{ runtime: { done: false } }] });
await assert.rejects(
pending.api.installMarketplaceKnowledgeEngine(plugin, { scope: 'kb:new' }),
(error) => error.code === 'install-timeout',
);
assert.equal(
pending.api.readPendingKnowledgeEngineInstall('kb:new').taskId,
42,
);
assert.equal(
pending.calls.some((value) => value[0] === 'engines'),
false,
);
const unregistered = setup();
await assert.rejects(
unregistered.api.installMarketplaceKnowledgeEngine(plugin, {
scope: 'kb:new',
}),
(error) => error.code === 'registration-timeout',
);
assert.equal(unregistered.storage.size, 0);
});
test('a plugin without a version never starts an install task', async () => {
const { api, calls } = setup();
await assert.rejects(
api.installMarketplaceKnowledgeEngine(
{ ...plugin, latest_version: '' },
{ scope: 'kb:new' },
),
(error) => error.code === 'version-unavailable',
);
assert.deepEqual(calls, []);
});
+144 -73
View File
@@ -1,86 +1,157 @@
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';
import ts from 'typescript';
const webRoot = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
'../..',
);
const read = (relativePath) =>
fs.readFileSync(path.join(webRoot, relativePath), 'utf8');
function load(relative, deps, globals = {}) {
const source = fs.readFileSync(
new URL('../../src/' + relative, import.meta.url),
'utf8',
);
const code = ts.transpileModule(source, {
compilerOptions: {
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2022,
},
}).outputText;
const mod = { exports: {} };
new Function('require', 'module', 'exports', ...Object.keys(globals), code)(
(name) => {
assert.ok(name in deps, name);
return deps[name];
},
mod,
mod.exports,
...Object.values(globals),
);
return mod.exports;
}
test('marketplace defaults to shared hot sorting and cards support fingerprint likes', () => {
const market = read(
'src/app/home/plugins/components/plugin-market/PluginMarketComponent.tsx',
test('likes reuse the browser fingerprint, update cached state and notify only until unsubscribed', async () => {
const calls = [],
events = [];
const client = {
getMarketplaceLikedExtensions: async (fingerprint) => {
calls.push(['status', fingerprint]);
return {
extensions: [{ type: 'plugin', extension_id: 'qa/already-liked' }],
};
},
setMarketplaceExtensionLike: async (...args) => {
calls.push(['toggle', ...args]);
return { liked: args[4], like_count: args[4] ? 8 : 7 };
},
};
const api = load(
'app/home/plugins/components/plugin-market/marketplace-likes.ts',
{
'@fingerprintjs/fingerprintjs': {
default: {
load: async (options) => {
calls.push(['fingerprint', options]);
return { get: async () => ({ visitorId: 'browser-id' }) };
},
},
},
'@/app/infra/http': { getCloudServiceClient: async () => client },
},
{ window: new EventTarget(), CustomEvent },
);
const card = read(
'src/app/home/plugins/components/plugin-market/plugin-market-card/PluginMarketCardComponent.tsx',
assert.equal(
await api.getMarketplaceExtensionLiked(undefined, 'qa', 'already-liked'),
true,
);
const cardVO = read(
'src/app/home/plugins/components/plugin-market/plugin-market-card/PluginMarketCardVO.ts',
assert.equal(
await api.getMarketplaceExtensionLiked('skill', 'qa', 'already-liked'),
false,
);
const entity = read('src/app/infra/entities/plugin/index.ts');
const client = read('src/app/infra/http/CloudServiceClient.ts');
const likes = read(
'src/app/home/plugins/components/plugin-market/marketplace-likes.ts',
const unsubscribe = api.subscribeMarketplaceLikeChanges((change) =>
events.push(change),
);
assert.match(
market,
/hot_score_desc/,
'market must expose and default to hot-score sorting',
assert.deepEqual(
await api.toggleMarketplaceExtensionLike(undefined, 'qa', 'new', true),
{ key: 'plugin:qa/new', liked: true, likeCount: 8 },
);
assert.match(
market,
/sortBy:\s*['"]hot_score['"]/,
'hot option must use the shared API field',
assert.equal(
await api.getMarketplaceExtensionLiked('plugin', 'qa', 'new'),
true,
);
assert.match(
entity,
/like_count\??:\s*number/,
'marketplace extension responses must carry like counts',
);
assert.match(
cardVO,
/likeCount:\s*number/,
'market cards must carry like counts',
);
assert.match(card, /\bHeart\b/, 'market cards must render a like affordance');
assert.match(
card,
/toggleMarketplaceExtensionLike/,
'market cards must support liking and unliking',
);
assert.match(
client,
/marketplace\/extensions\/likes/,
'client must load browser likes',
);
assert.match(
client,
/setMarketplaceExtensionLike\(/,
'client must update likes through the shared API',
);
assert.match(
client,
/data\.sort_by === ['"]hot_score['"] \? ['"]install_count['"] : data\.sort_by/,
'older Space servers must fall back from hot score to install sorting',
);
assert.match(
likes,
/@fingerprintjs\/fingerprintjs/,
'anonymous likes must use browser fingerprinting',
);
assert.match(
likes,
/FingerprintJS\.load\(\{/,
'fingerprint agent must be initialized with options',
);
assert.match(
likes,
/monitoring:\s*false/,
'fingerprinting must not send vendor monitoring requests',
await api.toggleMarketplaceExtensionLike('plugin', 'qa', 'new', false);
assert.equal(
await api.getMarketplaceExtensionLiked('plugin', 'qa', 'new'),
false,
);
unsubscribe();
await api.toggleMarketplaceExtensionLike('skill', 'qa', 'new', true);
assert.deepEqual(events, [
{ key: 'plugin:qa/new', liked: true, likeCount: 8 },
{ key: 'plugin:qa/new', liked: false, likeCount: 7 },
]);
assert.deepEqual(calls, [
['fingerprint', { monitoring: false }],
['status', 'browser-id'],
['toggle', 'plugin', 'qa', 'new', 'browser-id', true],
['toggle', 'plugin', 'qa', 'new', 'browser-id', false],
['toggle', 'skill', 'qa', 'new', 'browser-id', true],
]);
});
test('legacy marketplace search converts hot sorting while preserving filters', async () => {
const calls = [];
class BaseHttpClient {
async post(path, body) {
calls.push([path, body]);
if (path === '/api/v1/marketplace/extensions/search')
throw new Error('Legacy server');
return {
plugins: [{ author: 'qa', name: 'legacy', type: 'plugin' }],
total: 1,
};
}
}
const { CloudServiceClient } = load('app/infra/http/CloudServiceClient.ts', {
'./BaseHttpClient': { BaseHttpClient },
'@/app/infra/entities/plugin': {},
});
const result = await new CloudServiceClient().searchMarketplaceExtensions({
query: 'needle',
page: 2,
page_size: 10,
sort_by: 'hot_score',
sort_order: 'DESC',
type_filter: 'plugin',
tags_filter: ['qa'],
});
assert.deepEqual(
result.plugins.map((value) => value.name),
['legacy'],
);
assert.deepEqual(calls, [
[
'/api/v1/marketplace/extensions/search',
{
query: 'needle',
page: 2,
page_size: 10,
sort_by: 'hot_score',
sort_order: 'DESC',
type_filter: 'plugin',
tags_filter: ['qa'],
},
],
[
'/api/v1/marketplace/plugins/search',
{
query: 'needle',
page: 2,
page_size: 10,
sort_by: 'install_count',
sort_order: 'DESC',
component_filter: undefined,
tags_filter: ['qa'],
type_filter: 'plugin',
runner_usage: undefined,
},
],
]);
});
+112 -118
View File
@@ -1,126 +1,120 @@
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';
import ts from 'typescript';
const currentDirectory = path.dirname(fileURLToPath(import.meta.url));
const wizardSource = fs.readFileSync(
path.resolve(currentDirectory, '../../src/app/wizard/page.tsx'),
'utf8',
);
const runnerMarketplaceSource = fs.readFileSync(
path.resolve(
currentDirectory,
'../../src/app/home/agents/runner-marketplace.ts',
),
'utf8',
);
const widgetSource = fs.readFileSync(
path.resolve(
currentDirectory,
'../../../src/langbot/templates/embed/widget.js',
),
'utf8',
);
function load(source, deps = {}, globals = {}) {
const code = ts.transpileModule(
fs.readFileSync(new URL('../../src/' + source, import.meta.url), 'utf8'),
{
compilerOptions: {
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2022,
},
},
).outputText;
const mod = { exports: {} };
new Function('require', 'module', 'exports', ...Object.keys(globals), code)(
(id) => {
assert.ok(id in deps, id);
return deps[id];
},
mod,
mod.exports,
...Object.values(globals),
);
return mod.exports;
}
test('shows the test-only notice only when the wizard opts in', () => {
assert.match(
wizardSource,
/widget\.js\?preview=wizard&v=\$\{Date\.now\(\)\}/,
test('wizard Runner recovery waits for the matching plugin registration without installing twice', async () => {
const scope = 'wizard-ai-runner';
const pending = {
scope,
taskId: 12,
pluginId: 'qa/runner',
pluginAuthor: 'qa',
pluginName: 'runner',
pluginLabel: 'QA Runner',
startedAt: -1000000,
};
const storage = new Map([
[`langbot-runner-install:alpha:${scope}`, JSON.stringify(pending)],
]);
const calls = [],
progress = [];
let workspace = 'beta',
tick = 0,
polls = 0;
const option = {
name: 'plugin:qa/runner/default',
label: { en_US: 'Registered Runner' },
};
const configTab = {
name: 'ai',
stages: [
{
name: 'runner',
config: [
{
name: 'id',
options: [{ name: 'plugin:qa/runner-neighbor/default' }],
},
],
},
],
};
const httpClient = {
getAsyncTask: async (id) => {
calls.push(['task', id]);
return { runtime: { done: ++polls >= 2 } };
},
getGeneralPipelineMetadata: async () => {
calls.push(['metadata']);
if (calls.filter((value) => value[0] === 'metadata').length === 2)
configTab.stages[0].config[0].options.push(option);
return { configs: [configTab] };
},
installPluginFromMarketplace: async () =>
assert.fail('recovery must not reinstall'),
};
const api = load(
'app/home/agents/runner-marketplace.ts',
{
'@/app/infra/http/HttpClient': { httpClient },
'@/app/infra/http': {},
'@/app/infra/http/workspaceContext': {
getActiveWorkspaceUuid: () => workspace,
},
'@/app/infra/entities/plugin': {},
},
{
window: new EventTarget(),
CustomEvent,
sessionStorage: {
getItem: (key) => storage.get(key) ?? null,
removeItem: (key) => storage.delete(key),
},
Date: { now: () => tick },
setTimeout: (callback, ms) => {
tick += ms;
queueMicrotask(callback);
},
},
);
assert.match(wizardSource, /script\.dataset\.testNotice = testNotice/);
assert.match(
wizardSource,
/testNotice=\{t\('wizard\.botConfig\.pageBotTestNotice'\)\}/,
assert.equal(await api.resumePendingRunnerInstall(scope), null);
workspace = 'alpha';
assert.equal(await api.resumePendingRunnerInstall('other'), null);
const recovered = await api.resumePendingRunnerInstall(scope, (task) =>
progress.push(task.runtime.done),
);
assert.match(widgetSource, /getAttribute\("data-test-notice"\)/);
assert.match(widgetSource, /if \(scriptTestNotice\)/);
assert.match(widgetSource, /testNotice\.textContent = scriptTestNotice/);
});
test('opens the Page Bot preview after every successful wizard save', () => {
assert.match(wizardSource, /script\.dataset\.autoOpen = 'true'/);
assert.match(
wizardSource,
/setPageBotPreviewRequest\(\(request\) => request \+ 1\)/,
);
assert.match(widgetSource, /getAttribute\("data-auto-open"\) === "true"/);
assert.match(widgetSource, /root\.langbotOpen = function \(\)/);
assert.match(widgetSource, /if \(scriptAutoOpen\) root\.langbotOpen\(\)/);
});
test('binds every wizard bot to its provisional pipeline before verification', () => {
assert.match(
wizardSource,
/if \(!previewPipelineUuid\) \{[\s\S]*?httpClient\.createPipeline\(/,
);
assert.match(
wizardSource,
/event_pattern: 'message\.received',[\s\S]*?target_type: 'pipeline',[\s\S]*?target_uuid: previewPipelineUuid/,
);
assert.doesNotMatch(
wizardSource,
/selectedAdapter === 'web_page_bot' && !previewPipelineUuid/,
);
});
test('keeps the 4.11 Runner marketplace installation flow', () => {
assert.match(wizardSource, /loadRunnerCatalog\(\)/);
assert.match(
wizardSource,
/installMarketplaceRunner\(plugin, \{[\s\S]*?scope: WIZARD_RUNNER_INSTALL_SCOPE/,
);
assert.match(wizardSource, /resumePendingRunnerInstall\(/);
assert.match(runnerMarketplaceSource, /RUNNER_COMPONENT_FILTER = 'Runner'/);
assert.match(runnerMarketplaceSource, /installPluginFromMarketplace\(/);
assert.match(
runnerMarketplaceSource,
/const prefix = runnerPluginPrefix\(\{/,
);
assert.match(runnerMarketplaceSource, /option\.name\.startsWith\(prefix\)/);
assert.match(runnerMarketplaceSource, /registrationDeadline/);
assert.match(runnerMarketplaceSource, /sessionStorage\.setItem\(/);
});
test('requires the selected Runner mandatory configuration before finishing', () => {
assert.match(
wizardSource,
/isRequiredRunnerConfigComplete\(selectedRunnerConfigItems, runnerConfig\)/,
);
assert.match(wizardSource, /aiChoice === 'external'/);
assert.match(
wizardSource,
/!createdBotUuid \|\| !createdPipelineUuid \|\| !canProceed\(\)/,
);
});
test('requires an observed message before configuring the runner', () => {
assert.match(
wizardSource,
/createdBotUuid !== null && botSaved && messageReceived/,
);
assert.match(wizardSource, /requiresMessageVerification/);
assert.match(wizardSource, /onMessageReceived=\{handleMessageReceived\}/);
});
test('warns local-account users after the bot receives an IM message', () => {
assert.match(
wizardSource,
/messageReceived && userInfo\?\.account_type !== 'space'/,
);
assert.match(
wizardSource,
/wizard\.botConfig\.messageReceivedLocalAccountWarning/,
);
assert.match(wizardSource, /<AlertTriangle className="size-3 text-white"/);
});
test('offers the HTTP Bot test through the signed inbound API', () => {
assert.match(
wizardSource,
/testHttpBotInbound\(createdBotUuid, testMessage\.trim\(\)\)/,
);
assert.match(wizardSource, /wizard\.botConfig\.sendHttpTest/);
assert.equal(recovered.runner, option);
assert.equal(recovered.configTab, configTab);
assert.deepEqual(progress, [false, true]);
assert.deepEqual(calls, [
['task', 12],
['task', 12],
['metadata'],
['metadata'],
]);
assert.equal(storage.size, 0);
});