mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-24 09:25:21 +08:00
test(web): exercise marketplace installs and wizard contracts
This commit is contained in:
@@ -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, []);
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user