Files
mnote/scripts/task117-next-retirement-guard.js
T

209 lines
6.7 KiB
JavaScript
Raw Normal View History

2026-04-29 12:24:44 +08:00
#!/usr/bin/env node
"use strict";
const assert = require("node:assert");
const { spawn } = require("node:child_process");
const {
fetchWithTimeout,
findFreePort,
waitForGateway,
} = require("./task114-rust-web-gateway-entry-smoke.js");
const { resolveRuntimePlan } = require("./desktop-hot.js");
function buildRetirementFixtureEnv(port) {
return {
SKIP_NEXT_LEGACY: "1",
MNOTE_WEB_ALLOW_DEV_FIXTURES: "1",
MNOTE_WEB_BIND: `127.0.0.1:${port}`,
MNOTE_WEB_PUBLIC_BIND: "127.0.0.1:3000",
MNOTE_WEB_ENABLE_LEGACY_NEXT_COMPAT: "0",
MNOTE_WEB_QUERY_FIXTURES_JSON: JSON.stringify({
"documents:getMeta": {
id: "doc_1",
workspace_id: "ws_demo",
title: "服务端页面",
updated_at: "2026-04-28T00:00:00Z",
can_edit: true,
word_count: 42,
character_count: 128,
block_count: 3,
},
"documents:getContent": {
content: [{ id: "block_1", type: "paragraph", content: [] }],
revision: 7,
conflict_detection_key: "doc_1:7",
pageSubtree: { rootNodeId: "doc_1", outline: [] },
},
"sidebar:datasetList": {
active_workspace_id: "ws_demo",
workspaces: [],
documents: [
{
id: "doc_1",
workspace_id: "ws_demo",
title: "服务端页面",
parent_id: null,
sort_order: 0,
is_starred: false,
is_template: false,
created_at: "2026-04-28T00:00:00Z",
updated_at: "2026-04-28T00:00:00Z",
},
],
trashed_documents: [],
media_assets: [],
trashed_media_assets: [],
mindmap_assets: [],
trashed_mindmap_assets: [],
table_assets: [],
trashed_table_assets: [],
mindmap_docs: [],
mindmap_asset_children: {},
},
"bridgeLogs:listWorkspaceOverview": {
workspace_id: "ws_demo",
command_logs: [],
domain_events: [],
next_cursor: null,
has_more: false,
filters: {
command_status: null,
event_status: null,
target_page_id: null,
target_block_id: null,
aggregate_type: null,
aggregate_id: null,
},
generated_at: "2026-04-28T00:00:00Z",
},
}),
};
}
function startRetiredNextGateway(port) {
return spawn("cargo", ["run", "-q", "-p", "mnote-web", "--bin", "mnote-web"], {
cwd: "/mnt/Data1T/mnote/rust",
env: {
...process.env,
...buildRetirementFixtureEnv(port),
},
stdio: ["ignore", "pipe", "pipe"],
});
}
async function readText(baseUrl, path, init) {
const response = await fetchWithTimeout(`${baseUrl}${path}`, init);
const text = await response.text();
assert.equal(response.status, 200, `${path} 请求失败: ${response.status} ${text.slice(0, 200)}`);
assert.equal(response.headers.get("x-mnote-web-owner"), "mnote-web", `${path} 缺少 mnote-web owner`);
assert.equal(
response.headers.get("x-mnote-legacy-upstream"),
null,
`${path} 默认主路径不应出现 Next fallback header`,
);
2026-04-29 12:24:44 +08:00
return { response, text };
}
async function validateSkipNextRuntimePlan() {
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.publicPort, 3000);
assert.equal(plan.legacyPort, 3100);
assert.equal(plan.mnoteWebEnv.MNOTE_WEB_ENABLE_LEGACY_NEXT_COMPAT, "0");
assert.equal("MNOTE_WEB_LEGACY_NEXT_BASE_URL" in plan.mnoteWebEnv, false);
return plan;
}
async function validateCoreShellsWithoutNext(baseUrl) {
const root = await readText(baseUrl, "/?workspaceId=ws_demo");
assert.match(root.text, /data-mnote-shell="workspace"/);
assert.doesNotMatch(root.text, /next-app-router/i);
2026-04-29 12:24:44 +08:00
const auth = await readText(baseUrl, "/auth");
assert.match(auth.text, /data-mnote-shell="auth"/);
assert.doesNotMatch(auth.text, /next-app-router/i);
const document = await readText(baseUrl, "/documents/doc_1?workspaceId=ws_demo");
assert.equal(document.response.headers.get("x-mnote-web-shell"), "document");
assert.match(document.text, /mnote\.page_aggregate\.v1/);
const treeEvents = await fetchWithTimeout(`${baseUrl}/api/tree/events?workspaceId=ws_demo&maxPolls=0`);
const treeBody = await treeEvents.text();
assert.equal(treeEvents.status, 200, `/api/tree/events 请求失败: ${treeEvents.status} ${treeBody.slice(0, 200)}`);
assert.equal(treeEvents.headers.get("x-mnote-web-owner"), "mnote-web");
assert.equal(treeEvents.headers.get("x-mnote-tree-stream-owner"), "rust-web");
assert.match(treeBody, /event:\s*snapshot/);
const search = await readText(baseUrl, "/search?workspaceId=ws_demo&q=Rust");
assert.equal(search.response.headers.get("x-mnote-web-shell"), "search");
assert.match(search.text, /mnote\.search_shell\.v1/);
const mindmap = await readText(baseUrl, "/mindmap/doc_1/mind_1");
assert.equal(mindmap.response.headers.get("x-mnote-web-shell"), "mindmap");
assert.match(mindmap.text, /mnote\.mindmap_shell\.v1/);
assert.doesNotMatch(mindmap.text, /next-app-router/i);
2026-04-29 12:24:44 +08:00
}
async function validateDocs() {
const fs = require("node:fs");
const architecture = fs.readFileSync("/mnt/Data1T/mnote/ARCHITECTURE.md", "utf8");
assert.match(architecture, /Next App Router.*legacy compat/is, "ARCHITECTURE.md 未标记 Next App Router legacy compat");
assert.doesNotMatch(architecture, /Next App Router 仍是当前 legacy 主壳/, "ARCHITECTURE.md 仍保留旧主壳口径");
}
async function main() {
const plan = await validateSkipNextRuntimePlan();
const port = await findFreePort();
const baseUrl = `http://127.0.0.1:${port}`;
const gateway = startRetiredNextGateway(port);
let stderr = "";
gateway.stderr.on("data", (chunk) => {
stderr += chunk.toString("utf8");
});
try {
await waitForGateway(baseUrl);
await validateCoreShellsWithoutNext(baseUrl);
await validateDocs();
console.log(
JSON.stringify(
{
ok: true,
baseUrl,
publicEntry: String(plan.publicPort),
legacyPort: String(plan.legacyPort),
nextLegacyStarted: false,
owner: "mnote-web",
},
null,
2,
),
);
} catch (error) {
if (stderr.trim()) {
console.error(stderr.trim().slice(-2000));
}
throw error;
} finally {
if (gateway.pid) {
gateway.kill("SIGTERM");
setTimeout(() => {
if (!gateway.killed) {
gateway.kill("SIGKILL");
}
}, 2000).unref();
}
}
}
main().catch((error) => {
console.error(error instanceof Error ? error.stack || error.message : String(error));
process.exit(1);
});