Files
mnote/scripts/task-pi-lab-static-smoke.js
Agent Board 262e66b02e feat: purge legacy agent hosts and land vault Chrome extension path
Retire ACP/Hermes/OpenCode surfaces and rename hermes_tools to
mnote_agent_tools so Page AI stays on Pi Lab only. Add Chrome vault
extension + extension token route, pre-release purge design, and soft-retire
legacy smokes for the small-group production cut.
2026-07-25 14:25:37 +08:00

425 lines
43 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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'),
// page_ai_pi 已拆为目录模块(constants.rs / runtime.rs / mod.rs
routeDir: path.join(repoRoot, 'rust/crates/mnote-web/src/routes/page_ai_pi'),
routeMod: path.join(repoRoot, 'rust/crates/mnote-web/src/routes/page_ai_pi/mod.rs'),
routeConstants: path.join(repoRoot, 'rust/crates/mnote-web/src/routes/page_ai_pi/constants.rs'),
routeRuntime: path.join(repoRoot, 'rust/crates/mnote-web/src/routes/page_ai_pi/runtime.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);
// 合并目录模块,保持后续 route.includes(...) 检查不变
const route = [
readFile(files.routeMod),
readFile(files.routeConstants),
readFile(files.routeRuntime),
].join('\n');
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')],
// Wave 8legacy page-ai runtime 已瘦身为 Pi-only stub,不再含 OpenCode host
['legacy Page AI runtime is Pi-only stub (no OpenCode host)',
legacyPageAiRuntime.includes('Pi Lab only')
&& legacyPageAiRuntime.includes('openPageAiDrawer')
&& legacyPageAiRuntime.includes('mnote:pi-lab-show')
&& !legacyPageAiRuntime.includes('opencode_host')
&& !legacyPageAiRuntime.includes('page-ai-opencode')],
['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/ ===
["page_ai_pi module directory exists", fs.existsSync(files.routeDir) && fs.statSync(files.routeDir).isDirectory()],
["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).');