Files
mnote/scripts/task793-openhub-mnote-context-fastapi-chain-smoke.js
T

226 lines
7.8 KiB
JavaScript
Raw Normal View History

2026-06-26 20:01:02 +08:00
#!/usr/bin/env node
"use strict";
const http = require("node:http");
const { spawn } = require("node:child_process");
const OPENHUB_BACKEND_DIR = process.env.OPENHUB_BACKEND_DIR || "/tmp/mnote-openhub-research/OpenHub/smart-query-backend";
const OPENCODE_PORT = Number(process.env.TASK793_OPENCODE_PORT || 19096);
const OPENHUB_PORT = Number(process.env.TASK793_OPENHUB_PORT || 18181);
const SESSION_ID = "ses_mnote_context_fullchain";
function wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function readBody(req) {
return new Promise((resolve) => {
let data = "";
req.on("data", (chunk) => { data += chunk; });
req.on("end", () => resolve(data));
});
}
function sseEvent(type, properties) {
return `data: ${JSON.stringify({ payload: { type, properties } })}\n\n`;
}
function startFakeOpencode() {
const capturedPrompts = [];
let eventResponse = null;
function sendEvents() {
if (!eventResponse) return false;
eventResponse.write(sseEvent("message.updated", {
sessionID: SESSION_ID,
info: { id: "msg_assistant", role: "assistant", sessionID: SESSION_ID },
}));
eventResponse.write(sseEvent("message.part.updated", {
sessionID: SESSION_ID,
part: { id: "prt_text", messageID: "msg_assistant", sessionID: SESSION_ID, type: "text", text: "OK" },
}));
eventResponse.write(sseEvent("session.status", {
sessionID: SESSION_ID,
status: { type: "idle" },
}));
eventResponse.end();
eventResponse = null;
return true;
}
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, `http://127.0.0.1:${OPENCODE_PORT}`);
if (req.method === "GET" && url.pathname === "/global/health") {
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ ok: true }));
return;
}
if (req.method === "POST" && url.pathname === "/session") {
await readBody(req);
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ id: SESSION_ID }));
return;
}
if (req.method === "GET" && url.pathname === "/global/event") {
res.writeHead(200, {
"content-type": "text/event-stream; charset=utf-8",
"cache-control": "no-cache",
connection: "keep-alive",
});
eventResponse = res;
eventResponse.write(`data: ${JSON.stringify({ payload: { type: "ready", properties: { sessionID: SESSION_ID } } })}\n\n`);
if (capturedPrompts.length) {
setTimeout(sendEvents, 50);
}
return;
}
if (req.method === "POST" && url.pathname === `/session/${SESSION_ID}/prompt_async`) {
const body = JSON.parse(await readBody(req) || "{}");
capturedPrompts.push(body);
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ ok: true }));
setTimeout(sendEvents, 50);
return;
}
res.writeHead(404, { "content-type": "application/json" });
res.end(JSON.stringify({ error: "not_found", path: url.pathname }));
});
return {
capturedPrompts,
listen: () => new Promise((resolve) => server.listen(OPENCODE_PORT, "127.0.0.1", resolve)),
close: () => new Promise((resolve) => server.close(resolve)),
};
}
async function waitForHealth(url, timeoutMs = 20_000) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
try {
const response = await fetch(url, { signal: AbortSignal.timeout(2_000) });
if (response.status < 500) return;
} catch {}
await wait(250);
}
throw new Error(`等待服务超时: ${url}`);
}
async function sendOpenHub(payload, extraHeaders = {}) {
const body = JSON.stringify(payload);
return new Promise((resolve, reject) => {
const req = http.request({
hostname: "127.0.0.1",
port: OPENHUB_PORT,
path: "/api/query/stream",
method: "POST",
headers: {
"content-type": "application/json",
"content-length": Buffer.byteLength(body),
"x-mnote-user-key": "openhub_user_test",
"x-mnote-workspace-key": "workspace_test",
"x-mnote-session-scope": `session_${Date.now()}_${Math.random().toString(16).slice(2)}`,
"x-mnote-root-uri": "file:///tmp/mnote-fullchain-workspace",
"x-mnote-page-resource-id": "local-md:Inbox~2FPage.md",
...extraHeaders,
},
}, (res) => {
let text = "";
const timeout = setTimeout(() => {
req.destroy();
resolve(text);
}, 10_000);
res.setEncoding("utf8");
res.on("data", (chunk) => {
text += chunk;
if (text.includes("message_complete")) {
clearTimeout(timeout);
req.destroy();
resolve(text);
}
});
res.on("end", () => {
clearTimeout(timeout);
if (res.statusCode < 200 || res.statusCode >= 300) {
reject(new Error(`OpenHub HTTP ${res.statusCode}: ${text.slice(0, 500)}`));
} else {
resolve(text);
}
});
});
req.on("error", (error) => {
if (error.code === "ECONNRESET") return;
reject(error);
});
req.write(body);
req.end();
});
}
async function main() {
const fakeOpencode = startFakeOpencode();
let uvicorn = null;
let stderr = "";
await fakeOpencode.listen();
try {
uvicorn = spawn(".venv/bin/uvicorn", ["app.main:app", "--host", "127.0.0.1", "--port", String(OPENHUB_PORT)], {
cwd: OPENHUB_BACKEND_DIR,
env: {
...process.env,
OPENCODE_BASE_URL: `http://localhost:${OPENCODE_PORT}`,
MNOTE_OPENHUB_TOOL_BRIDGE_AUTO: "0",
SQLITE_DB_PATH: "/tmp/openhub-fullchain-smoke.db",
NO_PROXY: "127.0.0.1,localhost",
no_proxy: "127.0.0.1,localhost",
},
stdio: ["ignore", "ignore", "pipe"],
});
uvicorn.stderr.on("data", (chunk) => { stderr += String(chunk); });
await waitForHealth(`http://127.0.0.1:${OPENHUB_PORT}/api/health`);
const tabUrl = "http://127.0.0.1:3000/documents/local-md:Inbox~2FPage.md?sourceKind=local_folder&rootUri=file:///tmp/mnote-fullchain-workspace";
await sendOpenHub({
question: "只回答 OK",
conversation_id: "",
agent: "build",
mnote_context: {
kind: "tab",
value: tabUrl,
tabUrl,
rootUri: "file:///tmp/mnote-fullchain-workspace",
documentId: "local-md:Inbox~2FPage.md",
relativePath: "Inbox/Page.md",
},
});
await wait(200);
await sendOpenHub({ question: "只回答 OK fallback", conversation_id: "", agent: "build" });
await wait(200);
const explicitPrompt = fakeOpencode.capturedPrompts[0]?.parts?.[0]?.text || "";
const fallbackPrompt = fakeOpencode.capturedPrompts[1]?.parts?.[0]?.text || "";
const result = {
ok: false,
promptCount: fakeOpencode.capturedPrompts.length,
explicitContextOk: explicitPrompt.includes("<mnote_current_context>")
&& explicitPrompt.includes("当前页面: http://127.0.0.1:3000/documents/")
&& explicitPrompt.includes("relativePath: Inbox/Page.md"),
fallbackContextOk: fallbackPrompt.includes("<mnote_current_context>")
&& fallbackPrompt.includes("source: mnote_scope_fallback")
&& fallbackPrompt.includes("file:///tmp/mnote-fullchain-workspace"),
explicitPromptPreview: explicitPrompt.slice(0, 700),
fallbackPromptPreview: fallbackPrompt.slice(0, 700),
};
result.ok = result.promptCount >= 2 && result.explicitContextOk && result.fallbackContextOk;
console.log(JSON.stringify(result, null, 2));
if (!result.ok) process.exitCode = 1;
} finally {
if (uvicorn) {
uvicorn.kill("SIGTERM");
await wait(400);
}
await fakeOpencode.close();
if (process.exitCode) process.stderr.write(stderr.slice(-3000));
}
}
main().catch((error) => {
console.error(error.stack || error.message);
process.exit(1);
});