179 lines
5.0 KiB
JavaScript
179 lines
5.0 KiB
JavaScript
const assert = require("node:assert");
|
||
const { spawn } = require("node:child_process");
|
||
const net = require("node:net");
|
||
const { test } = require("node:test");
|
||
const {
|
||
resolveBackendExecutable,
|
||
ensurePortFree,
|
||
isPortFree,
|
||
resolveRuntimePlan,
|
||
} = require("./desktop-hot.js");
|
||
|
||
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);
|
||
});
|
||
|
||
test("默认热启动计划使用 mnote-web 作为 3000 owner,Next 仅作为 legacy upstream", () => {
|
||
const plan = resolveRuntimePlan({
|
||
FRONTEND_PORT: "3000",
|
||
NEXT_LEGACY_PORT: "3100",
|
||
});
|
||
|
||
assert.equal(plan.skipGateway, false);
|
||
assert.equal(plan.publicPort, 3000);
|
||
assert.equal(plan.legacyPort, 3100);
|
||
assert.equal(plan.frontendTaskName, "next-legacy");
|
||
assert.equal(plan.frontendCommand, "pnpm dev -p 3100");
|
||
assert.equal(plan.mnoteWebCommand, "cargo run -p mnote-web --bin mnote-web");
|
||
assert.deepEqual(plan.mnoteWebEnv, {
|
||
MNOTE_WEB_BIND: "127.0.0.1:3000",
|
||
MNOTE_WEB_PUBLIC_BIND: "127.0.0.1:3000",
|
||
MNOTE_WEB_LEGACY_NEXT_BASE_URL: "http://127.0.0.1:3100",
|
||
MNOTE_WEB_ENABLE_LEGACY_NEXT_COMPAT: "1",
|
||
});
|
||
});
|
||
|
||
test("默认任务数组顺序变化时后端命令不应覆盖 mnote-web gateway", () => {
|
||
const plan = resolveRuntimePlan({
|
||
FRONTEND_PORT: "3000",
|
||
NEXT_LEGACY_PORT: "3100",
|
||
BACKEND_CMD: "/custom/backend",
|
||
});
|
||
|
||
assert.equal(plan.mnoteWebCommand, "cargo run -p mnote-web --bin mnote-web");
|
||
assert.equal(plan.frontendCommand, "pnpm dev -p 3100");
|
||
});
|
||
|
||
test("MNOTE_WEB_SKIP_GATEWAY 可临时恢复旧 Next 3000 入口", () => {
|
||
const plan = resolveRuntimePlan({
|
||
FRONTEND_PORT: "3000",
|
||
NEXT_LEGACY_PORT: "3100",
|
||
MNOTE_WEB_SKIP_GATEWAY: "1",
|
||
});
|
||
|
||
assert.equal(plan.skipGateway, true);
|
||
assert.equal(plan.frontendTaskName, "frontend");
|
||
assert.equal(plan.frontendCommand, "pnpm dev -p 3000");
|
||
assert.deepEqual(plan.mnoteWebEnv, {});
|
||
});
|
||
|
||
|
||
test("SKIP_NEXT_LEGACY 保持 Rust gateway 为 3000 owner,但不启动 Next legacy upstream", () => {
|
||
const plan = resolveRuntimePlan({
|
||
FRONTEND_PORT: "3000",
|
||
NEXT_LEGACY_PORT: "3100",
|
||
SKIP_NEXT_LEGACY: "1",
|
||
});
|
||
|
||
assert.equal(plan.skipGateway, false);
|
||
assert.equal(plan.skipNextLegacy, true);
|
||
assert.equal(plan.frontendTaskName, null);
|
||
assert.equal(plan.mnoteWebCommand, "cargo run -p mnote-web --bin mnote-web");
|
||
assert.deepEqual(plan.mnoteWebEnv, {
|
||
MNOTE_WEB_BIND: "127.0.0.1:3000",
|
||
MNOTE_WEB_PUBLIC_BIND: "127.0.0.1:3000",
|
||
MNOTE_WEB_ENABLE_LEGACY_NEXT_COMPAT: "0",
|
||
});
|
||
});
|