mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-27 03:46:39 +08:00
fix(migration): improve runner migration and plugin installation feedback
This commit is contained in:
@@ -7,11 +7,11 @@
|
||||
"name": "model",
|
||||
"description": {
|
||||
"en_US": "Primary/fallback model UUIDs and per-model reasoning levels. Host validates and applies reasoning for every request; this is per-pipeline configuration.",
|
||||
"zh_Hans": "\u4e3b\u6a21\u578b/\u5907\u7528\u6a21\u578b UUID \u4e0e\u5404\u6a21\u578b\u63a8\u7406\u7b49\u7ea7\u3002\u7531 Host \u5bf9\u6bcf\u6b21\u8bf7\u6c42\u6821\u9a8c\u5e76\u5e94\u7528\uff0c\u5c5e\u4e8e\u6d41\u6c34\u7ebf\u72ec\u7acb\u914d\u7f6e\u3002"
|
||||
"zh_Hans": "主模型/备用模型 UUID 与各模型推理等级。由 Host 对每次请求校验并应用,属于流水线独立配置。"
|
||||
},
|
||||
"label": {
|
||||
"en_US": "Model",
|
||||
"zh_Hans": "\u6a21\u578b"
|
||||
"zh_Hans": "模型"
|
||||
},
|
||||
"type": "model-fallback-selector",
|
||||
"required": true,
|
||||
@@ -25,7 +25,7 @@
|
||||
"name": "prompt",
|
||||
"label": {
|
||||
"en_US": "Prompt",
|
||||
"zh_Hans": "\u63d0\u793a\u8bcd"
|
||||
"zh_Hans": "提示词"
|
||||
},
|
||||
"type": "prompt-editor",
|
||||
"required": true,
|
||||
@@ -40,21 +40,122 @@
|
||||
"name": "knowledge-bases",
|
||||
"label": {
|
||||
"en_US": "Knowledge Bases",
|
||||
"zh_Hans": "\u77e5\u8bc6\u5e93"
|
||||
"zh_Hans": "知识库"
|
||||
},
|
||||
"type": "knowledge-base-multi-selector",
|
||||
"required": false,
|
||||
"default": []
|
||||
},
|
||||
{
|
||||
"name": "box-enabled",
|
||||
"label": {
|
||||
"en_US": "Sandbox",
|
||||
"zh_Hans": "沙箱"
|
||||
},
|
||||
"description": {
|
||||
"en_US": "Use Box for code execution and file processing.",
|
||||
"zh_Hans": "使用 Box 执行代码和处理文件。"
|
||||
},
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"name": "box-session-id-template",
|
||||
"label": {
|
||||
"en_US": "Sandbox reuse",
|
||||
"zh_Hans": "沙箱复用范围"
|
||||
},
|
||||
"description": {
|
||||
"en_US": "Choose how sandbox files and environments are shared. Custom templates substitute {variable_name}, for example {launcher_type}_{launcher_id}_{sender_id}. Equal results reuse the same sandbox; {global} shares one sandbox throughout the workspace. Available variables: {launcher_type}, {launcher_id}, {sender_id}, {conversation_id}, {bot_id}, {query_id}, {run_id}, {event_id}, {event_type}, and public request variables. Only named variables are supported, not expressions or attribute access; unavailable variables cause an error.",
|
||||
"zh_Hans": "选择沙箱文件和环境的共享方式。自定义模板使用 {变量名} 插值,如 {launcher_type}_{launcher_id}_{sender_id};插值结果相同就复用同一个沙箱,{global} 表示在工作区内全局共享。可用变量:{launcher_type}(会话类型)、{launcher_id}(会话 ID)、{sender_id}(用户 ID)、{conversation_id}(对话上下文 ID)、{bot_id}(机器人 ID)、{query_id}(请求 ID)、{run_id}(运行 ID)、{event_id}(事件 ID)、{event_type}(事件类型),也可引用公开请求变量。仅支持变量名,不支持运算或属性访问;变量不可用时会报错。"
|
||||
},
|
||||
"type": "select",
|
||||
"allow_custom": true,
|
||||
"required": true,
|
||||
"default": "{launcher_type}_{launcher_id}",
|
||||
"options": [
|
||||
{
|
||||
"name": "{global}",
|
||||
"label": {
|
||||
"en_US": "Global (shared by all)",
|
||||
"zh_Hans": "全局(所有人共享)",
|
||||
"zh_Hant": "全域(所有人共用)",
|
||||
"ja_JP": "グローバル(全員共有)",
|
||||
"vi_VN": "Toàn cục (chia sẻ cho tất cả)",
|
||||
"th_TH": "ทั่วไป (แชร์ทั้งหมด)",
|
||||
"es_ES": "Global (compartido por todos)",
|
||||
"ru_RU": "Глобальный (общий для всех)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "{launcher_type}_{launcher_id}",
|
||||
"label": {
|
||||
"en_US": "Per chat (Recommended)",
|
||||
"zh_Hans": "每个会话(推荐)",
|
||||
"zh_Hant": "每個會話(推薦)",
|
||||
"ja_JP": "チャットごと(推奨)",
|
||||
"vi_VN": "Mỗi cuộc trò chuyện (Khuyến nghị)",
|
||||
"th_TH": "ต่อแชท (แนะนำ)",
|
||||
"es_ES": "Por chat (Recomendado)",
|
||||
"ru_RU": "По чату (Рекомендуется)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "{launcher_type}_{launcher_id}_{sender_id}",
|
||||
"label": {
|
||||
"en_US": "Per user in chat",
|
||||
"zh_Hans": "会话中每个用户",
|
||||
"zh_Hant": "會話中每個用戶",
|
||||
"ja_JP": "チャット内のユーザーごと",
|
||||
"vi_VN": "Mỗi người dùng trong cuộc trò chuyện",
|
||||
"th_TH": "ต่อผู้ใช้ในแชท",
|
||||
"es_ES": "Por usuario en chat",
|
||||
"ru_RU": "По пользователю в чате"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "{launcher_type}_{launcher_id}_{conversation_id}",
|
||||
"label": {
|
||||
"en_US": "Per conversation context",
|
||||
"zh_Hans": "每个对话上下文",
|
||||
"zh_Hant": "每個對話上下文",
|
||||
"ja_JP": "会話コンテキストごと",
|
||||
"vi_VN": "Mỗi ngữ cảnh hội thoại",
|
||||
"th_TH": "ต่อบริบทการสนทนา",
|
||||
"es_ES": "Por contexto de conversación",
|
||||
"ru_RU": "По контексту разговора"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "{query_id}",
|
||||
"label": {
|
||||
"en_US": "Per message (isolated)",
|
||||
"zh_Hans": "每条消息(完全隔离)",
|
||||
"zh_Hant": "每條訊息(完全隔離)",
|
||||
"ja_JP": "メッセージごと(隔離)",
|
||||
"vi_VN": "Mỗi tin nhắn (cách ly)",
|
||||
"th_TH": "ต่อข้อความ (แยกส่วน)",
|
||||
"es_ES": "Por mensaje (aislado)",
|
||||
"ru_RU": "По сообщению (изолированно)"
|
||||
}
|
||||
}
|
||||
],
|
||||
"show_if": {
|
||||
"field": "box-enabled",
|
||||
"operator": "eq",
|
||||
"value": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "advanced-settings",
|
||||
"label": {
|
||||
"en_US": "Advanced Settings",
|
||||
"zh_Hans": "\u9ad8\u7ea7\u8bbe\u7f6e"
|
||||
"zh_Hans": "高级设置"
|
||||
},
|
||||
"description": {
|
||||
"en_US": "Show tuning controls for retrieval, tools, timeouts, and context management.",
|
||||
"zh_Hans": "\u663e\u793a\u68c0\u7d22\u3001\u5de5\u5177\u3001\u8d85\u65f6\u548c\u4e0a\u4e0b\u6587\u7ba1\u7406\u7684\u8c03\u4f18\u9009\u9879\u3002"
|
||||
"zh_Hans": "显示检索、工具、超时和上下文管理的调优选项。"
|
||||
},
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
@@ -64,11 +165,11 @@
|
||||
"name": "date-grounding",
|
||||
"label": {
|
||||
"en_US": "Current Date Grounding",
|
||||
"zh_Hans": "\u5f53\u524d\u65e5\u671f\u951a\u5b9a"
|
||||
"zh_Hans": "当前日期锚定"
|
||||
},
|
||||
"description": {
|
||||
"en_US": "Add the current UTC date and a reminder to verify time-sensitive facts with available search tools.",
|
||||
"zh_Hans": "\u6ce8\u5165\u5f53\u524d UTC \u65e5\u671f\uff0c\u5e76\u63d0\u9192\u4f7f\u7528\u53ef\u7528\u641c\u7d22\u5de5\u5177\u6838\u5b9e\u6709\u65f6\u6548\u6027\u7684\u4fe1\u606f\u3002"
|
||||
"zh_Hans": "注入当前 UTC 日期,并提醒使用可用搜索工具核实有时效性的信息。"
|
||||
},
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
@@ -83,7 +184,7 @@
|
||||
"name": "timeout",
|
||||
"label": {
|
||||
"en_US": "Timeout",
|
||||
"zh_Hans": "\u6267\u884c\u8d85\u65f6"
|
||||
"zh_Hans": "执行超时"
|
||||
},
|
||||
"type": "integer",
|
||||
"required": false,
|
||||
@@ -98,7 +199,7 @@
|
||||
"name": "remove-think",
|
||||
"label": {
|
||||
"en_US": "Remove Thinking Output",
|
||||
"zh_Hans": "\u79fb\u9664\u601d\u8003\u5185\u5bb9"
|
||||
"zh_Hans": "移除思考内容"
|
||||
},
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
@@ -113,7 +214,7 @@
|
||||
"name": "retrieval-top-k",
|
||||
"label": {
|
||||
"en_US": "Retrieval Top K",
|
||||
"zh_Hans": "\u77e5\u8bc6\u5e93\u68c0\u7d22\u6570\u91cf"
|
||||
"zh_Hans": "知识库检索数量"
|
||||
},
|
||||
"type": "integer",
|
||||
"required": false,
|
||||
@@ -128,7 +229,7 @@
|
||||
"name": "rerank-model",
|
||||
"label": {
|
||||
"en_US": "Rerank Model",
|
||||
"zh_Hans": "\u91cd\u6392\u5e8f\u6a21\u578b"
|
||||
"zh_Hans": "重排序模型"
|
||||
},
|
||||
"type": "rerank-model-selector",
|
||||
"required": false,
|
||||
@@ -143,7 +244,7 @@
|
||||
"name": "rerank-top-k",
|
||||
"label": {
|
||||
"en_US": "Rerank Top K",
|
||||
"zh_Hans": "\u91cd\u6392\u5e8f\u4fdd\u7559\u6570\u91cf"
|
||||
"zh_Hans": "重排序保留数量"
|
||||
},
|
||||
"type": "integer",
|
||||
"required": false,
|
||||
@@ -158,7 +259,7 @@
|
||||
"name": "max-tool-iterations",
|
||||
"label": {
|
||||
"en_US": "Max Tool Iterations",
|
||||
"zh_Hans": "\u6700\u5927\u5de5\u5177\u8c03\u7528\u8f6e\u6570"
|
||||
"zh_Hans": "最大工具调用轮数"
|
||||
},
|
||||
"type": "integer",
|
||||
"required": false,
|
||||
@@ -173,11 +274,11 @@
|
||||
"name": "tool-execution-mode",
|
||||
"description": {
|
||||
"en_US": "Parallel is faster for independent tools. Use serial for dependent or side-effecting actions; result order does not guarantee execution order.",
|
||||
"zh_Hans": "\u5e76\u884c\u9002\u5408\u72ec\u7acb\u5de5\u5177\uff1b\u6709\u4f9d\u8d56\u6216\u526f\u4f5c\u7528\u7684\u64cd\u4f5c\u8bf7\u9009\u62e9\u4e32\u884c\u3002\u7ed3\u679c\u6392\u5217\u4e0d\u4fdd\u8bc1\u6267\u884c\u987a\u5e8f\u3002"
|
||||
"zh_Hans": "并行适合独立工具;有依赖或副作用的操作请选择串行。结果排列不保证执行顺序。"
|
||||
},
|
||||
"label": {
|
||||
"en_US": "Tool Execution Mode",
|
||||
"zh_Hans": "\u5de5\u5177\u6267\u884c\u6a21\u5f0f"
|
||||
"zh_Hans": "工具执行模式"
|
||||
},
|
||||
"type": "select",
|
||||
"required": false,
|
||||
@@ -187,14 +288,14 @@
|
||||
"name": "parallel",
|
||||
"label": {
|
||||
"en_US": "Parallel",
|
||||
"zh_Hans": "\u5e76\u884c"
|
||||
"zh_Hans": "并行"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "serial",
|
||||
"label": {
|
||||
"en_US": "Serial",
|
||||
"zh_Hans": "\u4e32\u884c"
|
||||
"zh_Hans": "串行"
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -208,7 +309,7 @@
|
||||
"name": "max-tool-result-chars",
|
||||
"label": {
|
||||
"en_US": "Max Tool Result Characters",
|
||||
"zh_Hans": "\u6700\u5927\u5de5\u5177\u7ed3\u679c\u5b57\u7b26\u6570"
|
||||
"zh_Hans": "最大工具结果字符数"
|
||||
},
|
||||
"type": "integer",
|
||||
"required": false,
|
||||
@@ -223,7 +324,7 @@
|
||||
"name": "context-history-fetch-limit",
|
||||
"label": {
|
||||
"en_US": "History Fetch Limit",
|
||||
"zh_Hans": "\u5386\u53f2\u6d88\u606f\u62c9\u53d6\u6570\u91cf"
|
||||
"zh_Hans": "历史消息拉取数量"
|
||||
},
|
||||
"type": "integer",
|
||||
"required": false,
|
||||
@@ -238,7 +339,7 @@
|
||||
"name": "context-window-tokens",
|
||||
"label": {
|
||||
"en_US": "Context Window Tokens",
|
||||
"zh_Hans": "\u4e0a\u4e0b\u6587\u7a97\u53e3 Token \u6570"
|
||||
"zh_Hans": "上下文窗口 Token 数"
|
||||
},
|
||||
"type": "integer",
|
||||
"required": false,
|
||||
@@ -253,7 +354,7 @@
|
||||
"name": "context-reserve-tokens",
|
||||
"label": {
|
||||
"en_US": "Reserved Output Tokens",
|
||||
"zh_Hans": "\u8f93\u51fa\u4fdd\u7559 Token \u6570"
|
||||
"zh_Hans": "输出保留 Token 数"
|
||||
},
|
||||
"type": "integer",
|
||||
"required": false,
|
||||
@@ -268,7 +369,7 @@
|
||||
"name": "context-keep-recent-tokens",
|
||||
"label": {
|
||||
"en_US": "Recent Context Tokens",
|
||||
"zh_Hans": "\u6700\u8fd1\u4e0a\u4e0b\u6587 Token \u6570"
|
||||
"zh_Hans": "最近上下文 Token 数"
|
||||
},
|
||||
"type": "integer",
|
||||
"required": false,
|
||||
@@ -283,7 +384,7 @@
|
||||
"name": "context-summary-tokens",
|
||||
"label": {
|
||||
"en_US": "Summary Tokens",
|
||||
"zh_Hans": "\u6458\u8981 Token \u6570"
|
||||
"zh_Hans": "摘要 Token 数"
|
||||
},
|
||||
"type": "integer",
|
||||
"required": false,
|
||||
|
||||
@@ -864,3 +864,90 @@ async def test_all_mode_rejects_duplicate_tasks_and_unconfirmed_requests(env):
|
||||
with pytest.raises(env.m.MigrationError, match='migration_running'):
|
||||
await execute_all(env)
|
||||
env.svc._all_tasks.clear()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize('stage', ['discovery', 'install', 'validation'])
|
||||
async def test_disconnected_runtime_has_specific_error_without_modifying_sources(env, stage):
|
||||
from langbot.pkg.plugin.connector import PluginRuntimeNotConnectedError
|
||||
|
||||
error = PluginRuntimeNotConnectedError('private connection diagnostics')
|
||||
if stage == 'discovery':
|
||||
env.ap.runner_registry.list_runners.side_effect = error
|
||||
elif stage == 'install':
|
||||
async with env.engine.begin() as conn:
|
||||
await conn.execute(sa.delete(PluginSetting))
|
||||
env.ap.plugin_connector.install_plugin = AsyncMock(side_effect=error)
|
||||
else:
|
||||
env.svc._verify_runtime = AsyncMock(side_effect=error)
|
||||
_, metadata = await execute_all(env)
|
||||
assert all(r['code'] == 'plugin_runtime_unavailable' for r in metadata['results'])
|
||||
assert 'private connection diagnostics' not in str(metadata)
|
||||
configs, backups = await rows(env)
|
||||
assert configs['one'] == SOURCE and configs['two'] == SOURCE
|
||||
assert not backups
|
||||
|
||||
|
||||
def test_install_progress_records_stages_without_exposing_diagnostics():
|
||||
from langbot.pkg.api.http.service.pipeline_migration import MigrationInstallContext
|
||||
|
||||
ctx = MigrationInstallContext({'author': 'team', 'name': 'Runner', 'version': '1.0'})
|
||||
ctx.trace('private-token', action='downloading plugin package')
|
||||
ctx.metadata.update(download_current=120, download_total=240, progress_percent=15, url='private-token')
|
||||
ctx.set_current_action('validating plugin package')
|
||||
ctx.metadata['progress_percent'] = float('nan')
|
||||
ctx.finish('plugin_install_failed')
|
||||
assert ctx.progress['download_current'] == 120
|
||||
assert ctx.progress['progress_percent'] == 15
|
||||
assert [step['stage'] for step in ctx.progress['steps']] == ['checking', 'downloading', 'validating']
|
||||
assert all(step['finished_at'] is not None for step in ctx.progress['steps'])
|
||||
assert ctx.progress['status'] == 'failed'
|
||||
assert 'private-token' not in str(ctx.progress)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_install_progress_visible_while_download_is_running(env):
|
||||
import asyncio
|
||||
|
||||
async with env.engine.begin() as conn:
|
||||
await conn.execute(sa.delete(PluginSetting))
|
||||
started, release = asyncio.Event(), asyncio.Event()
|
||||
|
||||
async def install(source, info, task_context):
|
||||
task_context.set_current_action('downloading plugin package')
|
||||
task_context.metadata.update(download_current=123, download_total=456)
|
||||
started.set()
|
||||
await release.wait()
|
||||
raise RuntimeError('private-token')
|
||||
|
||||
env.ap.plugin_connector.install_plugin = AsyncMock(side_effect=install)
|
||||
response = await env.svc.execute(context(), {'confirmed': True, 'all': True, 'install_plugins': True})
|
||||
task = env.ap.task_mgr.get_task_by_id(response['task_id'])
|
||||
try:
|
||||
await asyncio.wait_for(started.wait(), 3)
|
||||
await asyncio.sleep(0.25)
|
||||
progress = task.task_context.metadata['installations'][0]
|
||||
assert progress['status'] == 'installing'
|
||||
assert progress['download_current'] == 123
|
||||
assert progress['stage'] == 'downloading'
|
||||
finally:
|
||||
release.set()
|
||||
await task.task
|
||||
assert len(task.task_context.metadata['installations']) == 1
|
||||
assert progress['status'] == 'failed'
|
||||
assert 'private-token' not in str(task.task_context.to_dict())
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_missing_marketplace_release_has_specific_migration_result(env):
|
||||
from langbot.pkg.plugin.connector import MarketplacePluginVersionNotFoundError
|
||||
|
||||
async with env.engine.begin() as conn:
|
||||
await conn.execute(sa.delete(PluginSetting))
|
||||
env.ap.plugin_connector.install_plugin = AsyncMock(
|
||||
side_effect=MarketplacePluginVersionNotFoundError('private-token')
|
||||
)
|
||||
_, metadata = await execute_all(env)
|
||||
assert all(r['code'] == 'plugin_version_unavailable' for r in metadata['results'])
|
||||
assert metadata['installations'][0]['code'] == 'plugin_version_unavailable'
|
||||
assert 'private-token' not in str(metadata)
|
||||
|
||||
@@ -168,3 +168,76 @@ async def test_local_descriptor_migrates_through_detached_task(local, monkeypatc
|
||||
assert configs['one'] == plan['config']
|
||||
assert len(snapshots) == 1 and snapshots[0]['source_snapshot']['config'] == base.SOURCE
|
||||
env.ap.pipeline_mgr.publish_pipeline.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
'template',
|
||||
[
|
||||
None,
|
||||
'{global}',
|
||||
'{launcher_type}_{launcher_id}',
|
||||
'{launcher_type}_{launcher_id}_{sender_id}',
|
||||
'{launcher_type}_{launcher_id}_{conversation_id}',
|
||||
'{query_id}',
|
||||
'{bot_id}_{launcher_type}_{launcher_id}',
|
||||
'project-{project}',
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize('install_plugins', [False, True])
|
||||
async def test_box_templates_roundtrip_in_both_migration_modes(local, monkeypatch, template, install_plugins):
|
||||
from langbot.pkg.pipeline.legacy_config_migration import plan_legacy_pipeline
|
||||
|
||||
env, descriptor, _, _ = local
|
||||
monkeypatch.setattr(env.m, 'plan_legacy_pipeline', plan_legacy_pipeline)
|
||||
section = {'model': 'model', 'prompt': [], 'enable-all-tools': False, 'tools': []}
|
||||
if template is not None:
|
||||
section['box-session-id-template'] = template
|
||||
source = {'ai': {'runner': {'runner': 'local-agent'}, 'local-agent': section}}
|
||||
plan = plan_legacy_pipeline(source)
|
||||
descriptor.plugin_version = plan['target_plugin']['version']
|
||||
async with env.engine.begin() as conn:
|
||||
await conn.execute(
|
||||
sa.update(base.LegacyPipeline).where(base.LegacyPipeline.uuid == 'one').values(config=source)
|
||||
)
|
||||
await conn.execute(sa.delete(base.LegacyPipeline).where(base.LegacyPipeline.uuid == 'two'))
|
||||
if install_plugins:
|
||||
await conn.execute(
|
||||
sa.insert(base.PluginSetting).values(
|
||||
workspace_uuid=WS,
|
||||
plugin_author='langbot-team',
|
||||
plugin_name='LocalAgent',
|
||||
enabled=True,
|
||||
)
|
||||
)
|
||||
if not install_plugins:
|
||||
env.ap.runner_registry.list_runners.side_effect = AssertionError('Data-only must work offline')
|
||||
env.ap.plugin_connector.install_plugin = AsyncMock(side_effect=AssertionError('Data-only must not install'))
|
||||
_, result = await base.execute_all(env, install_plugins=install_plugins)
|
||||
assert result['results'] == [
|
||||
{
|
||||
'pipeline_uuid': 'one',
|
||||
'state': 'migrated',
|
||||
'code': None if install_plugins else 'data_only',
|
||||
}
|
||||
]
|
||||
configs, snapshots = await base.rows(env)
|
||||
saved = configs['one']['ai']['runner_config'][plan['target_runner_id']]
|
||||
effective = {field['name']: field['default'] for field in descriptor.config_schema if 'default' in field}
|
||||
effective.update(saved)
|
||||
assert effective['box-enabled'] is True
|
||||
assert effective['box-session-id-template'] == (template or '{launcher_type}_{launcher_id}')
|
||||
assert snapshots[0]['source_snapshot']['config'] == source
|
||||
assert 'box-session-id-template' not in configs['one']['ai']
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize('allow_custom', [False, None, 'true'])
|
||||
async def test_select_requires_explicit_custom_support(local, allow_custom):
|
||||
env, descriptor, config, plan = local
|
||||
field = next(f for f in descriptor.config_schema if f['name'] == 'box-session-id-template')
|
||||
field['allow_custom'] = allow_custom
|
||||
config['box-session-id-template'] = '{bot_id}_{launcher_id}'
|
||||
async with env.pm.tenant_scope(WS):
|
||||
with pytest.raises(env.m.MigrationError, match='runner_schema_incompatible'):
|
||||
await env.svc._verify_runtime(ExecutionContext.from_request(context()), plan)
|
||||
|
||||
@@ -992,3 +992,13 @@ def test_local_box_reuse_templates_are_preserved_for_plugin(template):
|
||||
result = plan(source)
|
||||
assert result['state'] != 'blocked', result
|
||||
assert result['config']['ai']['runner_config'][result['target_runner_id']]['box-session-id-template'] == template
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'template', ['', ' ', '{}', '{0}', '{sender_id', '{sender_id!r}', '{query_id:04}', '{actor.id}', '{actor[id]}']
|
||||
)
|
||||
def test_invalid_box_templates_are_reported_before_migration(template):
|
||||
source = source_for('local-agent')
|
||||
source['ai']['local-agent']['box-session-id-template'] = template
|
||||
result = plan(source)
|
||||
assert_block(result, 'local.box_template_invalid', 'ai.local-agent.box-session-id-template')
|
||||
|
||||
@@ -62,4 +62,6 @@ def test_empty_box_template_requires_correction():
|
||||
source['ai']['local-agent']['box-session-id-template'] = ''
|
||||
result = plan_legacy_pipeline(source)
|
||||
assert result['state'] == 'blocked'
|
||||
assert {'code': 'invalid_type', 'field': 'ai.local-agent.box-session-id-template'} in result['blockers']
|
||||
assert {'code': 'local.box_template_invalid', 'field': 'ai.local-agent.box-session-id-template'} in result[
|
||||
'blockers'
|
||||
]
|
||||
|
||||
@@ -163,3 +163,32 @@ async def test_marketplace_response_reader_is_bounded():
|
||||
|
||||
with pytest.raises(ValueError, match='exceeds'):
|
||||
await _read_httpx_response_limited(response, max_bytes=4)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_marketplace_missing_release_is_classified_without_upstream_message():
|
||||
from langbot.pkg.plugin.connector import _marketplace_get, MarketplacePluginVersionNotFoundError
|
||||
|
||||
async with httpx.AsyncClient(
|
||||
transport=httpx.MockTransport(
|
||||
lambda r: httpx.Response(500, json={'msg': 'plugin version not found: record not found private-token'})
|
||||
)
|
||||
) as client:
|
||||
with pytest.raises(MarketplacePluginVersionNotFoundError) as exc:
|
||||
await _marketplace_get(client, 'https://market.test/download', max_bytes=4096)
|
||||
assert 'private-token' not in str(exc.value)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_marketplace_download_reports_byte_progress():
|
||||
from langbot.pkg.plugin.connector import _marketplace_get
|
||||
from langbot.pkg.core.taskmgr import TaskContext
|
||||
|
||||
ctx = TaskContext.new()
|
||||
async with httpx.AsyncClient(
|
||||
transport=httpx.MockTransport(lambda r: httpx.Response(200, content=b'x' * 1024))
|
||||
) as client:
|
||||
status, body = await _marketplace_get(client, 'https://market.test/download', max_bytes=2048, task_context=ctx)
|
||||
assert status == 200 and len(body) == 1024
|
||||
assert ctx.metadata['download_current'] == ctx.metadata['download_total'] == 1024
|
||||
assert ctx.metadata['download_speed'] > 0
|
||||
|
||||
Reference in New Issue
Block a user