197 lines
15 KiB
JavaScript
197 lines
15 KiB
JavaScript
#!/usr/bin/env node
|
|
// Pi Lab static code smoke
|
|
// 验证 Pi Lab 相关源码结构正确,不依赖后端运行
|
|
// 确认:新端点、状态机、无轮询、SSE、Pi builtin 禁用、allowed roots、receipt
|
|
// 确认:默认模型 omniroute/freefirst 在前端 UI 和 header 中明确体现
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const repoRoot = path.resolve(__dirname, '..');
|
|
|
|
// Files to check
|
|
const files = {
|
|
runtime: path.join(repoRoot, 'rust/crates/mnote-web/browser/sidebar-page-ai-pi-lab-runtime.js'),
|
|
route: path.join(repoRoot, 'rust/crates/mnote-web/src/routes/page_ai_pi.rs'),
|
|
mod: path.join(repoRoot, 'rust/crates/mnote-web/src/routes/mod.rs'),
|
|
webShell: path.join(repoRoot, 'rust/crates/mnote-web/src/routes/web_shell.rs'),
|
|
gateway: path.join(repoRoot, 'rust/crates/mnote-web/src/routes/gateway.rs'),
|
|
app: path.join(repoRoot, 'rust/crates/mnote-web/src/app.rs'),
|
|
};
|
|
|
|
function readFile(p) {
|
|
try { return fs.readFileSync(p, 'utf8'); } catch { return ''; }
|
|
}
|
|
|
|
const runtime = readFile(files.runtime);
|
|
const route = readFile(files.route);
|
|
const routesMod = readFile(files.mod);
|
|
const webShell = readFile(files.webShell);
|
|
const gateway = readFile(files.gateway);
|
|
const app = readFile(files.app);
|
|
|
|
const checks = [
|
|
// === Runtime JS: existence ===
|
|
['runtime JS exists', runtime.length > 0],
|
|
['runtime exports createSidebarPageAiPiLabRuntime', runtime.includes('window.createSidebarPageAiPiLabRuntime')],
|
|
|
|
// === Runtime JS: API endpoints (new flow: start/send/abort/events) ===
|
|
['runtime uses /api/page-ai/pi/status', runtime.includes('/api/page-ai/pi/status')],
|
|
['runtime uses /api/page-ai/pi/start', runtime.includes('/api/page-ai/pi/start')],
|
|
['runtime uses /api/page-ai/pi/send', runtime.includes('/api/page-ai/pi/send')],
|
|
['runtime uses /api/page-ai/pi/abort', runtime.includes('/api/page-ai/pi/abort')],
|
|
['runtime uses /api/page-ai/pi/events', runtime.includes('/api/page-ai/pi/events')],
|
|
['runtime keeps legacy /api/page-ai/pi/bootstrap fallback', runtime.includes('/api/page-ai/pi/bootstrap')],
|
|
|
|
// === Runtime JS: state machine ===
|
|
['runtime has idle state', runtime.includes("STATE_IDLE") || (runtime.includes("'idle'") && runtime.includes('STATE_IDLE'))],
|
|
['runtime has starting state', runtime.includes("STATE_STARTING")],
|
|
['runtime has started state', runtime.includes("STATE_STARTED")],
|
|
['runtime has streaming state', runtime.includes("STATE_STREAMING")],
|
|
['runtime has aborted state', runtime.includes("STATE_ABORTED")],
|
|
['runtime has error state', runtime.includes("STATE_ERROR")],
|
|
|
|
// === Runtime JS: NO periodic polling ===
|
|
['runtime does NOT use setInterval for periodic polling',
|
|
!runtime.includes('setInterval(checkStatus') && !runtime.includes("setInterval(checkStatus") &&
|
|
!runtime.match(/setInterval\s*\([^)]*checkStatus/i)],
|
|
['runtime comments "NO setInterval polling"', runtime.includes('NO setInterval polling')],
|
|
|
|
// === Runtime JS: SSE / EventSource ===
|
|
['runtime uses EventSource for SSE', runtime.includes('EventSource')],
|
|
['runtime connects to /api/page-ai/pi/events via SSE', runtime.includes('API.EVENTS') || runtime.includes('/api/page-ai/pi/events')],
|
|
['runtime handles pi_rpc_event from SSE', runtime.includes('pi_rpc_event')],
|
|
['runtime handles runtime_started event', runtime.includes('runtime_started')],
|
|
['runtime handles runtime_aborted event', runtime.includes('runtime_aborted')],
|
|
|
|
// === Runtime JS: UI rendering ===
|
|
['runtime renders stream text', runtime.includes('text_delta') || runtime.includes('streamingAssistantMsg.text')],
|
|
['runtime renders tool calls', runtime.includes('toolCalls')],
|
|
['runtime renders citations', runtime.includes('citations')],
|
|
['runtime renders diff summary', runtime.includes('diffSummary')],
|
|
['runtime has start button', runtime.includes('btn-start')],
|
|
['runtime has send button', runtime.includes('btn-send')],
|
|
['runtime has abort button', runtime.includes('btn-abort')],
|
|
['runtime has clear button', runtime.includes('btn-clear')],
|
|
|
|
// === Runtime JS: Pi builtin disabled ===
|
|
['runtime handles disabledPiBuiltinTools from status/start', runtime.includes('disabledBuiltinTools') || runtime.includes('disabledPiBuiltinTools')],
|
|
['runtime has Pi builtin disabled UI indicator', runtime.includes('builtin-disabled')],
|
|
['runtime mentions bash/read/write/edit disabled', runtime.includes('bash') && runtime.includes('read') && runtime.includes('write') && runtime.includes('edit')],
|
|
|
|
// === Runtime JS: tool receipt ===
|
|
['runtime references receipt', runtime.includes('receipt') || runtime.includes('Receipt')],
|
|
['runtime has receipt UI display', runtime.includes('receipts')],
|
|
|
|
// === Runtime JS: independent native drawer ===
|
|
['runtime checks enabled flag via status API but does not hide launcher behind it', runtime.includes('enabled') && runtime.includes('checkStatus')],
|
|
['runtime renders independent Pi Lab drawer', runtime.includes('data-page-ai-pi-lab-drawer') && runtime.includes('data-page-ai-pi-lab') && runtime.includes('drawer')],
|
|
['runtime creates drawer through ensureDrawer', runtime.includes('function ensureDrawer') && runtime.includes('setDrawerVisible')],
|
|
['runtime does NOT mount inside OpenHub drawer', !runtime.includes('attachPanelToDrawer') && !runtime.includes('wolai-page-ai-drawer')],
|
|
['runtime does NOT toggle OpenHub iframe visibility', !runtime.includes('setOpenHubVisible') && !runtime.includes('data-page-ai-openhub-frame-wrap')],
|
|
['runtime has no OpenHub provider tab inside Pi Lab', !runtime.includes('data-page-ai-pi-lab-openhub-tab')],
|
|
['runtime has context strip chips', runtime.includes('data-page-ai-pi-lab-context-strip') && runtime.includes('data-page-ai-pi-lab-current-page') && runtime.includes('data-page-ai-pi-lab-allowed-roots') && runtime.includes('data-page-ai-pi-lab-lightrag')],
|
|
['runtime collapses secondary context/settings like OpenHub chrome', runtime.includes('<details class="wolai-page-ai-pi-lab-settings"') && runtime.includes('<details class="wolai-page-ai-pi-lab-context-strip"')],
|
|
['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 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 injects CSS styles', runtime.includes('injectStyles')],
|
|
|
|
// === Runtime JS: default model (omniroute/freefirst) ===
|
|
['runtime has defaultModelProvider with omniroute', runtime.includes('defaultModelProvider') && runtime.includes('omniroute')],
|
|
['runtime has defaultModelId with freefirst', runtime.includes('defaultModelId') && runtime.includes('freefirst')],
|
|
['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/freefirst" in empty state', runtime.includes('omniroute') && runtime.includes('freefirst') && 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("'freefirst'")],
|
|
['runtime has Pi Lab floating launcher', runtime.includes('data-page-ai-pi-lab-launcher')],
|
|
['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 send endpoint', route.includes('pub async fn send')],
|
|
['route has abort endpoint', route.includes('pub async fn abort')],
|
|
['route has events SSE endpoint', route.includes('pub async fn events')],
|
|
['route has bootstrap endpoint (legacy)', route.includes('pub async fn bootstrap')],
|
|
['route has tool_call endpoint', route.includes('pub async fn tool_call')],
|
|
['route has internal tool_call_bridge endpoint', route.includes('pub async fn tool_call_bridge')],
|
|
['route uses AppConfig enable_page_ai_pi_lab instead of direct env gate', route.includes('state.config().enable_page_ai_pi_lab') && !route.includes('std::env::var("MNOTE_PAGE_AI_PI_LAB")')],
|
|
['route returns enabled=false when not enabled', route.includes('"enabled": false')],
|
|
['route marks independent native drawer ui mode', route.includes('independent_mnote_native_drawer')],
|
|
['route does not return openHubDefaultPreserved marker', !route.includes('openHubDefaultPreserved')],
|
|
['route returns schema mnote.page_ai_pi.status.v1', route.includes('mnote.page_ai_pi.status.v1')],
|
|
['route returns schema mnote.page_ai_pi.bootstrap.v1', route.includes('mnote.page_ai_pi.bootstrap.v1')],
|
|
['route returns schema mnote.page_ai_pi.start.v1', route.includes('mnote.page_ai_pi.start.v1')],
|
|
['route returns schema mnote.page_ai_pi.send.v1', route.includes('mnote.page_ai_pi.send.v1')],
|
|
['route returns schema mnote.page_ai_pi.abort.v1', route.includes('mnote.page_ai_pi.abort.v1')],
|
|
['route has event schema PI_LAB_SCHEMA_EVENT', route.includes('PI_LAB_SCHEMA_EVENT')],
|
|
['route has receipt schema PI_LAB_SCHEMA_RECEIPT', route.includes('PI_LAB_SCHEMA_RECEIPT')],
|
|
['route disables Pi builtin tools', route.includes('disabledPiBuiltinTools') || route.includes('--no-builtin-tools')],
|
|
['route has receipt storage policy', route.includes('receiptStorage') || route.includes('PI_LAB_SCHEMA_RECEIPT')],
|
|
['route has session dir policy', route.includes('managedPiSessionDirPolicy')],
|
|
['route enforces allowed roots', route.includes('active_allowed_roots') || route.includes('allowed_roots')],
|
|
['route blocks path escape', route.includes('path_escape') || route.includes('path_is_inside')],
|
|
['route has default model provider constant PI_LAB_DEFAULT_MODEL_PROVIDER=omniroute', route.includes('PI_LAB_DEFAULT_MODEL_PROVIDER') && route.includes('omniroute')],
|
|
['route has default model id constant PI_LAB_DEFAULT_MODEL_ID=freefirst', route.includes('PI_LAB_DEFAULT_MODEL_ID') && route.includes('freefirst')],
|
|
['route rejects arbitrary cookie_header auth bypass', route.includes('page_ai_pi_lab_invalid_session_cookie') && !route.includes('context.auth.cookie_header.is_some() {\n return Ok') ],
|
|
['route stores Pi Lab session owner id', route.includes('mnote_user_id') && route.includes('ensure_session_owner')],
|
|
['route validates session owner on send/abort/tool/events', route.includes('get_session_for_context')],
|
|
['route requires sidebar host selection snapshot', route.includes('page_ai_pi_lab_selection_snapshot_required') && route.includes('mnote_sidebar_host_snapshot')],
|
|
['route tool_receipt.write is not a silent no-op', route.includes('"requestedReceipt"') && route.includes('execute_tool 统一写入')],
|
|
['route has bridge token header and non-serialized session token', route.includes('HEADER_PI_LAB_BRIDGE_TOKEN') && route.includes('x-mnote-pi-lab-bridge-token') && route.includes('skip_serializing')],
|
|
['route generates bridge token from OS randomness', route.includes('generate_bridge_token') && route.includes('/dev/urandom') && !route.includes('bridge_token: generate_id("pi_bridge")')],
|
|
['route does not write bridge token literal into extension file', route.includes('MNOTE_PI_LAB_BRIDGE_TOKEN') && route.includes('process.env.MNOTE_PI_LAB_BRIDGE_TOKEN') && !route.includes('const BRIDGE_TOKEN = {bridge_token}')],
|
|
['route rejects bridge calls for non-running sessions', route.includes('page_ai_pi_lab_bridge_session_not_running') && route.includes('PiLabSessionStatus::RuntimeRunning | PiLabSessionStatus::TurnRunning')],
|
|
['route generates Pi extension with registered MNote tools', route.includes('ensure_session_tool_bridge_extension') && route.includes('pi.registerTool') && route.includes('mnote_current_page_read')],
|
|
['route starts Pi with explicit extension bridge', route.includes('--extension') && route.includes('mnoteToolBridgeExtension')],
|
|
['route starts Pi with MNote tool allowlist', route.includes('--tools') && route.includes('mnoteToolNames') && route.includes('pi_lab_extension_tool_names')],
|
|
['route keeps Pi builtin tools disabled while loading extension', route.includes('--no-builtin-tools') && route.includes('disabledBuiltinTools')],
|
|
['route applies TTL and rate limits', route.includes('PI_LAB_SESSION_TTL_MS') && route.includes('PI_LAB_RATE_LIMITS') && route.includes('check_rate_limit')],
|
|
['route cleans expired session dirs and rate buckets', route.includes('fs::remove_dir_all') && route.includes('buckets.retain')],
|
|
|
|
// === mod.rs checks ===
|
|
['mod.rs declares page_ai_pi module', routesMod.includes('mod page_ai_pi;')],
|
|
['mod.rs mounts pi status route', routesMod.includes('/api/page-ai/pi/status')],
|
|
['mod.rs mounts pi start route', routesMod.includes('/api/page-ai/pi/start')],
|
|
['mod.rs mounts pi send route', routesMod.includes('/api/page-ai/pi/send')],
|
|
['mod.rs mounts pi abort route', routesMod.includes('/api/page-ai/pi/abort')],
|
|
['mod.rs mounts pi events route', routesMod.includes('/api/page-ai/pi/events')],
|
|
['mod.rs mounts pi tool-call route', routesMod.includes('/api/page-ai/pi/tool-call')],
|
|
['mod.rs mounts pi internal tool-call-bridge route', routesMod.includes('/api/page-ai/pi/tool-call-bridge')],
|
|
['mod.rs mounts pi bootstrap route (legacy)', routesMod.includes('/api/page-ai/pi/bootstrap')],
|
|
['mod.rs mounts pi tool_call route', routesMod.includes('/api/page-ai/pi/tool-call')],
|
|
['mod.rs mounts pi lab runtime asset', routesMod.includes('sidebar-page-ai-pi-lab-runtime.js')],
|
|
|
|
// === web_shell.rs checks ===
|
|
['web_shell.rs has pi lab runtime asset function', webShell.includes('sidebar_page_ai_pi_lab_runtime_asset')],
|
|
['web_shell.rs includes pi lab runtime JS', webShell.includes('sidebar-page-ai-pi-lab-runtime.js')],
|
|
|
|
|
|
|
|
// === gateway.rs checks ===
|
|
['gateway.rs loads Pi Lab runtime when config enabled', gateway.includes('createSidebarPageAiPiLabRuntime')],
|
|
['gateway.rs does NOT stamp body hidden gate', !gateway.includes('data-page-ai-pi-lab-hidden')],
|
|
['app.rs defaults Pi Lab config on', app.includes('enable_page_ai_pi_lab: env_bool("MNOTE_PAGE_AI_PI_LAB", true)')],
|
|
|
|
// === Key architectural constraints ===
|
|
['no modification of sidebar-page-ai-runtime', !runtime.includes('sidebar-page-ai-runtime')],
|
|
['no sidebar-page-ai-runtime default behavior change',
|
|
!runtime.includes('sidebarPageAiRuntime')],
|
|
['no import of main page AI runtime', !runtime.includes('import.*sidebar-page-ai')],
|
|
];
|
|
|
|
const failed = checks.filter(([, ok]) => !ok);
|
|
if (failed.length) {
|
|
console.error('❌ Pi Lab static smoke failed:');
|
|
for (const [name] of failed) console.error(` - ${name}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log('✅ Pi Lab static smoke passed (' + checks.length + ' checks).');
|