#!/usr/bin/env node // Pi Lab API endpoint smoke // 验证 Pi Lab API 路由在 mnote-web 开发环境下的响应 // 需要 mnote-web 已在运行(npm run desktop:hot 或独立启动) // 检查:新端点 start/send/abort/events、SSE、permission-system managed builtin tools、receipt、no polling const BASE = process.env.MNOTE_PI_LAB_BASE || 'http://127.0.0.1:3000'; const AUTH_COOKIE = process.env.MNOTE_PI_LAB_AUTH || ''; const ACTOR_ID = process.env.MNOTE_E2E_ACTOR_ID || 'mnote-e2e'; async function check(description, fn) { try { const result = await fn(); if (result.passed) { console.log(` ✅ ${description}`); return true; } else { console.error(` ❌ ${description}: ${result.reason}`); return false; } } catch (err) { console.error(` ❌ ${description}: ${err.message}`); return false; } } async function fetchJson(url, options = {}) { const headers = { 'Content-Type': 'application/json', 'x-mnote-actor-id': ACTOR_ID, 'x-mnote-actor-type': 'user', ...options.headers, }; if (AUTH_COOKIE && AUTH_COOKIE.toLowerCase().startsWith('bearer ')) headers.Authorization = AUTH_COOKIE; else if (AUTH_COOKIE) headers.Cookie = AUTH_COOKIE; const res = await fetch(url, { ...options, headers }); const body = await res.json(); return { status: res.status, body }; } async function main() { console.log(`\n🧪 Pi Lab API smoke (base: ${BASE})\n`); const results = []; // 1. Status route returns the stable status schema in either disabled or enabled mode. results.push(await check('GET /api/page-ai/pi/status returns stable status schema', async () => { const { status, body } = await fetchJson(`${BASE}/api/page-ai/pi/status`); if (status === 401 || status === 403) return { passed: true }; if (status !== 200) return { passed: false, reason: `status ${status}` }; if (typeof body.enabled !== 'boolean') return { passed: false, reason: 'missing enabled boolean' }; if (body.schema !== 'mnote.page_ai_pi.status.v1') return { passed: false, reason: `schema mismatch: ${body.schema}` }; if (body.uiMode !== 'independent_mnote_native_drawer' && body.enabled !== false) { return { passed: false, reason: `unexpected uiMode: ${body.uiMode}` }; } return { passed: true }; })); // 2. Status response has managedPiSessionDirPolicy results.push(await check('GET /api/page-ai/pi/status has managedPiSessionDirPolicy and receiptStorage', async () => { const { body } = await fetchJson(`${BASE}/api/page-ai/pi/status`); if (body.managedPiSessionDirPolicy) return { passed: true }; // Accept missing fields when disabled if (body.enabled === false) return { passed: true }; return { passed: false, reason: 'missing managedPiSessionDirPolicy' }; })); // 3. Status response has managedPiBuiltinTools when enabled results.push(await check('GET /api/page-ai/pi/status has managedPiBuiltinTools', async () => { const { body } = await fetchJson(`${BASE}/api/page-ai/pi/status`); if (body.enabled === false) return { passed: true }; // skip when disabled if (Array.isArray(body.managedPiBuiltinTools)) return { passed: true }; return { passed: false, reason: 'missing managedPiBuiltinTools' }; })); // 4. Start endpoint exists and returns proper schema (may 404 if disabled) results.push(await check('POST /api/page-ai/pi/start returns proper response (disabled may 401/404)', async () => { const { status, body } = await fetchJson(`${BASE}/api/page-ai/pi/start`, { method: 'POST', body: JSON.stringify({}), }); if (status === 404 || status === 401 || status === 403) return { passed: true }; // disabled // If enabled, check schema if (body.schema === 'mnote.page_ai_pi.start.v1' || body.session) return { passed: true }; return { passed: true }; // Accept any non-error response })); // 5. Send endpoint schema results.push(await check('POST /api/page-ai/pi/configure exists for model/thinking changes (disabled may 404)', async () => { const { status, body } = await fetchJson(`${BASE}/api/page-ai/pi/configure`, { method: 'POST', body: JSON.stringify({ sessionId: 'test', modelProvider: 'omniroute', modelId: 'freefirst', thinkingLevel: 'off' }), }); if (status === 404 || status === 401 || status === 403 || status === 400) return { passed: true }; if (body.schema === 'mnote.page_ai_pi.configure.v1' || body.ok === true) return { passed: true }; return { passed: false, reason: `unexpected configure response: ${JSON.stringify(body).slice(0, 200)}` }; })); // 6. Send endpoint schema results.push(await check('POST /api/page-ai/pi/send returns proper schema (disabled may 404)', async () => { const { status } = await fetchJson(`${BASE}/api/page-ai/pi/send`, { method: 'POST', body: JSON.stringify({ sessionId: 'test', message: 'hello' }), }); if (status === 404 || status === 401 || status === 403 || status === 400) return { passed: true }; return { passed: true }; // routed correctly })); // 7. Abort endpoint results.push(await check('POST /api/page-ai/pi/abort returns proper response (disabled may 404)', async () => { const { status } = await fetchJson(`${BASE}/api/page-ai/pi/abort`, { method: 'POST', body: JSON.stringify({ sessionId: 'test' }), }); if (status === 404 || status === 401 || status === 403 || status === 400) return { passed: true }; return { passed: true }; })); // 8. Events SSE endpoint returns proper content type results.push(await check('GET /api/page-ai/pi/events returns SSE stream (disabled may 404)', async () => { const res = await fetch(`${BASE}/api/page-ai/pi/events`, { headers: { Accept: 'text/event-stream', 'x-mnote-actor-id': ACTOR_ID, 'x-mnote-actor-type': 'user', }, }); if (res.status === 404 || res.status === 401 || res.status === 403) return { passed: true }; const ct = res.headers.get('Content-Type') || ''; if (ct.includes('text/event-stream') || ct.includes('text/plain')) return { passed: true }; return { passed: false, reason: `unexpected Content-Type: ${ct}` }; })); // 9. Tool call endpoint results.push(await check('POST /api/page-ai/pi/tool-call exists (disabled may 404)', async () => { const { status } = await fetchJson(`${BASE}/api/page-ai/pi/tool-call`, { method: 'POST', body: JSON.stringify({ toolName: 'mnote.allowed_roots.describe', params: {} }), }); if (status === 404 || status === 401 || status === 403 || status === 400) return { passed: true }; return { passed: true }; })); // 9. Bootstrap legacy endpoint results.push(await check('POST /api/page-ai/pi/bootstrap returns 404 when disabled', async () => { const { status } = await fetchJson(`${BASE}/api/page-ai/pi/bootstrap`, { method: 'POST', body: JSON.stringify({ prompt: 'test' }), }); if (status === 404) return { passed: true }; return { passed: true }; // accept any response — mounted })); // 11. State endpoint results.push(await check("POST /api/page-ai/pi/state returns proper schema (disabled may 404)", async () => { const { status, body } = await fetchJson(`${BASE}/api/page-ai/pi/state`, { method: "POST", body: JSON.stringify({ sessionId: "test" }), }); if (status === 404 || status === 401 || status === 403 || status === 400) return { passed: true }; if (status !== 200) return { passed: false, reason: `status ${status}` }; if (body.schema !== "mnote.page_ai_pi.state.v1") return { passed: false, reason: `schema mismatch: ${body.schema}` }; if (typeof body.running !== "boolean") return { passed: false, reason: "missing running boolean" }; if (typeof body.pendingMessageCount !== "number") return { passed: false, reason: "missing pendingMessageCount number" }; return { passed: true }; })); // 12. Compact endpoint results.push(await check("POST /api/page-ai/pi/compact returns proper schema (disabled may 404)", async () => { const { status, body } = await fetchJson(`${BASE}/api/page-ai/pi/compact`, { method: "POST", body: JSON.stringify({ sessionId: "test" }), }); if (status === 404 || status === 401 || status === 403 || status === 400) return { passed: true }; if (status !== 200) return { passed: false, reason: `status ${status}` }; if (body.schema !== "mnote.page_ai_pi.compact.v1") return { passed: false, reason: `schema mismatch: ${body.schema}` }; if (typeof body.summary !== "string") return { passed: false, reason: "missing summary string" }; return { passed: true }; })); // 13. Queue-config endpoint results.push(await check("POST /api/page-ai/pi/queue-config returns proper schema (disabled may 404)", async () => { const { status, body } = await fetchJson(`${BASE}/api/page-ai/pi/queue-config`, { method: "POST", body: JSON.stringify({ sessionId: "test", steeringMode: "one-at-a-time", followUpMode: "all", autoCompaction: true }), }); if (status === 404 || status === 401 || status === 403 || status === 400) return { passed: true }; if (status !== 200) return { passed: false, reason: `status ${status}` }; if (body.schema !== "mnote.page_ai_pi.queue_config.v1") return { passed: false, reason: `schema mismatch: ${body.schema}` }; if (typeof body.applied !== "object") return { passed: false, reason: "missing applied object" }; return { passed: true }; })); // 14. Runtime asset exists results.push(await check('GET /api/mnote-browser-runtime/sidebar-page-ai-pi-lab-runtime.js returns 200', async () => { const res = await fetch(`${BASE}/api/mnote-browser-runtime/sidebar-page-ai-pi-lab-runtime.js`); if (res.status !== 200) return { passed: false, reason: `status ${res.status}` }; const text = await res.text(); if (!text.includes('createSidebarPageAiPiLabRuntime')) return { passed: false, reason: 'missing expected export' }; if (!text.includes('data-page-ai-pi-lab-drawer')) return { passed: false, reason: 'missing independent drawer marker' }; if (text.includes('attachPanelToDrawer') || text.includes('setOpenHubVisible')) return { passed: false, reason: 'runtime still references OpenHub drawer integration' }; if (text.includes('data-page-ai-pi-lab-openhub-tab')) return { passed: false, reason: 'runtime still exposes OpenHub tab inside Pi Lab' }; if (/setInterval\s*\(/.test(text)) return { passed: false, reason: 'runtime still has setInterval call' }; if (!text.includes('NO setInterval polling')) return { passed: false, reason: 'missing NO setInterval polling comment' }; if (!text.includes('EventSource')) return { passed: false, reason: 'missing EventSource for SSE' }; if (!text.includes('STATE_STARTED')) return { passed: false, reason: 'missing state machine states' }; if (!text.includes('/api/page-ai/pi/start')) return { passed: false, reason: 'missing /api/page-ai/pi/start endpoint' }; if (!text.includes('/api/page-ai/pi/send')) return { passed: false, reason: 'missing /api/page-ai/pi/send endpoint' }; if (!text.includes('/api/page-ai/pi/abort')) return { passed: false, reason: 'missing /api/page-ai/pi/abort endpoint' }; if (!text.includes('/api/page-ai/pi/events')) return { passed: false, reason: 'missing /api/page-ai/pi/events endpoint' }; return { passed: true }; })); // 15. Session tree endpoint results.push(await check("GET /api/page-ai/pi/sessions/{sessionId}/tree returns proper schema (disabled may 404)", async () => { const { status, body } = await fetchJson(`${BASE}/api/page-ai/pi/sessions/nonexistent/tree`); if (status === 404 || status === 401 || status === 403 || status === 400) return { passed: true }; if (status !== 200) return { passed: false, reason: `status ${status}` }; if (body.schema !== "mnote.page_ai_pi.session_tree.v1") return { passed: false, reason: `schema mismatch: ${body.schema}` }; if (typeof body.sessionId !== "string") return { passed: false, reason: "missing sessionId string" }; return { passed: true }; })); // 16. Fork endpoint results.push(await check("POST /api/page-ai/pi/fork returns proper schema (disabled may 404)", async () => { const { status, body } = await fetchJson(`${BASE}/api/page-ai/pi/fork`, { method: "POST", body: JSON.stringify({ sessionId: "nonexistent" }), }); if (status === 404 || status === 401 || status === 403 || status === 400) return { passed: true }; if (status !== 200) return { passed: false, reason: `status ${status}` }; if (body.schema !== "mnote.page_ai_pi.fork.v1") return { passed: false, reason: `schema mismatch: ${body.schema}` }; if (typeof body.sourceSessionId !== "string") return { passed: false, reason: "missing sourceSessionId string" }; return { passed: true }; })); // 17. Artifact diff endpoint results.push(await check("GET /api/page-ai/pi/artifacts/{toolEventId}/diff returns proper response (disabled may 404)", async () => { const { status, body } = await fetchJson(`${BASE}/api/page-ai/pi/artifacts/nonexistent/diff?sessionId=nonexistent`); if (status === 404 || status === 401 || status === 403 || status === 400) return { passed: true }; if (status !== 200) return { passed: false, reason: `status ${status}` }; if (body.schema !== "mnote.page_ai_pi.artifact_diff.v1") return { passed: false, reason: `schema mismatch: ${body.schema}` }; if (typeof body.toolEventId !== "string") return { passed: false, reason: "missing toolEventId string" }; return { passed: true }; })); // Summary const passed = results.filter(Boolean).length; const total = results.length; console.log(`\n📊 ${passed}/${total} passed`); if (passed < total) { console.error(`❌ Pi Lab API smoke: ${total - passed} failed`); process.exit(1); } console.log('✅ Pi Lab API endpoint smoke passed.\n'); } main().catch(err => { console.error('❌ Smoke failed:', err.message); process.exit(1); });