#!/usr/bin/env node // Pi Lab static code smoke // 验证 Pi Lab 相关源码结构正确,不依赖后端运行 // 确认:新端点、状态机、无轮询、SSE、Pi builtin 禁用、allowed roots、receipt // 确认:默认模型 omniroute/freefirst 在前端 UI 和 header 中明确体现 const fs = require('fs'); const path = require('path'); const repoRoot = path.resolve(__dirname, '..'); // Files to check const files = { runtime: path.join(repoRoot, 'rust/crates/mnote-web/browser/sidebar-page-ai-pi-lab-runtime.js'), route: path.join(repoRoot, 'rust/crates/mnote-web/src/routes/page_ai_pi.rs'), mod: path.join(repoRoot, 'rust/crates/mnote-web/src/routes/mod.rs'), webShell: path.join(repoRoot, 'rust/crates/mnote-web/src/routes/web_shell.rs'), gateway: path.join(repoRoot, 'rust/crates/mnote-web/src/routes/gateway.rs'), app: path.join(repoRoot, 'rust/crates/mnote-web/src/app.rs'), mnotePiPackage: path.join(repoRoot, 'packages/pi-mnote/package.json'), mnotePiExtension: path.join(repoRoot, 'packages/pi-mnote/extensions/mnote-bridge.ts'), mnotePiMcpExtension: path.join(repoRoot, 'packages/pi-mnote/extensions/mnote-mcp/index.ts'), mnotePiMcpClient: path.join(repoRoot, 'packages/pi-mnote/extensions/mnote-mcp/client.mjs'), }; function readFile(p) { try { return fs.readFileSync(p, 'utf8'); } catch { return ''; } } const runtime = readFile(files.runtime); const route = readFile(files.route); const routesMod = readFile(files.mod); const webShell = readFile(files.webShell); const gateway = readFile(files.gateway); const app = readFile(files.app); const mnotePiPackage = readFile(files.mnotePiPackage); const mnotePiExtension = readFile(files.mnotePiExtension); const mnotePiMcpExtension = readFile(files.mnotePiMcpExtension); const mnotePiMcpClient = readFile(files.mnotePiMcpClient); const checks = [ // === Runtime JS: existence === ['runtime JS exists', runtime.length > 0], ['runtime exports createSidebarPageAiPiLabRuntime', runtime.includes('window.createSidebarPageAiPiLabRuntime')], // === Runtime JS: API endpoints (new flow: start/send/abort/events) === ['runtime uses /api/page-ai/pi/status', runtime.includes('/api/page-ai/pi/status')], ['runtime uses /api/page-ai/pi/start', runtime.includes('/api/page-ai/pi/start')], ['runtime uses /api/page-ai/pi/send', runtime.includes('/api/page-ai/pi/send')], ['runtime uses /api/page-ai/pi/abort', runtime.includes('/api/page-ai/pi/abort')], ['runtime uses /api/page-ai/pi/events', runtime.includes('/api/page-ai/pi/events')], ['runtime keeps legacy /api/page-ai/pi/bootstrap fallback', runtime.includes('/api/page-ai/pi/bootstrap')], // === Runtime JS: state machine === ['runtime has idle state', runtime.includes("STATE_IDLE") || (runtime.includes("'idle'") && runtime.includes('STATE_IDLE'))], ['runtime has starting state', runtime.includes("STATE_STARTING")], ['runtime has started state', runtime.includes("STATE_STARTED")], ['runtime has streaming state', runtime.includes("STATE_STREAMING")], ['runtime has aborted state', runtime.includes("STATE_ABORTED")], ['runtime has error state', runtime.includes("STATE_ERROR")], // === Runtime JS: NO periodic polling === ['runtime does NOT use setInterval for periodic polling', !runtime.includes('setInterval(checkStatus') && !runtime.includes("setInterval(checkStatus") && !runtime.match(/setInterval\s*\([^)]*checkStatus/i)], ['runtime comments "NO setInterval polling"', runtime.includes('NO setInterval polling')], // === Runtime JS: SSE / EventSource === ['runtime uses EventSource for SSE', runtime.includes('EventSource')], ['runtime connects to /api/page-ai/pi/events via SSE', runtime.includes('API.EVENTS') || runtime.includes('/api/page-ai/pi/events')], ['runtime handles pi_rpc_event from SSE', runtime.includes('pi_rpc_event')], ['runtime handles runtime_started event', runtime.includes('runtime_started')], ['runtime handles runtime_aborted event', runtime.includes('runtime_aborted')], // === Runtime JS: UI rendering === ['runtime renders stream text', runtime.includes('text_delta') || runtime.includes('streamingAssistantMsg.text')], ['runtime renders tool calls', runtime.includes('toolCalls')], ['runtime renders citations', runtime.includes('citations')], ['runtime renders diff summary', runtime.includes('diffSummary')], ['runtime starts through drawer open/send instead of manual start button', !runtime.includes('btn-start')], ['runtime has send button', runtime.includes('btn-send')], ['runtime has abort button', runtime.includes('btn-abort')], ['runtime has clear button', runtime.includes('btn-clear')], // === Runtime JS: Pi builtins managed by MNote policy === ['runtime handles managedPiBuiltinTools from status/start', runtime.includes('managedBuiltinTools') || runtime.includes('managedPiBuiltinTools')], ['runtime has Pi builtin managed UI indicator', runtime.includes('builtin-managed')], ['runtime mentions managed bash/read/write/edit tools', runtime.includes('bash') && runtime.includes('read') && runtime.includes('write') && runtime.includes('edit')], // === Runtime JS: tool receipt === ['runtime references receipt', runtime.includes('receipt') || runtime.includes('Receipt')], ['runtime has receipt UI display', runtime.includes('receipts')], // === Runtime JS: independent native drawer === ['runtime checks enabled flag via status API but does not hide launcher behind it', runtime.includes('enabled') && runtime.includes('checkStatus')], ['runtime renders independent Pi Lab drawer', runtime.includes('data-page-ai-pi-lab-drawer') && runtime.includes('data-page-ai-pi-lab') && runtime.includes('drawer')], ['runtime creates drawer through ensureDrawer', runtime.includes('function ensureDrawer') && runtime.includes('setDrawerVisible')], ['runtime does NOT mount inside OpenHub drawer', !runtime.includes('attachPanelToDrawer') && !runtime.includes('wolai-page-ai-drawer')], ['runtime does NOT toggle OpenHub iframe visibility', !runtime.includes('setOpenHubVisible') && !runtime.includes('data-page-ai-openhub-frame-wrap')], ['runtime has no OpenHub provider tab inside Pi Lab', !runtime.includes('data-page-ai-pi-lab-openhub-tab')], ['runtime has context strip chips', runtime.includes('data-page-ai-pi-lab-context-strip') && runtime.includes('data-page-ai-pi-lab-current-page') && runtime.includes('data-page-ai-pi-lab-allowed-roots') && runtime.includes('data-page-ai-pi-lab-lightrag')], ['runtime collapses secondary context/settings like OpenHub chrome', runtime.includes('
0], ['route has status endpoint', route.includes('pub async fn status')], ['route has start endpoint', route.includes('pub async fn start')], ['route has send endpoint', route.includes('pub async fn send')], ['route has abort endpoint', route.includes('pub async fn abort')], ['route has events SSE endpoint', route.includes('pub async fn events')], ['route has bootstrap endpoint (legacy)', route.includes('pub async fn bootstrap')], ['route has tool_call endpoint', route.includes('pub async fn tool_call')], ['route has internal tool_call_bridge endpoint', route.includes('pub async fn tool_call_bridge')], ['route uses AppConfig enable_page_ai_pi_lab instead of direct env gate', route.includes('state.config().enable_page_ai_pi_lab') && !route.includes('std::env::var("MNOTE_PAGE_AI_PI_LAB")')], ['route returns enabled=false when not enabled', route.includes('"enabled": false')], ['route marks independent native drawer ui mode', route.includes('independent_mnote_native_drawer')], ['route does not return openHubDefaultPreserved marker', !route.includes('openHubDefaultPreserved')], ['route returns schema mnote.page_ai_pi.status.v1', route.includes('mnote.page_ai_pi.status.v1')], ['route returns schema mnote.page_ai_pi.bootstrap.v1', route.includes('mnote.page_ai_pi.bootstrap.v1')], ['route returns schema mnote.page_ai_pi.start.v1', route.includes('mnote.page_ai_pi.start.v1')], ['route returns schema mnote.page_ai_pi.send.v1', route.includes('mnote.page_ai_pi.send.v1')], ['route returns schema mnote.page_ai_pi.abort.v1', route.includes('mnote.page_ai_pi.abort.v1')], ['route has event schema PI_LAB_SCHEMA_EVENT', route.includes('PI_LAB_SCHEMA_EVENT')], ['route has receipt schema PI_LAB_SCHEMA_RECEIPT', route.includes('PI_LAB_SCHEMA_RECEIPT')], ['route exposes managed Pi builtin tools', route.includes('managedPiBuiltinTools') && route.includes('PI_LAB_MANAGED_BUILTIN_TOOLS')], ['route has receipt storage policy', route.includes('receiptStorage') || route.includes('PI_LAB_SCHEMA_RECEIPT')], ['route has session dir policy', route.includes('managedPiSessionDirPolicy')], ['route enforces allowed roots', route.includes('active_allowed_roots') || route.includes('allowed_roots')], ['route blocks path escape', route.includes('path_escape') || route.includes('path_is_inside')], ['route has default model provider constant PI_LAB_DEFAULT_MODEL_PROVIDER=omniroute', route.includes('PI_LAB_DEFAULT_MODEL_PROVIDER') && route.includes('omniroute')], ['route has default model id constant PI_LAB_DEFAULT_MODEL_ID=freefirst', route.includes('PI_LAB_DEFAULT_MODEL_ID') && route.includes('freefirst')], ['route defaults to Pi Rust runtime implementation', route.includes('PI_LAB_RUNTIME_IMPL_RUST') && route.includes('"pi-rust"') && route.includes('MNOTE_PAGE_AI_PI_RUST_BIN')], ['route keeps TS Pi as explicit fallback only', route.includes('PI_LAB_RUNTIME_IMPL_TS') && route.includes('MNOTE_PAGE_AI_PI_TS_BIN')], ['route rejects arbitrary cookie_header auth bypass', route.includes('page_ai_pi_lab_invalid_session_cookie') && !route.includes('context.auth.cookie_header.is_some() {\n return Ok') ], ['route stores Pi Lab session owner id', route.includes('mnote_user_id') && route.includes('ensure_session_owner')], ['route validates session owner on send/abort/tool/events', route.includes('get_session_for_context')], ['route requires sidebar host selection snapshot', route.includes('page_ai_pi_lab_selection_snapshot_required') && route.includes('mnote_sidebar_host_snapshot')], ['route tool_receipt.write is not a silent no-op', route.includes('"requestedReceipt"') && route.includes('execute_tool 统一写入')], ['route has bridge token header and non-serialized session token', route.includes('HEADER_PI_LAB_BRIDGE_TOKEN') && route.includes('x-mnote-pi-lab-bridge-token') && route.includes('skip_serializing')], ['route generates bridge token from OS randomness', route.includes('generate_bridge_token') && route.includes('/dev/urandom') && !route.includes('bridge_token: generate_id("pi_bridge")')], ['route does not write obsolete private MCP bridge config', !route.includes('write_session_mcp_bridge_config') && !route.includes('mnote.pi.mcp-bridge.v1') && !route.includes('mnote-bridge.json')], ['route never writes bridge token into generated extension source', !route.includes('const BRIDGE_TOKEN = {bridge_token}')], ['route rejects bridge calls for non-running sessions', route.includes('page_ai_pi_lab_bridge_session_not_running') && route.includes('PiLabSessionStatus::RuntimeRunning | PiLabSessionStatus::TurnRunning')], ['route loads official MNote Pi package extension', route.includes('mnote_pi_extension_path') && route.includes('packages/pi-mnote/extensions/mnote-bridge.ts')], ['route starts Pi with explicit MNote extension bridge', route.includes('--extension') && route.includes('mnotePiExtension')], ['route starts Pi with MNote tool allowlist', route.includes('--tools') && route.includes('mnoteToolNames') && route.includes('pi_lab_extension_tool_names')], ['route passes MNote tool manifest to extension env', route.includes('MNOTE_PI_BRIDGE_TOOLS') && route.includes('PI_MNOTE_BRIDGE_TOOLS') && route.includes('mnote_pi_tool_manifest')], ['route writes dynamic Pi Rust MNote context file', route.includes('mnote.pi.context.v1') && route.includes('PI_MNOTE_CONTEXT_FILE') && route.includes('write_pi_mnote_context_snapshot')], ['route injects dynamic Pi Rust input context', route.includes('MNOTE_PI_CONTEXT_V1') && route.includes('pi_mnote_input_context_prefix')], ['route disables Pi builtins by default unless external permission extension is opt-in', route.includes('pi_lab_enabled_builtin_tools') && route.includes('MNOTE_PAGE_AI_PI_ALLOW_EXTERNAL_EXTENSIONS') && route.includes('configuredPiExtensionSources') && route.includes('pi_lab_runtime_extension_sources')], ['route exposes Rust Pi runtime diagnostics', route.includes('hashline_edit') && route.includes('runtimeImplementation') && route.includes('runtimeBinary') && route.includes('runtimeAvailable') && route.includes('runtimeInstallHint') && route.includes('runtimeError')], ['runtime send path auto-starts Pi Rust instead of dead-end not-ready toast', runtime.includes('Pi Rust 正在启动,启动后自动发送') && runtime.includes('piLabStartPromise') && !runtime.includes('Pi 会话尚未就绪,请重新打开 Pi 面板或稍后重试')], ['route applies TTL and rate limits', route.includes('PI_LAB_SESSION_TTL_MS') && route.includes('PI_LAB_RATE_LIMITS') && route.includes('check_rate_limit')], ['route cleans expired session dirs and rate buckets', route.includes('fs::remove_dir_all') && route.includes('buckets.retain')], // === MNote Pi package checks === ['@mnote/pi package manifest exists', mnotePiPackage.includes('"name": "@mnote/pi"') && mnotePiPackage.includes('"pi"')], ['@mnote/pi package declares extension', mnotePiPackage.includes('./extensions/mnote-bridge.ts')], ['@mnote/pi package declares Pi Rust MCP extension', mnotePiPackage.includes('./extensions/mnote-mcp/index.ts')], ['@mnote/pi extension registers MNote tools', mnotePiExtension.includes('pi.registerTool') && mnotePiExtension.includes('mnote_current_page_read')], ['@mnote/pi extension keeps legacy MNote bridge API fallback', mnotePiExtension.includes('/api/page-ai/pi/tool-call-bridge') && mnotePiExtension.includes('x-mnote-pi-lab-bridge-token')], ['@mnote/pi extension uses Pi Rust native current-page read', mnotePiExtension.includes('pi-rust-native-fs') && mnotePiExtension.includes('PI_MNOTE_CONTEXT_FILE') && mnotePiExtension.includes('fs.readFileSync')], ['@mnote/pi extension consumes Pi Rust input context anywhere after skill expansion', mnotePiExtension.includes('pi.on?.("input"') && mnotePiExtension.includes('MNOTE_PI_CONTEXT_V1') && mnotePiExtension.includes('text.indexOf(CONTEXT_PREFIX)') && mnotePiExtension.includes('action: "transform"')], ['route preserves slash skill command before hidden input context', route.includes('pi_lab_command_message_for_session') && route.includes('message.starts_with("/skill:")') && route.includes('format!("{command}\\n{input_context_prefix}{effective_args}")')], ['@mnote/pi extension supports LightRAG tools', mnotePiExtension.includes('mnote_knowledge_rag_query') && mnotePiExtension.includes('mnote_knowledge_rag_section_context')], ['@mnote/pi MCP extension registers mcp tool', mnotePiMcpExtension.includes('registerTool') && mnotePiMcpExtension.includes('name: "mcp"')], ['@mnote/pi MCP extension uses official synchronous child_process bridge', mnotePiMcpExtension.includes('execFileSync') && mnotePiMcpExtension.includes('client.mjs') && mnotePiMcpExtension.includes('transport: "pi-rust-sync-client"') && !mnotePiMcpExtension.includes('/api/page-ai/pi/mcp-call-bridge') && !mnotePiMcpExtension.includes('fetch(')], ['@mnote/pi MCP extension does not depend on private session token config', !mnotePiMcpExtension.includes('mnote-bridge.json') && !mnotePiMcpExtension.includes('BRIDGE_TOKEN') && !mnotePiMcpExtension.includes('SESSION_ID')], ['@mnote/pi MCP client accepts inline sync request and adjacent session config', mnotePiMcpClient.includes('--request-json') && mnotePiMcpClient.includes('resolve(extDir, ".pi", "mcp.json")')], ['@mnote/pi MCP client implements protocol handshake and tool calls', mnotePiMcpClient.includes('"initialize"') && mnotePiMcpClient.includes('"tools/list"') && mnotePiMcpClient.includes('"tools/call"')], ['@mnote/pi MCP client supports stdio and streamable HTTP', mnotePiMcpClient.includes('spawn(') && mnotePiMcpClient.includes('text/event-stream')], ['route loads built-in Pi Rust MCP extension', route.includes('mnote_pi_mcp_extension_path') && route.includes('packages/pi-mnote/extensions/mnote-mcp/index.ts')], ['route stages per-session MCP config next to built-in extension', route.includes('stage_project_mcp_config_for_extension') && route.includes('extension_dir.join(".pi")') && route.includes('mcp.json')], ['route does not pass unsupported Pi Rust --mcp-config flag', !route.includes('.arg("--mcp-config")')], ['route does not globally disable Pi Rust extension capability policy', !route.includes('command.env("PI_EXTENSION_ALLOW_DANGEROUS"') && !route.includes('pi_tool_names.push("bash".into())')], ['route narrowly enables Pi Rust sync exec only for configured MCP sessions', route.includes('if pi_lab_mcp_enabled(&session)') && route.includes('command.env("PIJS_ALLOW_UNSAFE_SYNC_EXEC", "1")')], // === mod.rs checks === ['mod.rs declares page_ai_pi module', routesMod.includes('mod page_ai_pi;')], ['mod.rs mounts pi status route', routesMod.includes('/api/page-ai/pi/status')], ['mod.rs mounts pi start route', routesMod.includes('/api/page-ai/pi/start')], ['mod.rs mounts pi send route', routesMod.includes('/api/page-ai/pi/send')], ['mod.rs mounts pi abort route', routesMod.includes('/api/page-ai/pi/abort')], ['mod.rs mounts pi events route', routesMod.includes('/api/page-ai/pi/events')], ['mod.rs mounts pi session history delete and clear routes', routesMod.includes('get(page_ai_pi::list_sessions).delete(page_ai_pi::clear_sessions)') && routesMod.includes('.delete(page_ai_pi::delete_session_history)')], ['mod.rs mounts pi tool-call route', routesMod.includes('/api/page-ai/pi/tool-call')], ['mod.rs mounts pi internal tool-call-bridge route', routesMod.includes('/api/page-ai/pi/tool-call-bridge')], ['mod.rs mounts pi bootstrap route (legacy)', routesMod.includes('/api/page-ai/pi/bootstrap')], ['mod.rs mounts pi tool_call route', routesMod.includes('/api/page-ai/pi/tool-call')], ['mod.rs mounts pi lab runtime asset', routesMod.includes('sidebar-page-ai-pi-lab-runtime.js')], // === web_shell.rs checks === ['web_shell.rs has pi lab runtime asset function', webShell.includes('sidebar_page_ai_pi_lab_runtime_asset')], ['web_shell.rs includes pi lab runtime JS', webShell.includes('sidebar-page-ai-pi-lab-runtime.js')], // === gateway.rs checks === ['gateway.rs loads Pi Lab runtime when config enabled', gateway.includes('createSidebarPageAiPiLabRuntime')], ['gateway.rs does NOT stamp body hidden gate', !gateway.includes('data-page-ai-pi-lab-hidden')], ['app.rs defaults Pi Lab config on', app.includes('enable_page_ai_pi_lab: env_bool("MNOTE_PAGE_AI_PI_LAB", true)')], // === Key architectural constraints === ['no modification of sidebar-page-ai-runtime', !runtime.includes('sidebar-page-ai-runtime')], ['no sidebar-page-ai-runtime default behavior change', !runtime.includes('sidebarPageAiRuntime')], ['no import of main page AI runtime', !runtime.includes('import.*sidebar-page-ai')], ]; const failed = checks.filter(([, ok]) => !ok); if (failed.length) { console.error('❌ Pi Lab static smoke failed:'); for (const [name] of failed) console.error(` - ${name}`); process.exit(1); } console.log('✅ Pi Lab static smoke passed (' + checks.length + ' checks).');