2026-07-04 21:47:42 +08:00
|
|
|
#!/usr/bin/env node
|
|
|
|
|
// Pi Lab API endpoint smoke
|
|
|
|
|
// 验证 Pi Lab API 路由在 mnote-web 开发环境下的响应
|
|
|
|
|
// 需要 mnote-web 已在运行(npm run desktop:hot 或独立启动)
|
2026-07-10 10:54:34 +08:00
|
|
|
// 检查:新端点 start/send/abort/events、SSE、permission-system managed builtin tools、receipt、no polling
|
2026-07-04 21:47:42 +08:00
|
|
|
|
|
|
|
|
const BASE = process.env.MNOTE_PI_LAB_BASE || 'http://127.0.0.1:3000';
|
|
|
|
|
const AUTH_COOKIE = process.env.MNOTE_PI_LAB_AUTH || '';
|
2026-07-10 10:54:34 +08:00
|
|
|
const ACTOR_ID = process.env.MNOTE_E2E_ACTOR_ID || 'mnote-e2e';
|
2026-07-04 21:47:42 +08:00
|
|
|
|
|
|
|
|
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 = {}) {
|
2026-07-10 10:54:34 +08:00
|
|
|
const headers = {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
'x-mnote-actor-id': ACTOR_ID,
|
|
|
|
|
'x-mnote-actor-type': 'user',
|
|
|
|
|
...options.headers,
|
|
|
|
|
};
|
2026-07-04 21:47:42 +08:00
|
|
|
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' };
|
|
|
|
|
}));
|
|
|
|
|
|
2026-07-10 10:54:34 +08:00
|
|
|
// 3. Status response has managedPiBuiltinTools when enabled
|
|
|
|
|
results.push(await check('GET /api/page-ai/pi/status has managedPiBuiltinTools', async () => {
|
2026-07-04 21:47:42 +08:00
|
|
|
const { body } = await fetchJson(`${BASE}/api/page-ai/pi/status`);
|
|
|
|
|
if (body.enabled === false) return { passed: true }; // skip when disabled
|
2026-07-10 10:54:34 +08:00
|
|
|
if (Array.isArray(body.managedPiBuiltinTools)) return { passed: true };
|
|
|
|
|
return { passed: false, reason: 'missing managedPiBuiltinTools' };
|
2026-07-04 21:47:42 +08:00
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
// 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/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) return { passed: true };
|
|
|
|
|
return { passed: true }; // routed correctly
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
// 6. 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) return { passed: true };
|
|
|
|
|
return { passed: true };
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
// 7. 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`, {
|
2026-07-10 10:54:34 +08:00
|
|
|
headers: {
|
|
|
|
|
Accept: 'text/event-stream',
|
|
|
|
|
'x-mnote-actor-id': ACTOR_ID,
|
|
|
|
|
'x-mnote-actor-type': 'user',
|
|
|
|
|
},
|
2026-07-04 21:47:42 +08:00
|
|
|
});
|
|
|
|
|
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}` };
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
// 8. 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) 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
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
// 10. 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 };
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
});
|