220 lines
7.0 KiB
JavaScript
220 lines
7.0 KiB
JavaScript
#!/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"],
|
|
});
|
|
}
|
|
|
|
function authedInit(init = {}) {
|
|
return {
|
|
...init,
|
|
headers: {
|
|
...(init.headers || {}),
|
|
"x-mnote-actor-id": "user_real",
|
|
"x-mnote-actor-type": "user",
|
|
},
|
|
};
|
|
}
|
|
|
|
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`,
|
|
);
|
|
return { response, text };
|
|
}
|
|
|
|
async function validateSkipNextRuntimePlan() {
|
|
const plan = resolveRuntimePlan({
|
|
FRONTEND_PORT: "3000",
|
|
NEXT_LEGACY_PORT: "3100",
|
|
MNOTE_WEB_ENABLE_LEGACY_NEXT_COMPAT: "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", authedInit());
|
|
assert.match(root.text, /data-mnote-shell="workspace"/);
|
|
assert.doesNotMatch(root.text, /next-app-router/i);
|
|
|
|
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", authedInit());
|
|
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`, authedInit());
|
|
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", authedInit());
|
|
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", authedInit());
|
|
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);
|
|
}
|
|
|
|
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);
|
|
});
|