#!/usr/bin/env node // Pi Lab static code smoke // 验证 Pi Lab 相关源码结构正确,不依赖后端运行 // 确认:新端点、状态机、无轮询、SSE、Pi builtin 禁用、allowed roots、receipt // 确认:默认模型 omniroute/gpt-5.4-mini 在前端 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 devHot = readFile(path.join(repoRoot, 'scripts/dev-hot.js')); function functionBody(source, name) { const start = source.indexOf(`function ${name}(`); if (start < 0) return ''; const brace = source.indexOf('{', start); if (brace < 0) return ''; let depth = 0; for (let i = brace; i < source.length; i += 1) { const ch = source[i]; if (ch === '{') depth += 1; if (ch === '}') { depth -= 1; if (depth === 0) return source.slice(start, i + 1); } } return source.slice(start); } const showPiLabBody = functionBody(runtime, 'showPiLab'); 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 configure endpoint for Pi RPC model/thinking changes', route.includes('pub async fn configure') && route.includes('"type": "set_model"') && route.includes('"type": "set_thinking_level"')], ['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=gpt-5.4-mini', route.includes('PI_LAB_DEFAULT_MODEL_ID') && route.includes('gpt-5.4-mini')], ['route checks OmniRoute tool_calling capability before real runtime start', route.includes('ensure_session_model_supports_tools') && route.includes('page_ai_pi_model_tools_unsupported') && route.includes('tool_calling')], ['runtime replays deferred model config after streaming', runtime.includes('maybeApplyPendingModelConfig') && runtime.includes('pendingModelConfigApply')], ['runtime preserves queued permission mode across in-flight mode changes', runtime.includes('pendingPermissionMode') && runtime.includes('piLabState.pendingPermissionMode = piLabState.permissionMode')], ['route writes Pi Rust models apiKey as bare env var name, not shell literal', route.includes('"apiKey": "OPENAI_API_KEY"') && !route.includes('"apiKey": "$OPENAI_API_KEY"')], ['route derives OmniRoute tool support from verified model capability', route.includes('"supportsTools": supports_tools') && route.includes('omniroute_model_tool_calling_capability')], ['route keeps OmniRoute streaming usage enabled', route.includes('"supportsUsageInStreaming": true') && !route.includes('"supportsUsageInStreaming": false')], ['route sends Pi Rust directly to configured OmniRoute base URL', route.includes('"baseUrl": omniroute_base_url()') && !route.includes('omniroute_proxy_chat_completions') && !routesMod.includes('/api/page-ai/pi/omniroute-proxy/')], ['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')], ['MNote Pi bridge reads session and base URL from private context fallback', mnotePiExtension.includes('bridgeSessionId') && mnotePiExtension.includes('bridgeBaseUrl') && mnotePiExtension.includes('readContextFileSnapshot')], ['MNote Pi bridge does not trust generic runtime session id env', mnotePiExtension.includes('PI_MNOTE_BRIDGE_SESSION_ID') && mnotePiExtension.includes('MNOTE_PI_BRIDGE_SESSION_ID') && !mnotePiExtension.includes('MNOTE_PI_LAB_SESSION_ID')], ['MNote Pi bridge allows native tools when context file proves Pi Rust', mnotePiExtension.includes('isPiRustNativeRuntime') && mnotePiExtension.includes('Boolean(CONTEXT_FILE)')], ['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 binds staged MNote bridge to absolute context path and private embedded snapshot', route.includes('bind_mnote_bridge_context_path') && route.includes('DEFAULT_CONTEXT_FILE') && route.includes('EMBEDDED_CONTEXT')], ['route sends per-prompt Pi Rust context through extension input hook', route.includes('mnote.pi.context.v1') && route.includes('write_pi_mnote_context_snapshot') && route.includes('fn pi_mnote_input_context_prefix') && route.includes('format!(\"{input_context_prefix}{message}\")')], ['route resolves local file tools against session rootUri when params omit rootUri', route.includes('session: Option<&PiLabSession>') && route.includes('session.and_then(|session| session.root_uri.clone())')], ['route exposes Pi JSONL replay messages from session tree', route.includes('"messages": replay_messages') && route.includes('build_pi_replay_messages(&entries)') && route.includes('pi_lab_active_path_entries')], ['route exposes Pi Rust builtins in full_access instead of replacing them with MNote file tools', route.includes('fn pi_lab_enabled_builtin_tools') && route.includes('Some("full_access")') && route.includes('pi_lab_managed_builtin_tools()') && route.includes('Some("auto_edit")') && route.includes('Some("plan")') && route.includes('PI_LAB_MANAGED_BUILTIN_TOOLS')], ['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')], ['route reports warmup runtime without exposing it as current page session', route.includes('"warmupRunning"') && route.includes('"warmupSessionId"') && route.includes('"warmupProcessCount"') && route.includes('is_pi_lab_warmup_session_id(&session.session_id)')], ['runtime send path distinguishes warm runtime binding from cold start', runtime.includes('正在绑定当前页 Pi 会话,完成后自动发送') && runtime.includes('Pi Rust 正在启动,启动后自动发送') && runtime.includes('piLabState.warmupRunning')], ['dev:hot warmup defaults to no real model prompt', devHot.includes('MNOTE_PAGE_AI_PI_WARMUP_SEND') && devHot.includes('?? "0"')], ['runtime opens Pi drawer by prestarting current page session', showPiLabBody.includes('startRuntime().then') && showPiLabBody.includes('syncRuntimeState()')], ['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 面板或稍后重试')], ['runtime keeps selected model before start/configure', runtime.includes('ensurePiToolCapableModel') && !runtime.includes('isPiToolUnsupportedModel') && runtime.includes('gpt-5.4-mini') && runtime.includes('freefirst')], ['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 uses Pi Rust native local file read/patch', mnotePiExtension.includes('executeNativeLocalFileRead') && mnotePiExtension.includes('executeNativeLocalFilePatch') && !mnotePiExtension.includes('仍依赖旧 HTTP bridge')], ['@mnote/pi extension resolves local file tools relative to selected folder', mnotePiExtension.includes('joinRelativePath') && mnotePiExtension.includes('preferFolder: true') && mnotePiExtension.includes('folderPath')], ['@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 while passing hidden context to extension hook', route.includes('pi_lab_command_message_for_session') && route.includes('message.starts_with("/skill:")') && route.includes('format!("{command}\\n{effective_args}")') && route.includes('format!("{input_context_prefix}{skill_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)')], // === Runtime JS: new API endpoints (STATE/COMPACT/QUEUE_CONFIG) === ['runtime uses /api/page-ai/pi/state', runtime.includes('/api/page-ai/pi/state')], ['runtime uses /api/page-ai/pi/compact', runtime.includes('/api/page-ai/pi/compact')], ['runtime uses /api/page-ai/pi/queue-config', runtime.includes('/api/page-ai/pi/queue-config')], ['runtime has API.STATE constant', runtime.includes("STATE: '/api/page-ai/pi/state'")], ['runtime has API.COMPACT constant', runtime.includes("COMPACT: '/api/page-ai/pi/compact'")], ['runtime has API.QUEUE_CONFIG constant', runtime.includes("QUEUE_CONFIG: '/api/page-ai/pi/queue-config'")], ['runtime maps session tree preview into history messages', runtime.includes('entry.text || entry.content || entry.preview')], ['runtime fetches artifact diff with sessionId query', runtime.includes("'?sessionId=' + encodeURIComponent(sid)")], // === Runtime JS: state sync function (no setInterval) === ['runtime has syncRuntimeState function', runtime.includes('function syncRuntimeState')], ['syncRuntimeState calls API.STATE', runtime.includes("API.STATE") && runtime.includes('sessionId: piLabState.sessionId')], ['syncRuntimeState syncs queuedMessages', runtime.includes('queuedMessages') && runtime.includes('piLabState.queuedMessages')], ['syncRuntimeState syncs pendingMessageCount', runtime.includes('pendingMessageCount')], ['syncRuntimeState syncs isCompacting', runtime.includes('isCompacting') && runtime.includes('piLabState.isCompacting')], ['syncRuntimeState syncs contextUsage', runtime.includes('contextUsage') && runtime.includes('piLabState.contextUsage')], ['syncRuntimeState syncs model/thinking from API.STATE', runtime.includes('data.modelProvider') && runtime.includes('data.thinkingLevel')], ['syncRuntimeState called after showPiLab drawer open', runtime.indexOf('showPiLab') < runtime.indexOf('syncRuntimeState()') || runtime.includes('showPiLab') && runtime.includes('syncRuntimeState')], ['syncRuntimeState called after startRuntime success (in success .then)', runtime.includes('syncRuntimeState();') && runtime.includes('Pi runtime ready') && runtime.includes('return data;')], ['syncRuntimeState called after sendPrompt accepted (in sendPrompt .then)', runtime.includes('syncRuntimeState();') && runtime.includes('data.accepted') && runtime.includes('Pi Lab rejected prompt')], ['syncRuntimeState called after abortPrompt (in abortPrompt function)', runtime.includes('syncRuntimeState();') && runtime.includes('abortPrompt') && runtime.includes('setState(STATE_ABORTED)')], ['syncRuntimeState called in SSE connected event', runtime.includes("'connected'") && runtime.includes('syncRuntimeState()')], ['syncRuntimeState called in runtime_started event', runtime.includes("runtime_started") && runtime.includes('syncRuntimeState()')], ['syncRuntimeState called in runtime_aborted event', runtime.includes("runtime_aborted") && runtime.includes('syncRuntimeState')], ['syncRuntimeState called from checkStatus when session running', runtime.includes("connectEventSource(data.session.sessionId)") && runtime.includes("syncRuntimeState()")], ['syncRuntimeState does NOT use setInterval', !runtime.includes('setInterval(syncRuntimeState') && !runtime.includes("setInterval(syncRuntimeState")], // === Runtime JS: compact function === ['runtime has triggerCompact function', runtime.includes('function triggerCompact')], ['triggerCompact calls API.COMPACT', runtime.includes("API.COMPACT")], ['triggerCompact sets isCompacting=true', runtime.includes('piLabState.isCompacting = true')], ['triggerCompact guards against streaming in function body', runtime.includes('if (piLabState.status === STATE_STREAMING) return;')], ['triggerCompact guards against replay in function body', runtime.includes('if (piLabState.viewingHistorySessionId) return;')], ['triggerCompact shows compaction summary as diagnostic', runtime.includes('updateDiagnostics') && runtime.includes('Compaction:')], ['triggerCompact inserts compaction result card', runtime.includes("type: 'compaction'") && runtime.includes('data-page-ai-pi-lab-compaction-result')], ['triggerCompact shows toast on done', runtime.includes('showPiToast') && runtime.includes('会话压缩完成')], ['triggerCompact shows toast on failure', runtime.includes('showPiToast') && runtime.includes('压缩失败')], // === Runtime JS: queue config function === ['runtime has fetchQueueConfig function', runtime.includes('function fetchQueueConfig')], ['fetchQueueConfig calls API.QUEUE_CONFIG', runtime.includes("API.QUEUE_CONFIG")], ['fetchQueueConfig posts sessionId JSON body', runtime.includes("method: 'POST'") && runtime.includes('sessionId: piLabState.sessionId')], // === Runtime JS: compact button in UI === ['runtime has compact button in commandbar', runtime.includes('data-page-ai-pi-lab-btn-compact')], ['compact button disabled in updateButtons', runtime.includes('compactBtn.disabled')], ['compact button wired in wireEvents', runtime.includes('compactBtn.addEventListener') && runtime.includes('triggerCompact')], ['compact button uses compact icon', runtime.includes("compact: '")], // === Runtime JS: new state fields === ['runtime has queuedMessages state field', runtime.includes('queuedMessages: []')], ['runtime has pendingMessageCount state field', runtime.includes('pendingMessageCount: 0')], ['runtime has isCompacting state field', runtime.includes('isCompacting: false')], ['runtime has contextUsage state field', runtime.includes('contextUsage: {}')], // === 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')], // === B2: JSONL reading constants in page_ai_pi.rs === ["route has PI_LAB_JSONL_MAX_FILE_BYTES constant", route.includes("PI_LAB_JSONL_MAX_FILE_BYTES")], ["route has PI_LAB_JSONL_MAX_LINE_BYTES constant", route.includes("PI_LAB_JSONL_MAX_LINE_BYTES")], ["route has PI_LAB_JSONL_MAX_ENTRIES constant", route.includes("PI_LAB_JSONL_MAX_ENTRIES")], ["route has PI_LAB_JSONL_WINDOW_ENTRIES constant", route.includes("PI_LAB_JSONL_WINDOW_ENTRIES")], // === B2: JSONL helper functions === ["route has read_pi_session_jsonl function", route.includes("fn read_pi_session_jsonl")], ["route has build_pi_entry_tree function", route.includes("fn build_pi_entry_tree")], // === B1: pi_session_file field === ["route serializes piSessionFile in build_run_runtime_json", route.includes("piSessionFile")], // === B3: session_tree endpoint === ["route has PI_LAB_SCHEMA_SESSION_TREE constant", route.includes("PI_LAB_SCHEMA_SESSION_TREE")], ["route has session_tree endpoint function", route.includes("pub async fn session_tree")], // === B5: fork endpoint === ["route has PI_LAB_SCHEMA_FORK constant", route.includes("PI_LAB_SCHEMA_FORK")], ["route has fork_pi_session endpoint function", route.includes("pub async fn fork_pi_session")], // === D2: artifact_diff endpoint === ["route has PI_LAB_SCHEMA_ARTIFACT_DIFF constant", route.includes("PI_LAB_SCHEMA_ARTIFACT_DIFF")], ["route has artifact_diff endpoint function", route.includes("pub async fn artifact_diff")], ["artifact_diff query accepts camelCase sessionId", route.includes('serde(rename_all = "camelCase")') && route.includes("pub struct PiLabArtifactDiffQuery")], ["route publishes file patch artifact event", route.includes('"artifact_file_patch"') && route.includes('mnote.page_ai_pi.artifact.file_patch.v1')], // === mod.rs: new B3/B5/D2 routes === ["mod.rs mounts pi session_tree route", routesMod.includes("/api/page-ai/pi/sessions/{session_id}/tree")], ["mod.rs mounts pi fork route", routesMod.includes("/api/page-ai/pi/fork")], ["mod.rs mounts pi artifact_diff route", routesMod.includes("/api/page-ai/pi/artifacts/{tool_event_id}/diff")], // === mod.rs: state/compact/queue_config/configure routes === ["mod.rs mounts pi state route", routesMod.includes("/api/page-ai/pi/state")], ["mod.rs mounts pi compact route", routesMod.includes("/api/page-ai/pi/compact")], ["mod.rs mounts pi queue-config route", routesMod.includes("/api/page-ai/pi/queue-config")], ["mod.rs mounts pi configure route", routesMod.includes("/api/page-ai/pi/configure")], ]; 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).');