#!/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`, ); 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); 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); } 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); });