Files
mnote/scripts/task-pi-lab-static-smoke.js
T

409 lines
42 KiB
JavaScript

#!/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'),
legacyPageAiRuntime: path.join(repoRoot, 'rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js'),
layout: path.join(repoRoot, 'rust/crates/mnote-web/src/ssr/pages/layout.rs'),
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 legacyPageAiRuntime = readFile(files.legacyPageAiRuntime);
const layout = readFile(files.layout);
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 retired host drawer', !runtime.includes('attachPanelToDrawer') && !runtime.includes('wolai-page-ai-drawer')],
['runtime does NOT toggle retired host iframe visibility', !runtime.includes('setOpenHubVisible') && !runtime.includes('data-page-ai-openhub-frame-wrap')],
['runtime has no retired host 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 compact chat chrome', runtime.includes('<details class="wolai-page-ai-pi-lab-settings"') && runtime.includes('<details class="wolai-page-ai-pi-lab-context-strip"')],
['runtime settings gear opens AI management page', runtime.includes('data-page-ai-pi-lab-open-settings title="AI 管理"') && runtime.includes("window.location.assign('/user/ai#ai-admin-access')")],
['runtime keeps only meaningful top commandbar buttons', runtime.includes('data-page-ai-pi-lab-new') && runtime.includes('data-page-ai-pi-lab-history') && runtime.includes('data-page-ai-pi-lab-btn-clear') && runtime.includes('data-page-ai-pi-lab-open-settings')],
['runtime removes no-op top commandbar buttons', !runtime.includes('data-page-ai-pi-lab-open title=') && !runtime.includes('data-page-ai-pi-lab-toggle-artifacts') && !runtime.includes('data-page-ai-pi-lab-clock') && !runtime.includes('data-page-ai-pi-lab-notify')],
['runtime has left history drawer markers', runtime.includes('data-page-ai-pi-lab-history-layer') && runtime.includes('wolai-page-ai-pi-lab-history-scrim') && runtime.includes('历史对话')],
['runtime history drawer supports refresh/delete/export/clear', runtime.includes('data-page-ai-pi-lab-history-refresh') && runtime.includes('data-page-ai-pi-lab-history-delete') && runtime.includes('data-page-ai-pi-lab-history-export') && runtime.includes('data-page-ai-pi-lab-history-clear')],
['runtime does not expose manual Pi runtime start button', !runtime.includes('data-page-ai-pi-lab-btn-start') && !runtime.includes('启动 Pi runtime') && !runtime.includes('预启动 Pi 会话')],
['runtime auto starts current page Pi session when drawer opens', showPiLabBody.includes('checkStatus().then(function ()') && showPiLabBody.includes('startRuntime().then')],
['runtime has native model controls', runtime.includes('data-page-ai-pi-lab-model-provider') && runtime.includes('data-page-ai-pi-lab-model-id') && runtime.includes('data-page-ai-pi-lab-model-custom')],
['runtime applies model controls through Pi RPC configure endpoint', runtime.includes("CONFIGURE: '/api/page-ai/pi/configure'") && runtime.includes('applyModelConfigToRuntime') && runtime.includes('pendingModelConfigApply')],
['runtime collapses secondary right rail sections', runtime.includes('<details class="wolai-page-ai-pi-lab-rail-section"') && runtime.includes('data-page-ai-pi-lab-receipts-section')],
['runtime has diagnostics collapsed by default', runtime.includes('<details class="wolai-page-ai-pi-lab-diagnostics"') && !runtime.includes('<details class="wolai-page-ai-pi-lab-diagnostics" open')],
['runtime has Pi diagnostics endpoint constant', runtime.includes("DIAGNOSTICS: '/api/page-ai/pi/diagnostics'")],
['runtime exposes official Pi diagnostics actions in collapsed panel', runtime.includes('data-page-ai-pi-lab-diagnostic-command="doctor"') && runtime.includes('data-page-ai-pi-lab-diagnostic-command="context-preview"') && runtime.includes('data-page-ai-pi-lab-diagnostic-command="list"') && runtime.includes('data-page-ai-pi-lab-diagnostic-command="update-index"') && runtime.includes('data-page-ai-pi-lab-diagnostic-command="search"') && runtime.includes('data-page-ai-pi-lab-diagnostic-command="info"')],
['runtime renders Pi diagnostics output separately from status text', runtime.includes('data-page-ai-pi-lab-diagnostic-output') && runtime.includes('setDiagnosticOutput')],
['runtime posts controlled diagnostics command payload', runtime.includes('function runPiDiagnostic') && runtime.includes('command: command') && runtime.includes('params: params') && runtime.includes('API.DIAGNOSTICS')],
['runtime surfaces effective advanced runtime summary', runtime.includes('advancedRuntimeSummary') && runtime.includes('advancedRuntime=') && runtime.includes('extensionPolicy') && runtime.includes('maxToolIterations')],
['runtime injects CSS styles', runtime.includes('injectStyles')],
// === Runtime JS: default model (omniroute/gpt-5.4-mini) ===
['runtime has defaultModelProvider with omniroute', runtime.includes('defaultModelProvider') && runtime.includes('omniroute')],
['runtime has defaultModelId with gpt-5.4-mini', runtime.includes('defaultModelId') && runtime.includes('gpt-5.4-mini')],
['runtime shows default model in header label', runtime.includes('updateModelLabel') && runtime.includes('defaultModelProvider') && runtime.includes('defaultModelId')],
['runtime consumes defaultModelProvider from backend status', runtime.includes('data.defaultModelProvider')],
['runtime consumes defaultModelId from backend status', runtime.includes('data.defaultModelId')],
['runtime shows "omniroute/gpt-5.4-mini" in empty state', runtime.includes('omniroute') && runtime.includes('gpt-5.4-mini') && runtime.includes('默认模型')],
['runtime comment mentions consuming backend default fields', runtime.includes('backend status/default fields')],
['runtime has DEFAULT_MODEL_PROVIDER constant', runtime.includes('DEFAULT_MODEL_PROVIDER') && runtime.includes("'omniroute'")],
['runtime has DEFAULT_MODEL_ID constant', runtime.includes('DEFAULT_MODEL_ID') && runtime.includes("'gpt-5.4-mini'")],
['runtime preserves Omniroute/freefirst as selectable model', runtime.includes("'freefirst'") && runtime.includes('modelControlOptions')],
['runtime labels freefirst option as omniroute/freefirst', runtime.includes("name: DEFAULT_MODEL_PROVIDER + '/freefirst'")],
['runtime has Pi Lab floating launcher', runtime.includes('data-page-ai-pi-lab-launcher')],
['runtime reuses the existing floating AI slot for Pi Lab launcher', runtime.includes('[data-mnote-action="open-page-ai-pi-lab"], [data-testid="wolai-floating-ai"]')],
['runtime hides Pi Lab launcher while drawer is active', runtime.includes('piLabLauncherEl.hidden = !!piLabState.active')],
['layout maps old floating AI slot to Pi Lab', layout.includes('data-testid="wolai-floating-ai"') && layout.includes('data-mnote-action="open-page-ai-pi-lab"') && layout.includes('{"\\u{03c0}"}')],
['layout no longer exposes legacy Page AI/OpenHub action from floating button', !layout.includes('data-mnote-action="open-page-ai"><span')],
['legacy Page AI opencode host requires explicit debug opt-in', legacyPageAiRuntime.includes("getItem('mnote.page_ai.opencode_host') === '1'") && !legacyPageAiRuntime.includes("getItem('mnote.page_ai.opencode_host') !== '0' : true")],
['runtime documents pi-web-ui evidence', runtime.includes('@earendil-works/pi-web-ui@0.75.3')],
['runtime uses MNote-native adapter boundary', runtime.includes('MNote-native adapter')],
// === Route checks ===
['route file exists', route.length > 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 removes TS Pi fallback', !route.includes('PI_LAB_RUNTIME_IMPL_TS') && !route.includes('MNOTE_PAGE_AI_PI_TS_BIN') && !route.includes('legacy-ts') && !route.includes('pi-ts')],
['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 keeps Pi Rust official prompt templates and @file context enabled', !route.includes('.arg("--no-prompt-templates")') && !route.includes('.arg("--no-context-files")')],
['route exposes controlled Pi Rust RPC command wrapper', route.includes('PI_LAB_SCHEMA_RPC_COMMAND') && route.includes('build_pi_rpc_command') && route.includes('abort_bash') && route.includes('get_available_models')],
['route exposes controlled Pi Rust diagnostics wrapper', route.includes('PI_LAB_SCHEMA_DIAGNOSTICS') && route.includes('pub async fn diagnostics') && route.includes('build_pi_diagnostics_args') && routesMod.includes('/api/page-ai/pi/diagnostics')],
['route only allows read-only or cache-only Pi diagnostics commands', route.includes('doctor/context-preview/list/info/search/update-index') && route.includes('mutatesCache') && route.includes('page_ai_pi_lab_diagnostics_command_not_allowed')],
['route rejects raw Pi diagnostics args passthrough', route.includes('page_ai_pi_lab_diagnostics_raw_args_not_allowed') && route.includes('不接受裸 args 透传')],
['route passes official Pi Rust advanced runtime flags', route.includes('--extension-policy') && route.includes('--repair-policy') && route.includes('--session-durability') && route.includes('--request-timeout') && route.includes('--max-tool-iterations') && route.includes('--hide-cwd-in-prompt')],
['route exposes effective Pi advanced runtime config', route.includes('advancedRuntime') && route.includes('pi_lab_effective_advanced_runtime_config') && route.includes('MNOTE_PAGE_AI_PI_EXTENSION_POLICY') && route.includes('MNOTE_PAGE_AI_PI_REPAIR_POLICY')],
['route maps mid-stream send to official steer/follow_up commands', route.includes('Some("steer") => "steer"') && route.includes('Some("follow-up") | Some("followUp") | Some("follow_up") => "follow_up"')],
['route sends Pi RPC images payload without MNote file substitution', route.includes('pub images: Option<Vec<Value>>') && route.includes('command["images"] = json!(images)')],
['route abort uses official RPC without killing runtime process', route.includes('json!({"type": "abort"})') && !route.includes('let _ = kill_session_process(&request.session_id).await;')],
['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 enables Pi RPC image attachments', runtime.includes('pendingImages') && runtime.includes('readFileAsPiImage') && runtime.includes('body.images = piLabState.pendingImages') && !runtime.includes('Pi RPC 附件上下文尚未接入')],
['runtime exposes official abort_bash command', runtime.includes("RPC_COMMAND: '/api/page-ai/pi/rpc-command'") && runtime.includes("callPiRpcCommand('abort_bash'")],
['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 diagnostics route', routesMod.includes('/api/page-ai/pi/diagnostics')],
['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 has API.DIAGNOSTICS constant', runtime.includes("DIAGNOSTICS: '/api/page-ai/pi/diagnostics'")],
['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: '<path d=\"M4 8h16M4 16h16\"/><path d=\"M8 4 12 8 16 4M8 20l4-4 4 4\"/>")],
// === 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).');