import { Page, Route } from '@playwright/test'; type JsonRecord = Record; interface SkillMock { name: string; display_name: string; description: string; instructions: string; package_root: string; updated_at: string; } interface PipelineMock { uuid: string; name: string; description: string; config: JsonRecord; emoji: string; is_default: boolean; updated_at: string; } interface KnowledgeBaseMock { uuid: string; name: string; description: string; emoji: string; knowledge_engine_plugin_id: string; creation_settings: JsonRecord; retrieval_settings: JsonRecord; knowledge_engine: { plugin_id: string; name: { en_US: string; zh_Hans: string; }; capabilities: string[]; }; updated_at: string; } interface MCPServerMock { name: string; mode: 'sse' | 'stdio' | 'http'; enable: boolean; extra_args: JsonRecord; runtime_info: { status: 'connected'; tool_count: number; tools: unknown[]; }; readme: string; updated_at: string; } interface LangBotApiMockState { counters: Record; knowledgeBases: KnowledgeBaseMock[]; mcpServers: MCPServerMock[]; pipelines: PipelineMock[]; skills: SkillMock[]; } function ok(data: unknown) { return { code: 0, message: 'ok', data, timestamp: Date.now(), }; } async function fulfillJson(route: Route, data: unknown) { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(ok(data)), }); } function routePath(route: Route) { return new URL(route.request().url()).pathname; } function parseJsonBody(route: Route): JsonRecord { return JSON.parse(route.request().postData() || '{}') as JsonRecord; } function now() { return new Date().toISOString(); } function nextId(state: LangBotApiMockState, prefix: string) { state.counters[prefix] = (state.counters[prefix] || 0) + 1; return `${prefix}-${state.counters[prefix]}`; } function emptyMonitoringData() { return { overview: { total_messages: 0, llm_calls: 0, embedding_calls: 0, model_calls: 0, success_rate: 0, active_sessions: 0, }, messages: [], llmCalls: [], embeddingCalls: [], sessions: [], errors: [], totalCount: { messages: 0, llmCalls: 0, embeddingCalls: 0, sessions: 0, errors: 0, }, }; } function emptyTokenStatistics() { return { summary: { total_calls: 0, success_calls: 0, error_calls: 0, total_input_tokens: 0, total_output_tokens: 0, total_tokens: 0, total_cost: 0, avg_tokens_per_call: 0, avg_duration_ms: 0, avg_tokens_per_second: 0, zero_token_success_calls: 0, }, by_model: [], timeseries: [], bucket: 'day', }; } function makeSkill(data: JsonRecord): SkillMock { return { name: String(data.name || ''), display_name: String(data.display_name || ''), description: String(data.description || ''), instructions: String(data.instructions || ''), package_root: String(data.package_root || ''), updated_at: new Date().toISOString(), }; } function makePipeline( state: LangBotApiMockState, data: JsonRecord, uuid = nextId(state, 'pipeline'), ): PipelineMock { return { uuid, name: String(data.name || ''), description: String(data.description || ''), config: (data.config as JsonRecord | undefined) || { ai: {}, trigger: {}, safety: {}, output: {}, }, emoji: String(data.emoji || '⚙️'), is_default: false, updated_at: now(), }; } function knowledgeEngine() { return { plugin_id: 'builtin/minimal-knowledge', name: { en_US: 'Minimal Knowledge Engine', zh_Hans: '最小知识库引擎', }, description: { en_US: 'Minimal mocked engine for frontend smoke tests.', zh_Hans: '用于前端冒烟测试的最小模拟引擎。', }, capabilities: ['text_retrieval'], creation_schema: [], retrieval_schema: [], }; } function makeKnowledgeBase( state: LangBotApiMockState, data: JsonRecord, uuid = nextId(state, 'knowledge'), ): KnowledgeBaseMock { const engine = knowledgeEngine(); return { uuid, name: String(data.name || ''), description: String(data.description || ''), emoji: String(data.emoji || '📚'), knowledge_engine_plugin_id: String( data.knowledge_engine_plugin_id || engine.plugin_id, ), creation_settings: (data.creation_settings as JsonRecord | undefined) || {}, retrieval_settings: (data.retrieval_settings as JsonRecord | undefined) || {}, knowledge_engine: { plugin_id: engine.plugin_id, name: engine.name, capabilities: engine.capabilities, }, updated_at: now(), }; } function makeMCPServer(data: JsonRecord): MCPServerMock { return { name: String(data.name || ''), mode: (data.mode as MCPServerMock['mode']) || 'sse', enable: data.enable !== false, extra_args: (data.extra_args as JsonRecord | undefined) || {}, runtime_info: { status: 'connected', tool_count: 0, tools: [], }, readme: '', updated_at: now(), }; } async function handleBackendApi(route: Route, state: LangBotApiMockState) { const request = route.request(); const url = new URL(request.url()); const path = url.pathname; const method = request.method(); if (path === '/api/v1/system/info') { return fulfillJson(route, { debug: false, version: 'frontend-smoke', edition: 'community', cloud_service_url: 'https://space.langbot.app', enable_marketplace: true, allow_modify_login_info: true, disable_models_service: false, limitation: { max_bots: -1, max_pipelines: -1, max_extensions: -1, }, outbound_ips: [], wizard_status: 'completed', wizard_progress: null, }); } if (path === '/api/v1/user/account-info') { return fulfillJson(route, { initialized: true, account_type: 'local', has_password: true, }); } if (path === '/api/v1/user/check-token') { return fulfillJson(route, { token: '' }); } if (path === '/api/v1/user/auth') { return fulfillJson(route, { token: 'playwright-token' }); } if (path === '/api/v1/user/info') { return fulfillJson(route, { user: 'admin@example.com', account_type: 'local', has_password: true, }); } if (path === '/api/v1/user/space-credits') { return fulfillJson(route, { credits: null }); } if (path === '/api/v1/platform/bots') { return fulfillJson(route, { bots: [] }); } if (path === '/api/v1/pipelines/_/metadata') { return fulfillJson(route, { configs: [] }); } if (path === '/api/v1/pipelines') { if (method === 'POST') { const pipeline = makePipeline(state, parseJsonBody(route)); state.pipelines = [ ...state.pipelines.filter((item) => item.uuid !== pipeline.uuid), pipeline, ]; return fulfillJson(route, { uuid: pipeline.uuid }); } return fulfillJson(route, { pipelines: state.pipelines }); } const pipelineMatch = path.match(/^\/api\/v1\/pipelines\/([^/]+)$/); if (pipelineMatch) { const pipelineId = decodeURIComponent(pipelineMatch[1]); if (method === 'PUT') { const pipeline = makePipeline(state, parseJsonBody(route), pipelineId); state.pipelines = [ ...state.pipelines.filter((item) => item.uuid !== pipelineId), pipeline, ]; return fulfillJson(route, {}); } if (method === 'DELETE') { state.pipelines = state.pipelines.filter( (item) => item.uuid !== pipelineId, ); return fulfillJson(route, {}); } const pipeline = state.pipelines.find((item) => item.uuid === pipelineId); return fulfillJson(route, { pipeline: pipeline || makePipeline(state, { name: pipelineId }, pipelineId), }); } const pipelineExtensionsMatch = path.match( /^\/api\/v1\/pipelines\/([^/]+)\/extensions$/, ); if (pipelineExtensionsMatch) { return fulfillJson(route, { enable_all_plugins: true, enable_all_mcp_servers: true, enable_all_skills: true, bound_plugins: [], available_plugins: [], bound_mcp_servers: [], available_mcp_servers: state.mcpServers, bound_skills: [], available_skills: state.skills, }); } if (path === '/api/v1/knowledge/bases') { if (method === 'POST') { const base = makeKnowledgeBase(state, parseJsonBody(route)); state.knowledgeBases = [ ...state.knowledgeBases.filter((item) => item.uuid !== base.uuid), base, ]; return fulfillJson(route, { uuid: base.uuid }); } return fulfillJson(route, { bases: state.knowledgeBases }); } const knowledgeBaseFilesMatch = path.match( /^\/api\/v1\/knowledge\/bases\/([^/]+)\/files$/, ); if (knowledgeBaseFilesMatch) { return fulfillJson(route, { files: [] }); } const knowledgeBaseMatch = path.match( /^\/api\/v1\/knowledge\/bases\/([^/]+)$/, ); if (knowledgeBaseMatch) { const baseId = decodeURIComponent(knowledgeBaseMatch[1]); if (method === 'PUT') { const base = makeKnowledgeBase(state, parseJsonBody(route), baseId); state.knowledgeBases = [ ...state.knowledgeBases.filter((item) => item.uuid !== baseId), base, ]; return fulfillJson(route, { uuid: base.uuid }); } if (method === 'DELETE') { state.knowledgeBases = state.knowledgeBases.filter( (item) => item.uuid !== baseId, ); return fulfillJson(route, {}); } const base = state.knowledgeBases.find((item) => item.uuid === baseId); return fulfillJson(route, { base: base || makeKnowledgeBase(state, { name: baseId }, baseId), }); } if (path === '/api/v1/knowledge/engines') { return fulfillJson(route, { engines: [knowledgeEngine()] }); } if (path === '/api/v1/knowledge/migration/status') { return fulfillJson(route, { needed: false, internal_kb_count: 0, external_kb_count: 0, }); } if (path === '/api/v1/plugins') { return fulfillJson(route, { plugins: [] }); } if (path === '/api/v1/extensions') { return fulfillJson(route, { extensions: [] }); } if (path === '/api/v1/mcp/servers') { if (method === 'POST') { const server = makeMCPServer(parseJsonBody(route)); state.mcpServers = [ ...state.mcpServers.filter((item) => item.name !== server.name), server, ]; return fulfillJson(route, { task_id: nextId(state, 'task') }); } return fulfillJson(route, { servers: state.mcpServers }); } const mcpTestMatch = path.match(/^\/api\/v1\/mcp\/servers\/([^/]+)\/test$/); if (mcpTestMatch) { return fulfillJson(route, { runtime_info: { status: 'connected', tool_count: 0, tools: [], }, }); } const mcpServerMatch = path.match(/^\/api\/v1\/mcp\/servers\/([^/]+)$/); if (mcpServerMatch) { const serverName = decodeURIComponent(mcpServerMatch[1]); if (method === 'PUT') { const existing = state.mcpServers.find( (item) => item.name === serverName, ); const server = makeMCPServer({ ...(existing || {}), ...parseJsonBody(route), name: serverName, }); state.mcpServers = [ ...state.mcpServers.filter((item) => item.name !== serverName), server, ]; return fulfillJson(route, { task_id: nextId(state, 'task') }); } if (method === 'DELETE') { state.mcpServers = state.mcpServers.filter( (item) => item.name !== serverName, ); return fulfillJson(route, { task_id: nextId(state, 'task') }); } const server = state.mcpServers.find((item) => item.name === serverName); return fulfillJson(route, { server: server || makeMCPServer({ name: serverName }), }); } if (path === '/api/v1/skills') { if (method === 'POST') { const skill = makeSkill( JSON.parse(request.postData() || '{}') as JsonRecord, ); state.skills = [ ...state.skills.filter((item) => item.name !== skill.name), skill, ]; return fulfillJson(route, { skill }); } return fulfillJson(route, { skills: state.skills }); } const skillFileMatch = path.match( /^\/api\/v1\/skills\/([^/]+)\/files\/(.+)$/, ); if (skillFileMatch) { const skillName = decodeURIComponent(skillFileMatch[1]); const filePath = decodeURIComponent(skillFileMatch[2]); const skill = state.skills.find((item) => item.name === skillName); return fulfillJson(route, { skill: { name: skillName }, path: filePath, content: skill?.instructions || '', }); } const skillFilesMatch = path.match(/^\/api\/v1\/skills\/([^/]+)\/files$/); if (skillFilesMatch) { const skillName = decodeURIComponent(skillFilesMatch[1]); return fulfillJson(route, { skill: { name: skillName }, base_path: '.', entries: [ { path: 'SKILL.md', name: 'SKILL.md', is_dir: false, size: null, }, ], truncated: false, }); } const skillMatch = path.match(/^\/api\/v1\/skills\/([^/]+)$/); if (skillMatch) { const skillName = decodeURIComponent(skillMatch[1]); if (method === 'PUT') { const skill = makeSkill({ ...parseJsonBody(route), name: skillName, }); state.skills = [ ...state.skills.filter((item) => item.name !== skillName), skill, ]; return fulfillJson(route, { skill }); } if (method === 'DELETE') { state.skills = state.skills.filter((item) => item.name !== skillName); return fulfillJson(route, {}); } const skill = state.skills.find((item) => item.name === skillName) || { name: skillName, display_name: '', description: '', instructions: '', package_root: '', updated_at: new Date().toISOString(), }; return fulfillJson(route, { skill }); } if (path === '/api/v1/system/status/plugin-system') { return fulfillJson(route, { is_enable: true, is_connected: true, plugin_connector_error: '', }); } if (path === '/api/v1/plugins/debug-info') { return fulfillJson(route, { debug_url: 'ws://127.0.0.1:5300/plugin/debug', plugin_debug_key: 'test-debug-key', }); } if (path === '/api/v1/box/status') { return fulfillJson(route, { available: true, enabled: true, profile: 'playwright', recent_error_count: 0, active_sessions: 0, managed_processes: 0, session_ttl_sec: 3600, backend: { name: 'playwright', available: true, }, }); } if (path === '/api/v1/box/sessions') { return fulfillJson(route, []); } if (path === '/api/v1/monitoring/data') { return fulfillJson(route, emptyMonitoringData()); } if (path === '/api/v1/monitoring/overview') { return fulfillJson(route, emptyMonitoringData().overview); } if (path === '/api/v1/monitoring/token-statistics') { return fulfillJson(route, emptyTokenStatistics()); } if (path === '/api/v1/monitoring/feedback/stats') { return fulfillJson(route, { total_feedback: 0, total_likes: 0, total_dislikes: 0, satisfaction_rate: 0, }); } if (path === '/api/v1/monitoring/feedback') { return fulfillJson(route, { feedback: [], total: 0 }); } if (path === '/api/v1/survey/pending') { return fulfillJson(route, { survey: null }); } if (path === '/api/v1/system/tasks') { return fulfillJson(route, { tasks: [] }); } if ( path === '/api/v1/marketplace/plugins' || path === '/api/v1/marketplace/plugins/search' || path === '/api/v1/marketplace/extensions/search' || path === '/api/v1/marketplace/mcps/search' || path === '/api/v1/marketplace/skills/search' ) { return fulfillJson(route, { plugins: [], total: 0 }); } if (path === '/api/v1/marketplace/tags') { return fulfillJson(route, { tags: [] }); } if (path === '/api/v1/marketplace/recommendation-lists') { return fulfillJson(route, { lists: [] }); } if (path === '/api/v1/dist/info/releases') { return fulfillJson(route, []); } if (path === '/api/v1/dist/info/repo') { return fulfillJson(route, { repo: { stargazers_count: 0, forks_count: 0, open_issues_count: 0, }, contributors: [], }); } await fulfillJson(route, {}); } async function handleCloudApi(route: Route) { const path = routePath(route); if ( path === '/api/v1/marketplace/plugins' || path === '/api/v1/marketplace/plugins/search' || path === '/api/v1/marketplace/extensions/search' || path === '/api/v1/marketplace/mcps/search' || path === '/api/v1/marketplace/skills/search' ) { return fulfillJson(route, { plugins: [], total: 0 }); } if (path === '/api/v1/marketplace/tags') { return fulfillJson(route, { tags: [] }); } if (path === '/api/v1/marketplace/recommendation-lists') { return fulfillJson(route, { lists: [] }); } if (path === '/api/v1/dist/info/releases') { return fulfillJson(route, []); } if (path === '/api/v1/dist/info/repo') { return fulfillJson(route, { repo: { stargazers_count: 0, forks_count: 0, open_issues_count: 0, }, contributors: [], }); } await fulfillJson(route, {}); } export async function installLangBotApiMocks( page: Page, options: { authenticated?: boolean; storage?: JsonRecord } = {}, ) { const { authenticated = false, storage = {} } = options; const state: LangBotApiMockState = { counters: {}, knowledgeBases: [], mcpServers: [], pipelines: [], skills: [], }; await page.addInitScript( ({ authenticated, storage }) => { localStorage.setItem('langbot_language', 'en-US'); localStorage.setItem('extensions_group_by_type', 'false'); if (authenticated) { localStorage.setItem('token', 'playwright-token'); localStorage.setItem('userEmail', 'admin@example.com'); } else { localStorage.removeItem('token'); localStorage.removeItem('userEmail'); } for (const [key, value] of Object.entries(storage)) { localStorage.setItem(key, String(value)); } }, { authenticated, storage }, ); await page.route('**/api/v1/**', (route) => handleBackendApi(route, state)); await page.route('https://space.langbot.app/**', handleCloudApi); }