Files
mnote/scripts/task514-sidebar-dev-hot-reload-gating-smoke.js
T
lix-2026 1882db7681 收口 MNote P0 P1 P2 审查尾项
- 归档 OnlyOffice live bridge、Page AI、mindmap、design governance 与相关 bug 条目
- 补齐 MinerU OCR 后端 runtime 合同与 smoke/test 基线
- 收口 ChatOnly/Doubao、ObjectIdentity、Page Aggregate compat 与 runtime owner 文档口径

验证:
- cargo test --manifest-path rust/Cargo.toml -p mnote-web local_ocr -- --test-threads=1
- cargo test --manifest-path rust/Cargo.toml -p mnote-web onlyoffice_bridge -- --test-threads=1
- git diff --check
- git diff --cached --check
- codegraph index . --force && codegraph status .
- codegraph sync . && codegraph status .
2026-06-01 09:29:12 +08:00

142 lines
4.8 KiB
JavaScript

#!/usr/bin/env node
"use strict";
const assert = require("node:assert");
const { spawn } = require("node:child_process");
const net = require("node:net");
const { chromium } = require("playwright");
const UI_TIMEOUT_MS = Number(process.env.MNOTE_UI_TIMEOUT_MS || 45000);
function findFreePort() {
return new Promise((resolve, reject) => {
const server = net.createServer();
server.listen(0, "127.0.0.1", () => {
const port = server.address().port;
server.close(() => resolve(port));
});
server.on("error", reject);
});
}
async function waitForGateway(baseUrl, timeoutMs = 120000) {
const deadline = Date.now() + timeoutMs;
let lastError = "";
while (Date.now() < deadline) {
try {
const response = await fetch(`${baseUrl}/`, {
headers: {
"x-mnote-actor-id": "mnote-e2e",
"x-mnote-actor-type": "user",
},
});
if (response.status === 200 || response.status === 303) return;
lastError = `${response.status} ${await response.text().catch(() => "")}`;
} catch (error) {
lastError = error && error.message ? error.message : String(error);
}
await new Promise((resolve) => setTimeout(resolve, 500));
}
throw new Error(`mnote-web 未就绪: ${lastError}`);
}
function startGateway(port, devHot) {
const env = {
...process.env,
MNOTE_WEB_BIND: `127.0.0.1:${port}`,
MNOTE_WEB_PUBLIC_BIND: `127.0.0.1:${port}`,
MNOTE_WEB_ALLOW_DEV_FIXTURES: "1",
MNOTE_WEB_LEGACY_NEXT_BASE_URL: "http://127.0.0.1:3100",
MNOTE_WEB_ENABLE_LEGACY_NEXT_COMPAT: "1",
};
delete env.MNOTE_WEB_DEV_HOT_RELOAD;
if (devHot) env.MNOTE_WEB_DEV_HOT_RELOAD = "1";
const child = spawn("cargo", ["run", "-q", "-p", "mnote-web", "--bin", "mnote-web"], {
cwd: "/mnt/Data1T/mnote/rust",
env,
stdio: ["ignore", "pipe", "pipe"],
});
let stderr = "";
child.stderr.on("data", (chunk) => {
stderr += chunk.toString("utf8");
});
child.stdout.on("data", () => {});
return {
child,
stderr: () => stderr,
};
}
async function stopGateway(gateway) {
if (!gateway || gateway.child.killed) return;
gateway.child.kill("SIGTERM");
await new Promise((resolve) => setTimeout(resolve, 500));
if (!gateway.child.killed) gateway.child.kill("SIGKILL");
}
async function verifyCase(devHot) {
const port = await findFreePort();
const baseUrl = `http://127.0.0.1:${port}`;
const gateway = startGateway(port, devHot);
try {
await waitForGateway(baseUrl);
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
extraHTTPHeaders: {
"x-mnote-actor-id": "mnote-e2e",
"x-mnote-actor-type": "user",
},
});
const page = await context.newPage();
const hotReloadRequests = [];
const networkFailures = [];
const consoleErrors = [];
page.on("request", (request) => {
if (request.url().includes("/api/dev/hot-reload")) hotReloadRequests.push(request.url());
});
page.on("requestfailed", (request) => {
networkFailures.push({ url: request.url(), failure: request.failure()?.errorText || "" });
});
page.on("console", (message) => {
if (message.type() === "error") consoleErrors.push(message.text());
});
await page.goto(`${baseUrl}/`, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
await page.locator('body[data-mnote-shell="workspace"]').waitFor({ state: "attached", timeout: UI_TIMEOUT_MS });
await page.waitForTimeout(1800);
const html = await page.content();
const shell = await page.locator("body").getAttribute("data-mnote-shell");
const devHotAttr = await page.locator("html").getAttribute("data-mnote-dev-hot-reload").catch(() => null);
await browser.close();
return {
devHot,
baseUrl,
shell,
scriptHasDevHot: html.includes("sidebar-tree-runtime.js?devHot="),
hotReloadRequestCount: hotReloadRequests.length,
hotReloadAttr: devHotAttr || "",
networkFailures,
consoleErrors,
};
} finally {
await stopGateway(gateway);
}
}
async function main() {
const normal = await verifyCase(false);
const hot = await verifyCase(true);
assert.equal(normal.shell, "workspace", JSON.stringify(normal));
assert.equal(normal.scriptHasDevHot, false, JSON.stringify(normal));
assert.equal(normal.hotReloadRequestCount, 0, JSON.stringify(normal));
assert.equal(hot.shell, "workspace", JSON.stringify(hot));
assert.equal(hot.scriptHasDevHot, true, JSON.stringify(hot));
assert(hot.hotReloadRequestCount >= 1, JSON.stringify(hot));
assert.equal(hot.hotReloadAttr, "enabled", JSON.stringify(hot));
console.log(JSON.stringify({ ok: true, task: "task514-sidebar-dev-hot-reload-gating-smoke", normal, hot }, null, 2));
}
main().catch((error) => {
console.error(error && error.stack ? error.stack : error);
process.exit(1);
});