feat: cut over rust web main shell
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
#!/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`);
|
||||
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 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/);
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user