Files
mnote/scripts/desktop-hot.test.js
T

227 lines
6.8 KiB
JavaScript
Raw Normal View History

2026-04-28 16:30:51 +08:00
const assert = require("node:assert");
const { spawn } = require("node:child_process");
const net = require("node:net");
const { test } = require("node:test");
2026-04-29 12:24:44 +08:00
const {
2026-07-03 23:20:16 +08:00
buildDefaultOpencodeCommand,
2026-07-04 21:47:42 +08:00
collectStaleMnoteWebCargoPids,
2026-04-29 12:24:44 +08:00
resolveBackendExecutable,
ensurePortFree,
2026-07-03 23:20:16 +08:00
isHttpHealthy,
2026-04-29 12:24:44 +08:00
isPortFree,
resolveRuntimePlan,
shouldStartBackend,
2026-07-13 09:00:19 +08:00
shouldStartOpencode,
2026-07-04 21:47:42 +08:00
stopStaleMnoteWebCargoProcesses,
2026-04-29 12:24:44 +08:00
} = require("./desktop-hot.js");
2026-04-28 16:30:51 +08:00
function findFreePort() {
return new Promise((resolve, reject) => {
const server = net.createServer();
server.listen(0, "127.0.0.1", () => {
const address = server.address();
if (!address || typeof address === "string") {
server.close(() => reject(new Error("无法分配测试端口")));
return;
}
const port = address.port;
server.close(() => resolve(port));
});
server.on("error", reject);
});
}
function startPythonListener(port) {
const pythonBin = resolveBackendExecutable("PYTHON_BIN", "python");
const child = spawn(
pythonBin,
[
"-c",
`
import socket
import time
import threading
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(("127.0.0.1", ${port}))
server.listen(16)
def accept_loop():
while True:
conn, _addr = server.accept()
conn.close()
threading.Thread(target=accept_loop, daemon=True).start()
print("ready", flush=True)
while True:
time.sleep(1)
`,
],
{
stdio: ["ignore", "pipe", "pipe"],
},
);
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
child.kill("SIGKILL");
reject(new Error("测试监听进程启动超时"));
}, 3000);
child.stdout.on("data", (chunk) => {
if (chunk.toString("utf8").includes("ready")) {
clearTimeout(timeout);
resolve(child);
}
});
child.on("error", (error) => {
clearTimeout(timeout);
reject(error);
});
});
}
function waitForExit(child, timeoutMs = 3000) {
if (child.exitCode !== null || child.signalCode !== null) {
return Promise.resolve();
}
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
child.off("exit", onExit);
reject(new Error(`进程 ${child.pid} 未在 ${timeoutMs}ms 内退出`));
}, timeoutMs);
const onExit = () => {
clearTimeout(timeout);
resolve();
};
child.once("exit", onExit);
});
}
test("ensurePortFree 在非 Windows 平台能释放后端监听进程", async (t) => {
if (process.platform === "win32") {
t.skip("该用例只覆盖 Linux/macOS 分支");
return;
}
const port = await findFreePort();
const child = await startPythonListener(port);
t.after(() => {
if (!child.killed) {
child.kill("SIGKILL");
}
});
assert.equal(await isPortFree("127.0.0.1", port), false);
const ok = await ensurePortFree(port, "backend");
assert.equal(ok, true);
assert.equal(await isPortFree("127.0.0.1", port), true);
await waitForExit(child);
});
2026-04-29 12:24:44 +08:00
2026-07-04 21:47:42 +08:00
test("陈旧 mnote-web cargo 清理不会匹配当前测试进程", async () => {
if (process.platform === "win32") {
return;
}
const pids = collectStaleMnoteWebCargoPids();
assert.ok(Array.isArray(pids));
assert.equal(pids.includes(process.pid), false);
await stopStaleMnoteWebCargoProcesses();
});
test("默认热启动计划只使用 mnote-web 作为 3000 owner", () => {
2026-04-29 12:24:44 +08:00
const plan = resolveRuntimePlan({
FRONTEND_PORT: "3000",
});
assert.equal(plan.skipGateway, false);
assert.equal(plan.publicPort, 3000);
2026-05-06 21:44:20 +08:00
assert.equal(plan.mnoteWebCommand, "cargo run -p mnote-web --bin mnote-web");
assert.deepEqual(plan.mnoteWebEnv, {
MNOTE_WEB_BIND: "0.0.0.0:3000",
2026-05-06 21:44:20 +08:00
MNOTE_WEB_PUBLIC_BIND: "127.0.0.1:3000",
2026-07-03 23:20:16 +08:00
MNOTE_KNOWLEDGE_PROVIDER: "lightrag_legacy",
2026-06-25 21:08:17 +08:00
MNOTE_OPENCODE_BASE_URL: "http://127.0.0.1:4096",
2026-07-03 23:20:16 +08:00
MNOTE_CONTROL_PLANE_BACKEND: "libsql-local",
MNOTE_TURSO_LOCAL_PATH: "/mnt/Data1T/Mnote_data/control-plane/control-plane-libsql.db",
2026-05-06 21:44:20 +08:00
});
});
2026-07-03 23:20:16 +08:00
test("热启动计划支持 libSQL local 控制面后端", () => {
const plan = resolveRuntimePlan({
FRONTEND_PORT: "3000",
MNOTE_CONTROL_PLANE_BACKEND: "libsql-local",
});
assert.equal(plan.mnoteWebEnv.MNOTE_CONTROL_PLANE_BACKEND, "libsql-local");
assert.equal(
plan.mnoteWebEnv.MNOTE_TURSO_LOCAL_PATH,
"/mnt/Data1T/Mnote_data/control-plane/control-plane-libsql.db",
);
assert.equal(plan.mnoteWebEnv.MNOTE_CONTROL_PLANE_DB_PATH, undefined);
});
test("热启动计划拒绝 SQLite runtime fallback", () => {
assert.throws(
() => resolveRuntimePlan({
FRONTEND_PORT: "3000",
MNOTE_CONTROL_PLANE_BACKEND: "sqlite",
}),
/不再支持 SQLite control-plane fallback/,
);
});
test("显式配置知识库 provider 时透传给 MNote host", () => {
const plan = resolveRuntimePlan({
FRONTEND_PORT: "3000",
MNOTE_KNOWLEDGE_PROVIDER: "weknora",
});
assert.equal(plan.mnoteWebEnv.MNOTE_KNOWLEDGE_PROVIDER, "weknora");
});
test("默认跳过 FastAPI 后端,只有显式开启时才启动", () => {
assert.equal(shouldStartBackend({}), false);
assert.equal(shouldStartBackend({ ENABLE_BACKEND: "1" }), true);
assert.equal(shouldStartBackend({ ENABLE_BACKEND: "true" }), true);
assert.equal(shouldStartBackend({ BACKEND_CMD: "custom-backend" }), true);
assert.equal(shouldStartBackend({ ENABLE_BACKEND: "1", SKIP_BACKEND: "1" }), false);
});
2026-06-26 20:01:02 +08:00
2026-07-13 09:00:19 +08:00
test("默认跳过 opencode,只有显式开启时才启动", () => {
assert.equal(shouldStartOpencode({}), false);
assert.equal(shouldStartOpencode({ ENABLE_OPENCODE: "1" }), true);
assert.equal(shouldStartOpencode({ ENABLE_OPENCODE: "true" }), true);
assert.equal(shouldStartOpencode({ OPENCODE_CMD: "custom-opencode" }), true);
assert.equal(shouldStartOpencode({ ENABLE_OPENCODE: "1", SKIP_OPENCODE: "1" }), false);
});
2026-07-12 14:18:26 +08:00
test("opencode 默认命令使用 MNote 专用运行目录", () => {
2026-07-03 23:20:16 +08:00
const command = buildDefaultOpencodeCommand(18085);
2026-07-12 14:18:26 +08:00
assert.match(command, /\/mnt\/Data1T\/Mnote_data\/opencode\/runtime/);
assert.match(command, /\/mnt\/Data1T\/Mnote_data\/opencode\/home/);
2026-07-03 23:20:16 +08:00
assert.match(command, /HOME=/);
assert.match(command, /XDG_CONFIG_HOME=/);
assert.doesNotMatch(command, /已检测到现有 opencode/);
assert.match(command, /opencode serve --hostname=127\.0\.0\.1 --port 18085 --print-logs/);
});
test("isHttpHealthy 只把 2xx HTTP health 视为可复用服务", async () => {
const server = net.createServer((socket) => {
socket.once("data", () => {
socket.end("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK");
});
});
await new Promise((resolve, reject) => {
server.listen(0, "127.0.0.1", resolve);
server.once("error", reject);
});
const { port } = server.address();
try {
assert.equal(await isHttpHealthy(`http://127.0.0.1:${port}/health`), true);
} finally {
server.close();
}
2026-06-26 20:01:02 +08:00
});