#!/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'), }; 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 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 has 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 builtin disabled === ['runtime handles disabledPiBuiltinTools from status/start', runtime.includes('disabledBuiltinTools') || runtime.includes('disabledPiBuiltinTools')], ['runtime has Pi builtin disabled UI indicator', runtime.includes('builtin-disabled')], ['runtime mentions bash/read/write/edit disabled', 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 disables Pi builtin tools', route.includes('disabledPiBuiltinTools') || route.includes('--no-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 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 bridge token literal into extension file', route.includes('MNOTE_PI_LAB_BRIDGE_TOKEN') && route.includes('process.env.MNOTE_PI_LAB_BRIDGE_TOKEN') && !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 generates Pi extension with registered MNote tools', route.includes('ensure_session_tool_bridge_extension') && route.includes('pi.registerTool') && route.includes('mnote_current_page_read')], ['route starts Pi with explicit extension bridge', route.includes('--extension') && route.includes('mnoteToolBridgeExtension')], ['route starts Pi with MNote tool allowlist', route.includes('--tools') && route.includes('mnoteToolNames') && route.includes('pi_lab_extension_tool_names')], ['route keeps Pi builtin tools disabled while loading extension', route.includes('--no-builtin-tools') && route.includes('disabledBuiltinTools')], ['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')], // === 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 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).');