Files
mnote/scripts/task169-mindmap-realtime-smoke.js
T
lix-2026 384da4e44c feat(tree): checkpoint resource lifecycle work
提交当前顶层 mnote Git 工作区,范围集中在 04-tree-domain 的 resource/trash/filetree 生命周期、mnote-web resource_trash 路由、Convex/Next 兼容接口、sidebar/file-tree 客户端适配、smoke 脚本与对应设计/bug 记录。

不包含被 ignore 的 design/05-editor-mainline/reference-code/leptos-tiptap 嵌套仓库改动。新增 smoke 的测试密码改为运行时读取 MNOTE_E2E_PASSWORD,避免提交明文 credential assignment。
2026-05-16 07:38:45 +08:00

1364 lines
53 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
"use strict";
const fs = require("node:fs/promises");
const path = require("node:path");
const { chromium } = require("playwright");
const {
BASE_URL,
UI_TIMEOUT_MS,
assert,
cleanupDocuments,
createTempDocument,
ensureAuthenticated,
openDocument,
openFilesystemView,
renameDocument,
} = require("./tree-shell-smoke-helpers");
const TASK = "task169-mindmap-realtime-smoke";
const OUTPUT_DIR = path.join(process.cwd(), "tmp", TASK);
const RESULT_PATH = path.join(OUTPUT_DIR, "result.json");
const LIVE_WAIT_MS = Number(process.env.MNOTE_MINDMAP_REALTIME_WAIT_MS || 12_000);
const BAD_TEXT_PATTERN = /ArgumentValidationError|domainEventHint|domainEventPlan|streamDeltaHint|command_failed|502 Bad Gateway/i;
async function writeResult(payload) {
await fs.mkdir(OUTPUT_DIR, { recursive: true });
await fs.writeFile(RESULT_PATH, `${JSON.stringify({ ...payload, resultPath: RESULT_PATH }, null, 2)}\n`, "utf8");
}
async function screenshot(page, name) {
const file = path.join(OUTPUT_DIR, `${name}.png`);
await page.screenshot({ path: file, fullPage: true });
return file;
}
async function installTreeEventRecorder(page) {
await page.addInitScript(() => {
window.__MNOTE_SMOKE_TREE_EVENTS__ = [];
const record = (name, event) => {
const detail = event && event.detail ? event.detail : null;
window.__MNOTE_SMOKE_TREE_EVENTS__.push({
name,
at: Date.now(),
revision: detail && detail.revision ? String(detail.revision) : "",
data: detail && detail.payload && detail.payload.data ? detail.payload.data : null,
eventType:
detail &&
detail.payload &&
detail.payload.overview &&
Array.isArray(detail.payload.overview.domain_events) &&
detail.payload.overview.domain_events[0]
? detail.payload.overview.domain_events[0].event_type || ""
: "",
});
};
window.addEventListener("tree:snapshot", (event) => record("tree:snapshot", event));
window.addEventListener("tree:delta", (event) => record("tree:delta", event));
window.addEventListener("tree:resync", (event) => record("tree:resync", event));
});
}
async function installMindmapLoadingRecorder(page) {
await page.addInitScript(() => {
window.__MNOTE_MINDMAP_LOADING_EVENTS__ = [];
const recordLoading = (node, reason) => {
if (!(node instanceof HTMLElement)) return;
const loading = node.matches?.('[data-testid="mindmap-scene-loading"]')
? node
: node.querySelector?.('[data-testid="mindmap-scene-loading"]');
if (!(loading instanceof HTMLElement)) return;
window.__MNOTE_MINDMAP_LOADING_EVENTS__.push({
at: Date.now(),
reason,
text: loading.textContent || "",
url: window.location.href,
});
};
const start = () => {
if (window.__MNOTE_MINDMAP_LOADING_OBSERVER__) return;
const target = document.documentElement || document.body;
if (!target) {
window.setTimeout(start, 0);
return;
}
recordLoading(target, "initial-scan");
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => recordLoading(node, "added-node"));
});
});
observer.observe(target, { childList: true, subtree: true });
window.__MNOTE_MINDMAP_LOADING_OBSERVER__ = observer;
};
start();
});
}
function attachNetworkCapture(page, label, records) {
page.on("request", (request) => {
const url = request.url();
if (url.includes("/api/tree/events")) {
records.push({ type: "request", label, method: request.method(), url });
}
});
page.on("response", async (response) => {
const url = response.url();
if (
!url.includes("/api/tree/events") &&
!url.includes("/api/mindmap/") &&
!url.includes("/mindmap/") &&
!url.includes("/api/documents/save")
) {
return;
}
const request = response.request();
const method = request.method();
const status = response.status();
let responseText = null;
if (method !== "GET" || status >= 400 || url.includes("/api/documents/save")) {
responseText = await response.text().catch((error) => `<<read_response_failed:${error.message}>>`);
}
records.push({
type: "response",
label,
method,
url,
status,
requestBody: request.postData() || null,
responseText: responseText ? responseText.slice(0, 12000) : null,
});
});
page.on("requestfailed", (request) => {
const url = request.url();
if (url.includes("/api/tree/events") || url.includes("/api/mindmap/") || url.includes("/api/documents/save")) {
records.push({
type: "requestfailed",
label,
method: request.method(),
url,
failure: request.failure()?.errorText || null,
requestBody: request.postData() || null,
});
}
});
page.on("console", (message) => {
const text = message.text();
if (message.type() === "error" || /mindmap|tree live|EventSource|502|command_failed/i.test(text)) {
records.push({
type: "console",
label,
level: message.type(),
text: text.slice(0, 2000),
});
}
});
}
async function insertMindmapThroughSlash(page) {
const editor = page
.locator('[data-testid="mnote-leptos-tiptap-island-editor-root"] .editor-surface .ProseMirror[contenteditable="true"]')
.first();
await editor.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await editor.click({ timeout: UI_TIMEOUT_MS });
await page.keyboard.type("/");
const item = page.getByTestId("slash-item-mindmap").first();
await item.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await item.click({ timeout: UI_TIMEOUT_MS });
}
async function readPageState(page) {
return page.evaluate(() => {
const html = document.documentElement;
const root = document.querySelector('[data-testid="mnote-mindmap-editor-root"]');
const runtime = document.querySelector('[data-testid="simple-mind-map-runtime"]');
const scene = document.querySelector('[data-testid="leptos-mindmap-island"]');
const error = document.querySelector('[data-testid="leptos-mindmap-error"]');
const loading = document.querySelector('[data-testid="mindmap-scene-loading"]');
const assetRows = Array.from(document.querySelectorAll("#sidebar-file-tree-root [data-asset-id]"))
.map((node) => (node instanceof HTMLElement ? node.getAttribute("data-asset-id") || "" : ""))
.filter(Boolean);
const fileTreeAssetRowElements = Array.from(document.querySelectorAll("#sidebar-file-tree-root [data-asset-id]"))
.map((node) => (node instanceof HTMLElement ? node.closest(".tree-row") || node : null))
.filter((node, index, rows) => node instanceof HTMLElement && rows.indexOf(node) === index);
const mindmapRows = fileTreeAssetRowElements
.filter((node) => {
if (!(node instanceof HTMLElement)) return false;
const objectKind = node.getAttribute("data-object-kind") || "";
const objectIdentity = node.getAttribute("data-object-identity") || "";
const assetId = node.getAttribute("data-asset-id") || "";
return objectKind === "mindmap" || objectIdentity.includes('"objectKind":"mindmap"') || assetId.startsWith("mindmap");
})
.map((node) => {
const element = node;
return {
rowId: element.getAttribute("data-row-id") || "",
documentId: element.getAttribute("data-document-id") || element.getAttribute("data-doc-id") || "",
assetId: element.getAttribute("data-asset-id") || "",
objectKind: element.getAttribute("data-object-kind") || "",
objectIdentity: element.getAttribute("data-object-identity") || "",
title: element.textContent || "",
};
});
return {
url: window.location.href,
bodyText: (document.body?.innerText || "").slice(0, 8000),
mindmapId: root instanceof HTMLElement ? root.dataset.mnoteMindmapId || null : null,
runtimeReady: runtime instanceof HTMLElement ? runtime.dataset.runtimeReady === "true" : false,
runtimeMountCount: scene instanceof HTMLElement ? Number(scene.dataset.runtimeMountCount || "0") : 0,
runtimeProjectionApplyCount:
scene instanceof HTMLElement ? Number(scene.dataset.runtimeProjectionApplyCount || "0") : 0,
lastRuntimeMountReason: scene instanceof HTMLElement ? scene.dataset.lastRuntimeMountReason || "" : "",
lastFetchSceneReason: scene instanceof HTMLElement ? scene.dataset.lastFetchSceneReason || "" : "",
backgroundProjectionRefresh: scene instanceof HTMLElement ? scene.dataset.backgroundProjectionRefresh || "" : "",
hasMindmapError: Boolean(error),
hasMindmapLoading: Boolean(loading),
commandStatus: scene instanceof HTMLElement ? scene.dataset.commandStatus || "" : "",
treeLiveStatus: html.getAttribute("data-mnote-tree-live-status") || "",
treeLiveApplied: html.getAttribute("data-mnote-tree-live-applied") || "",
treeLiveRevision: html.getAttribute("data-mnote-tree-live-revision") || "",
treeLiveApplyError: html.getAttribute("data-mnote-tree-live-apply-error") || "",
lastMindmapAssetOpenMode: html.getAttribute("data-mnote-last-mindmap-asset-open-mode") || "",
lastMindmapAssetId: html.getAttribute("data-mnote-last-mindmap-asset-id") || "",
objectEditor:
document.querySelector("[data-mnote-object-editor]") instanceof HTMLElement
? document.querySelector("[data-mnote-object-editor]").getAttribute("data-mnote-object-editor") || ""
: "",
objectIdentity:
document.querySelector("[data-mnote-object-identity]") instanceof HTMLElement
? document.querySelector("[data-mnote-object-identity]").getAttribute("data-mnote-object-identity") || ""
: "",
localStorageKeys: Object.keys(window.localStorage || {}).filter((key) =>
/mnote\.leptos-tiptap-spike\.document|__mindmap_object__|mindmap-object/.test(key),
),
mindmapLoadingEvents: window.__MNOTE_MINDMAP_LOADING_EVENTS__ || [],
fileTreeText: (document.getElementById("sidebar-file-tree-root")?.textContent || "").slice(0, 4000),
fileTreeAssetIds: assetRows,
fileTreeMindmapRows: mindmapRows,
treeEvents: window.__MNOTE_SMOKE_TREE_EVENTS__ || [],
};
});
}
async function readMindmapRuntimeStabilityState(page, mindmapId) {
return page.evaluate(
({ mindmapId }) => {
const scene = document.querySelector('[data-testid="leptos-mindmap-island"]');
const registry = window.__MNOTE_LEPTOS_MINDMAP_BRIDGES__ || {};
const bridge = registry[mindmapId];
const instance = bridge?.instance;
const topicNode = instance?.renderer?.findNodeByUid?.("topic") || null;
const topicRect = typeof topicNode?.getRect === "function" ? topicNode.getRect() : null;
const viewTransform = instance?.view?.getTransformData?.() || null;
return {
runtimeMountCount: scene instanceof HTMLElement ? Number(scene.dataset.runtimeMountCount || "0") : 0,
runtimeProjectionApplyCount:
scene instanceof HTMLElement ? Number(scene.dataset.runtimeProjectionApplyCount || "0") : 0,
lastRuntimeMountReason: scene instanceof HTMLElement ? scene.dataset.lastRuntimeMountReason || "" : "",
lastRuntimeProjectionApplyReason:
scene instanceof HTMLElement ? scene.dataset.lastRuntimeProjectionApplyReason || "" : "",
runtimeProjectionDeferred:
scene instanceof HTMLElement ? scene.dataset.runtimeProjectionDeferred || "" : "",
lastRuntimeProjectionDeferReason:
scene instanceof HTMLElement ? scene.dataset.lastRuntimeProjectionDeferReason || "" : "",
backgroundProjectionRefresh: scene instanceof HTMLElement ? scene.dataset.backgroundProjectionRefresh || "" : "",
commandStatus: scene instanceof HTMLElement ? scene.dataset.commandStatus || "" : "",
commandApplyMode: scene instanceof HTMLElement ? scene.dataset.commandApplyMode || "" : "",
topicRect: topicRect
? {
x: Number(topicRect.x ?? topicRect.left ?? 0),
y: Number(topicRect.y ?? topicRect.top ?? 0),
width: Number(topicRect.width ?? 0),
height: Number(topicRect.height ?? 0),
}
: null,
viewTransform,
};
},
{ mindmapId },
);
}
function stableJson(value) {
return JSON.stringify(value ?? null);
}
async function waitForMindmapRuntimeViewSettled(page, mindmapId, label) {
let previous = await readMindmapRuntimeStabilityState(page, mindmapId);
for (let index = 0; index < 12; index += 1) {
await page.waitForTimeout(350);
const current = await readMindmapRuntimeStabilityState(page, mindmapId);
if (
stableJson(current.viewTransform) === stableJson(previous.viewTransform) &&
stableJson(current.topicRect) === stableJson(previous.topicRect) &&
current.runtimeMountCount === previous.runtimeMountCount &&
current.runtimeProjectionApplyCount === previous.runtimeProjectionApplyCount
) {
return current;
}
previous = current;
}
throw new Error(`mindmap_runtime_view_not_settled:${label}`);
}
function rectCenter(rect) {
if (!rect) return null;
return {
x: rect.x + rect.width / 2,
y: rect.y + rect.height / 2,
};
}
function centerDistance(a, b) {
const ca = rectCenter(a);
const cb = rectCenter(b);
if (!ca || !cb) return 0;
return Math.hypot(ca.x - cb.x, ca.y - cb.y);
}
async function dispatchSyntheticMindmapTreeSignal(page, documentId, mindmapId, eventName) {
return page.evaluate(
({ documentId, mindmapId, eventName }) => {
const cursorId = `smoke-command-${Date.now()}`;
const payload =
eventName === "tree:delta"
? {
kind: "delta",
revision: JSON.stringify({ id: cursorId, createdAt: new Date().toISOString() }),
data: {
op: "resync_required",
reason: "mindmap.put",
documentId,
blockId: mindmapId,
},
}
: {
kind: "resync",
revision: JSON.stringify({ id: cursorId, createdAt: new Date().toISOString() }),
overview: {
command_logs: [
{
id: cursorId,
command_id: cursorId,
target_page_id: documentId,
target_block_id: mindmapId,
aggregate_type: "block",
aggregate_id: mindmapId,
payload: {
streamDelta: {
op: "resync_required",
reason: "mindmap.put",
documentId,
blockId: mindmapId,
},
},
},
],
domain_events: [],
},
};
window.dispatchEvent(new CustomEvent(eventName, { detail: { payload, revision: payload.revision } }));
return payload;
},
{ documentId, mindmapId, eventName },
);
}
async function assertMindmapRuntimeDoesNotRefreshForLiveSignals(page, documentId, mindmapId, failures, label) {
await waitForMindmapRuntimeViewSettled(page, mindmapId, label);
const before = await readPageState(page);
const beforeRuntime = await readMindmapRuntimeStabilityState(page, mindmapId);
if (before.hasMindmapError || before.hasMindmapLoading || !before.runtimeReady) {
failures.push({
code: "mindmap_unstable_before_live_signal",
label,
state: before,
});
return { before, afterResync: before, afterDelta: before };
}
const resyncPayload = await dispatchSyntheticMindmapTreeSignal(page, documentId, mindmapId, "tree:resync");
await page.waitForTimeout(2_500);
const afterResync = await readPageState(page);
const afterResyncRuntime = await readMindmapRuntimeStabilityState(page, mindmapId);
if (afterResync.runtimeMountCount !== before.runtimeMountCount) {
failures.push({
code: "mindmap_resync_remounted_runtime",
label,
before,
after: afterResync,
payload: resyncPayload,
});
}
if (afterResync.runtimeProjectionApplyCount !== before.runtimeProjectionApplyCount) {
failures.push({
code: "mindmap_resync_applied_projection_despite_usability_first",
label,
before,
after: afterResync,
payload: resyncPayload,
});
}
if (stableJson(afterResyncRuntime.viewTransform) !== stableJson(beforeRuntime.viewTransform)) {
failures.push({
code: "mindmap_resync_changed_view_transform",
label,
before: beforeRuntime,
after: afterResyncRuntime,
payload: resyncPayload,
});
}
if (centerDistance(beforeRuntime.topicRect, afterResyncRuntime.topicRect) > 2) {
failures.push({
code: "mindmap_resync_moved_topic_node",
label,
before: beforeRuntime,
after: afterResyncRuntime,
payload: resyncPayload,
});
}
const deltaPayload = await dispatchSyntheticMindmapTreeSignal(page, documentId, mindmapId, "tree:delta");
await page.waitForTimeout(2_500);
const afterDelta = await readPageState(page);
const afterDeltaRuntime = await readMindmapRuntimeStabilityState(page, mindmapId);
if (afterDelta.runtimeMountCount !== afterResync.runtimeMountCount) {
failures.push({
code: "mindmap_delta_remounted_runtime",
label,
before: afterResync,
after: afterDelta,
payload: deltaPayload,
});
}
if (afterDelta.runtimeProjectionApplyCount !== afterResync.runtimeProjectionApplyCount) {
failures.push({
code: "mindmap_delta_applied_projection_despite_usability_first",
label,
before: afterResync,
after: afterDelta,
payload: deltaPayload,
});
}
if (stableJson(afterDeltaRuntime.viewTransform) !== stableJson(afterResyncRuntime.viewTransform)) {
failures.push({
code: "mindmap_delta_changed_view_transform",
label,
before: afterResyncRuntime,
after: afterDeltaRuntime,
payload: deltaPayload,
});
}
if (centerDistance(afterResyncRuntime.topicRect, afterDeltaRuntime.topicRect) > 2) {
failures.push({
code: "mindmap_delta_moved_topic_node",
label,
before: afterResyncRuntime,
after: afterDeltaRuntime,
payload: deltaPayload,
});
}
if (afterDelta.hasMindmapError || afterDelta.hasMindmapLoading) {
failures.push({
code: "mindmap_live_signal_left_error_or_loading",
label,
after: afterDelta,
});
}
return { before, beforeRuntime, afterResync, afterResyncRuntime, afterDelta, afterDeltaRuntime };
}
async function waitForMindmapReady(page, stage) {
await page
.waitForFunction(
() => {
const root = document.querySelector('[data-testid="mnote-mindmap-editor-root"]');
const runtime = document.querySelector('[data-testid="simple-mind-map-runtime"]');
const mindmapId = root instanceof HTMLElement ? root.dataset.mnoteMindmapId || null : null;
const registry = window.__MNOTE_LEPTOS_MINDMAP_BRIDGES__ || {};
return (
root instanceof HTMLElement &&
runtime instanceof HTMLElement &&
runtime.dataset.runtimeReady === "true" &&
Boolean(mindmapId) &&
Boolean(registry[mindmapId])
);
},
null,
{ timeout: UI_TIMEOUT_MS },
)
.catch((error) => {
throw new Error(`${stage}:mindmap_not_ready:${error.message}`);
});
}
async function waitForMindmapId(page) {
await waitForMindmapReady(page, "mindmap-id");
const state = await readPageState(page);
assert(state.mindmapId, `mindmap_id_missing:${JSON.stringify(state)}`);
return state.mindmapId;
}
async function waitForDocumentContentToIncludeMindmap(requestContext, documentId, workspaceId, mindmapId) {
let lastText = "";
for (let index = 0; index < 45; index += 1) {
const response = await requestContext.fetch(
`${BASE_URL}/api/documents/content?documentId=${encodeURIComponent(documentId)}&workspaceId=${encodeURIComponent(workspaceId)}`,
{ method: "GET", timeout: 10_000 },
);
lastText = await response.text();
if (response.ok() && lastText.includes(mindmapId)) {
return lastText;
}
await new Promise((resolve) => setTimeout(resolve, 500));
}
throw new Error(`document_content_missing_mindmap:${lastText.slice(0, 3000)}`);
}
async function assertFileTreeMindmapOpenUsesObjectShell(page, documentId, mindmapId, failures) {
await openFilesystemView(page);
const opened = await page.evaluate(
({ documentId, mindmapId }) => {
const rows = Array.from(document.querySelectorAll("#sidebar-file-tree-root [data-asset-id]"));
const row = rows.find((node) => {
if (!(node instanceof HTMLElement)) return false;
const assetId = node.getAttribute("data-asset-id") || "";
const rowDocumentId = node.getAttribute("data-document-id") || node.getAttribute("data-doc-id") || "";
return assetId === mindmapId && rowDocumentId === documentId;
});
if (!(row instanceof HTMLElement)) {
return { clicked: false, reason: "mindmap_asset_row_missing" };
}
const button = row.querySelector('[data-rust-action="open"], .tree-link');
if (!(button instanceof HTMLElement)) {
return { clicked: false, reason: "mindmap_asset_open_button_missing" };
}
button.click();
return {
clicked: true,
assetId: row.getAttribute("data-asset-id") || "",
rowId: row.getAttribute("data-row-id") || "",
objectIdentity: row.getAttribute("data-object-identity") || "",
objectKind: row.getAttribute("data-object-kind") || "",
};
},
{ documentId, mindmapId },
);
if (!opened.clicked) {
failures.push({ code: "filetree_mindmap_asset_open_row_missing", opened });
return opened;
}
if (!opened.objectIdentity.includes(`"objectKind":"mindmap"`) || !opened.objectIdentity.includes(`"assetId":"${mindmapId}"`)) {
failures.push({
code: "filetree_mindmap_asset_row_missing_object_identity",
opened,
});
}
await page.waitForURL((url) => url.pathname.includes(`/mindmap/${encodeURIComponent(documentId)}/${encodeURIComponent(mindmapId)}`), {
timeout: UI_TIMEOUT_MS,
}).catch(() => null);
const state = await readPageState(page);
const url = new URL(state.url);
if (!url.pathname.includes(`/mindmap/${encodeURIComponent(documentId)}/${encodeURIComponent(mindmapId)}`)) {
failures.push({
code: "filetree_mindmap_asset_open_did_not_use_object_shell",
opened,
state,
});
}
if (url.pathname.includes(`/documents/${encodeURIComponent(documentId)}`)) {
failures.push({
code: "filetree_mindmap_asset_open_was_swallowed_by_index_document",
opened,
state,
});
}
await waitForMindmapReady(page, "filetree-object-shell-open");
const readyState = await readPageState(page);
if (readyState.mindmapId !== mindmapId || !readyState.runtimeReady || readyState.hasMindmapError) {
failures.push({
code: "filetree_mindmap_asset_object_shell_not_ready",
opened,
readyState,
});
}
if (readyState.objectEditor !== "mindmap" || readyState.objectIdentity !== `resource:mindmap:${documentId}:${mindmapId}`) {
failures.push({
code: "mindmap_object_shell_missing_identity_marker",
opened,
readyState,
});
}
return { opened, state: readyState };
}
async function assertSingleMindmapAssetRowStable(page, documentId, mindmapId, expectedObjectIdentity, failures, label) {
await openFilesystemView(page);
const state = await readPageState(page);
const rows = state.fileTreeMindmapRows.filter((row) => row.documentId === documentId);
if (rows.length !== 1) {
failures.push({
code: "filetree_mindmap_asset_row_count_changed",
label,
expectedCount: 1,
actualCount: rows.length,
rows,
state,
});
return { ok: false, rows, state, objectIdentity: expectedObjectIdentity };
}
const row = rows[0];
if (row.assetId !== mindmapId) {
failures.push({
code: "filetree_mindmap_asset_id_changed",
label,
expectedAssetId: mindmapId,
row,
state,
});
}
if (!row.objectIdentity.includes(`"objectKind":"mindmap"`) || !row.objectIdentity.includes(`"assetId":"${mindmapId}"`)) {
failures.push({
code: "filetree_mindmap_asset_identity_invalid",
label,
row,
state,
});
}
if (expectedObjectIdentity && row.objectIdentity !== expectedObjectIdentity) {
failures.push({
code: "filetree_mindmap_asset_identity_changed",
label,
expectedObjectIdentity,
row,
state,
});
}
return {
ok: failures.length === 0,
rows,
state,
objectIdentity: row.objectIdentity,
};
}
async function assertFileTreePageMarkdownOpenUsesPageAggregate(page, documentId, mindmapId, failures) {
await openFilesystemView(page);
const opened = await page.evaluate(
({ documentId }) => {
const row =
document.querySelector(`#sidebar-file-tree-root [data-row-id="doc:${CSS.escape(documentId)}"]`) ||
Array.from(document.querySelectorAll("#sidebar-file-tree-root [data-row-kind='document']")).find((node) => {
if (!(node instanceof HTMLElement)) return false;
const rowDocumentId = node.getAttribute("data-document-id") || node.getAttribute("data-doc-id") || "";
return rowDocumentId === documentId;
});
if (!(row instanceof HTMLElement)) {
return { clicked: false, reason: "page_markdown_row_missing" };
}
const button = row.querySelector('[data-rust-action="open"], .tree-link');
if (!(button instanceof HTMLElement)) {
return { clicked: false, reason: "page_markdown_open_button_missing" };
}
button.click();
return {
clicked: true,
rowId: row.getAttribute("data-row-id") || "",
objectIdentity: row.getAttribute("data-object-identity") || "",
};
},
{ documentId },
);
if (!opened.clicked) {
failures.push({ code: "filetree_page_markdown_open_row_missing", opened });
return opened;
}
if (!opened.objectIdentity.includes(`"objectKind":"page"`) || !opened.objectIdentity.includes(`"documentId":"${documentId}"`)) {
failures.push({
code: "filetree_page_markdown_row_missing_object_identity",
opened,
});
}
await page.waitForURL((url) => url.pathname.includes(`/documents/${encodeURIComponent(documentId)}`), {
timeout: UI_TIMEOUT_MS,
}).catch(() => null);
const state = await readPageState(page);
const url = new URL(state.url);
if (!url.pathname.includes(`/documents/${encodeURIComponent(documentId)}`)) {
failures.push({
code: "filetree_page_markdown_open_did_not_return_document_page",
opened,
state,
});
}
if (state.objectEditor === "mindmap" || state.objectIdentity.includes(`resource:mindmap:${documentId}:${mindmapId}`)) {
failures.push({
code: "filetree_page_markdown_open_loaded_mindmap_object_identity",
opened,
state,
});
}
return { opened, state };
}
function readMindmapBlocksFromDocumentContentText(text) {
const payload = JSON.parse(text);
const content = Array.isArray(payload?.content)
? payload.content
: Array.isArray(payload?.result?.content)
? payload.result.content
: Array.isArray(payload?.data?.content)
? payload.data.content
: Array.isArray(payload?.body?.content)
? payload.body.content
: [];
return content.filter((block) => block?.type === "mindmap" || block?.blockType === "mindmap");
}
async function assertDocumentMindmapBlockUsesReferenceSource(requestContext, documentId, workspaceId, mindmapId, failures, label) {
const text = await waitForDocumentContentToIncludeMindmap(requestContext, documentId, workspaceId, mindmapId);
const blocks = readMindmapBlocksFromDocumentContentText(text);
const target = blocks.find((block) => {
const props = block?.props && typeof block.props === "object" ? block.props : {};
return props.mindmapId === mindmapId || props.mindmap_id === mindmapId || block.id === mindmapId || block.blockId === mindmapId;
});
if (!target) {
failures.push({
code: "document_mindmap_block_reference_missing",
label,
mindmapId,
blocks,
responseText: text.slice(0, 3000),
});
return { ok: false, blocks };
}
const props = target.props && typeof target.props === "object" ? target.props : {};
if (!props.mindmapId || props.rootNodeId !== "root" || Object.prototype.hasOwnProperty.call(props, "data")) {
failures.push({
code: "document_mindmap_block_uses_wrong_source",
label,
mindmapId,
props,
target,
});
return { ok: false, target };
}
return { ok: true, target };
}
async function waitForMindmapProjectionText(requestContext, documentId, mindmapId, expectedText) {
let lastText = "";
for (let index = 0; index < 45; index += 1) {
const response = await requestContext.fetch(
`${BASE_URL}/api/mindmap/${encodeURIComponent(documentId)}/${encodeURIComponent(mindmapId)}?view=simple_mind_map_scene&queryName=mindmap.simple_mind_map_scene.get`,
{ method: "GET", timeout: 10_000 },
);
lastText = await response.text();
if (response.ok() && lastText.includes(expectedText)) return lastText;
await new Promise((resolve) => setTimeout(resolve, 500));
}
throw new Error(`mindmap_projection_missing_text:${expectedText}:${lastText.slice(0, 3000)}`);
}
async function readMindmapRuntimeTextState(page, mindmapId, expectedText) {
return page.evaluate(
({ mindmapId, expectedText }) => {
const registry = window.__MNOTE_LEPTOS_MINDMAP_BRIDGES__ || {};
const bridge = registry[mindmapId];
const snapshot = bridge?.getSnapshot?.() ?? bridge?.instance?.getData?.(true) ?? null;
const root = snapshot && typeof snapshot === "object" && "root" in snapshot ? snapshot.root : snapshot;
const texts = [];
const visit = (node) => {
if (!node || typeof node !== "object") return;
const data = node.data && typeof node.data === "object" ? node.data : {};
if (typeof data.text === "string") texts.push(data.text);
if (Array.isArray(node.children)) node.children.forEach(visit);
};
visit(root);
return {
texts,
includesExpectedText: texts.some((text) => text.includes(expectedText)),
snapshot,
};
},
{ mindmapId, expectedText },
);
}
async function readMindmapProjectionText(requestContext, documentId, mindmapId) {
const response = await requestContext.fetch(
`${BASE_URL}/api/mindmap/${encodeURIComponent(documentId)}/${encodeURIComponent(mindmapId)}?view=simple_mind_map_scene&queryName=mindmap.simple_mind_map_scene.get`,
{ method: "GET", timeout: 10_000 },
);
return {
ok: response.ok(),
status: response.status(),
text: await response.text(),
};
}
async function editTopicTextThroughRuntime(page, mindmapId, text) {
const result = await page.evaluate(
({ mindmapId, text }) => {
const scene = document.querySelector('[data-testid="leptos-mindmap-island"]');
if (scene instanceof HTMLElement) {
scene.dataset.commandStatus = "smoke-waiting-topic-edit";
}
const registry = window.__MNOTE_LEPTOS_MINDMAP_BRIDGES__ || {};
const bridge = registry[mindmapId];
const instance = bridge?.instance;
const topicNode = instance?.renderer?.findNodeByUid?.("topic") || null;
if (!topicNode || typeof topicNode !== "object") return { ok: false, reason: "topic_node_missing" };
if (typeof topicNode.setData === "function") {
topicNode.setData({ text });
} else if (topicNode.data && typeof topicNode.data === "object") {
topicNode.data.text = text;
} else {
return { ok: false, reason: "topic_node_not_mutable" };
}
const snapshot = typeof bridge.getSnapshot === "function" ? bridge.getSnapshot() : instance?.getData?.(true);
return {
ok: true,
snapshotText: snapshot?.children?.[0]?.data?.text ?? null,
commandStatus: scene instanceof HTMLElement ? scene.dataset.commandStatus || null : null,
};
},
{ mindmapId, text },
);
assert(result.ok, `topic_runtime_edit_failed:${JSON.stringify(result)}`);
await page.waitForFunction(
({ text }) => {
const bodyText = document.body?.innerText || "";
const scene = document.querySelector('[data-testid="leptos-mindmap-island"]');
return bodyText.includes(text) || (scene instanceof HTMLElement && scene.dataset.commandStatus !== "smoke-waiting-topic-edit");
},
{ text },
{ timeout: UI_TIMEOUT_MS },
);
return result;
}
async function readRuntimeTextEditState(page) {
return page.evaluate(() => {
const edit = document.querySelector(".smm-node-edit-wrap[contenteditable=\"true\"]");
const scene = document.querySelector('[data-testid="leptos-mindmap-island"]');
const active = document.activeElement;
return {
exists: edit instanceof HTMLElement,
visible:
edit instanceof HTMLElement &&
edit.style.display !== "none" &&
edit.getClientRects().length > 0,
text: edit instanceof HTMLElement ? edit.innerText || edit.textContent || "" : "",
active:
edit instanceof HTMLElement &&
(active === edit || (active instanceof Node && edit.contains(active))),
runtimeMountCount: scene instanceof HTMLElement ? Number(scene.dataset.runtimeMountCount || "0") : 0,
runtimeProjectionApplyCount:
scene instanceof HTMLElement ? Number(scene.dataset.runtimeProjectionApplyCount || "0") : 0,
runtimeProjectionDeferred:
scene instanceof HTMLElement ? scene.dataset.runtimeProjectionDeferred || "" : "",
lastRuntimeProjectionDeferReason:
scene instanceof HTMLElement ? scene.dataset.lastRuntimeProjectionDeferReason || "" : "",
backgroundProjectionRefresh:
scene instanceof HTMLElement ? scene.dataset.backgroundProjectionRefresh || "" : "",
};
});
}
async function openTopicTextEdit(page, mindmapId) {
const result = await page.evaluate(
({ mindmapId }) => {
const registry = window.__MNOTE_LEPTOS_MINDMAP_BRIDGES__ || {};
const bridge = registry[mindmapId];
const instance = bridge?.instance;
const topicNode = instance?.renderer?.findNodeByUid?.("topic") || null;
const textEdit = instance?.renderer?.textEdit;
if (!topicNode) return { ok: false, reason: "topic_node_missing" };
if (typeof textEdit?.show !== "function") return { ok: false, reason: "text_edit_show_missing" };
textEdit.show({ node: topicNode, isFromKeyDown: false });
return { ok: true };
},
{ mindmapId },
);
assert(result.ok, `topic_text_edit_open_failed:${JSON.stringify(result)}`);
const edit = page.locator(".smm-node-edit-wrap[contenteditable=\"true\"]").first();
await edit.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await edit.focus();
await page.keyboard.press("Control+A");
await page.keyboard.press("Backspace");
}
async function enterTopicTextDraftWithoutCommit(page, mindmapId, text) {
await openTopicTextEdit(page, mindmapId);
await page.keyboard.insertText(text);
await page.waitForFunction(
({ text }) => {
const edit = document.querySelector(".smm-node-edit-wrap[contenteditable=\"true\"]");
return edit instanceof HTMLElement && (edit.innerText || edit.textContent || "").includes(text);
},
{ text },
{ timeout: UI_TIMEOUT_MS },
);
return readRuntimeTextEditState(page);
}
async function assertLongLanguageEditSurvivesLiveRefresh(page, requestContext, documentId, mindmapId, failures) {
const longText =
`长文本编辑-LIVE-${Date.now().toString().slice(-6)}` +
"第一段用于覆盖真实中文连续输入,包含标点、换行前的较长句子,确保编辑框不会因为后台实时刷新而丢焦或闪烁。";
const secondHalf = "第二段继续输入,验证 live signal 之后仍然可以稳定完成编辑并保存到投影。";
await openTopicTextEdit(page, mindmapId);
const beforeEditRuntime = await readMindmapRuntimeStabilityState(page, mindmapId);
await page.keyboard.insertText(longText);
const beforeLive = await readRuntimeTextEditState(page);
const resyncPayload = await dispatchSyntheticMindmapTreeSignal(page, documentId, mindmapId, "tree:resync");
await page.waitForTimeout(2_500);
const afterLive = await readRuntimeTextEditState(page);
if (!afterLive.visible || !afterLive.active || !afterLive.text.includes(longText)) {
failures.push({
code: "mindmap_live_refresh_interrupted_long_text_edit",
beforeLive,
afterLive,
payload: resyncPayload,
});
return { beforeLive, afterLive, committed: false, longText: longText + secondHalf };
}
if (afterLive.runtimeMountCount !== beforeLive.runtimeMountCount) {
failures.push({
code: "mindmap_live_refresh_remounted_during_long_text_edit",
beforeLive,
afterLive,
payload: resyncPayload,
});
}
if (afterLive.runtimeProjectionApplyCount !== beforeLive.runtimeProjectionApplyCount) {
failures.push({
code: "mindmap_live_refresh_applied_projection_during_long_text_edit",
beforeLive,
afterLive,
payload: resyncPayload,
});
}
await page.keyboard.insertText(secondHalf);
await page.keyboard.press("Enter");
await waitForMindmapProjectionText(requestContext, documentId, mindmapId, longText + secondHalf);
await page.waitForTimeout(2_500);
const afterCommitRuntime = await readMindmapRuntimeStabilityState(page, mindmapId);
if (afterCommitRuntime.runtimeMountCount !== beforeEditRuntime.runtimeMountCount) {
failures.push({
code: "mindmap_long_text_commit_remounted_runtime",
before: beforeEditRuntime,
after: afterCommitRuntime,
});
}
if (afterCommitRuntime.runtimeProjectionApplyCount !== beforeEditRuntime.runtimeProjectionApplyCount) {
failures.push({
code: "mindmap_long_text_commit_applied_projection_refresh",
before: beforeEditRuntime,
after: afterCommitRuntime,
});
}
if (stableJson(afterCommitRuntime.viewTransform) !== stableJson(beforeEditRuntime.viewTransform)) {
failures.push({
code: "mindmap_long_text_commit_changed_view_transform",
before: beforeEditRuntime,
after: afterCommitRuntime,
});
}
const postCommitLivePayload = await dispatchSyntheticMindmapTreeSignal(page, documentId, mindmapId, "tree:resync");
await page.waitForTimeout(2_500);
const afterPostCommitLiveRuntime = await readMindmapRuntimeStabilityState(page, mindmapId);
if (afterPostCommitLiveRuntime.runtimeMountCount !== afterCommitRuntime.runtimeMountCount) {
failures.push({
code: "mindmap_post_commit_live_remounted_runtime",
before: afterCommitRuntime,
after: afterPostCommitLiveRuntime,
payload: postCommitLivePayload,
});
}
if (afterPostCommitLiveRuntime.runtimeProjectionApplyCount !== afterCommitRuntime.runtimeProjectionApplyCount) {
failures.push({
code: "mindmap_post_commit_live_applied_projection_refresh",
before: afterCommitRuntime,
after: afterPostCommitLiveRuntime,
payload: postCommitLivePayload,
});
}
if (stableJson(afterPostCommitLiveRuntime.viewTransform) !== stableJson(afterCommitRuntime.viewTransform)) {
failures.push({
code: "mindmap_post_commit_live_changed_view_transform",
before: afterCommitRuntime,
after: afterPostCommitLiveRuntime,
payload: postCommitLivePayload,
});
}
if (centerDistance(afterCommitRuntime.topicRect, afterPostCommitLiveRuntime.topicRect) > 2) {
failures.push({
code: "mindmap_post_commit_live_moved_topic_node",
before: afterCommitRuntime,
after: afterPostCommitLiveRuntime,
payload: postCommitLivePayload,
});
}
return {
beforeEditRuntime,
beforeLive,
afterLive,
committed: true,
longText: longText + secondHalf,
afterCommitRuntime,
afterPostCommitLiveRuntime,
afterCommit: await readPageState(page),
};
}
async function waitForPageCondition(page, predicate, timeoutMs = LIVE_WAIT_MS) {
const deadline = Date.now() + timeoutMs;
let last = null;
while (Date.now() < deadline) {
last = await readPageState(page);
if (predicate(last)) {
return { ok: true, state: last };
}
await page.waitForTimeout(500);
}
return { ok: false, state: last || (await readPageState(page)) };
}
function findBadRecord(records) {
return records.find((record) => {
if (record.type === "response" && record.status >= 500) return true;
return BAD_TEXT_PATTERN.test(`${record.responseText || ""}\n${record.text || ""}\n${record.failure || ""}`);
});
}
function parseJsonMaybe(value) {
if (typeof value !== "string" || !value.trim()) return null;
try {
return JSON.parse(value);
} catch {
return null;
}
}
function assertMindmapCommandApplyArtifactsDoNotTouchTreeResource(records, failures) {
const commandApplyResponses = records.filter((record) => {
if (record.type !== "response" || record.method !== "POST" || !record.url.includes("/api/mindmap/")) {
return false;
}
const requestBody = parseJsonMaybe(record.requestBody);
return requestBody?.commandName === "mindmap.command.apply";
});
if (commandApplyResponses.length === 0) {
failures.push({ code: "mindmap_command_apply_artifact_response_missing" });
return { checked: 0, artifacts: [] };
}
const artifacts = [];
let parsedCount = 0;
let skippedReadFailures = 0;
for (const record of commandApplyResponses) {
const responseBody = parseJsonMaybe(record.responseText);
const eventType = String(responseBody?.artifacts?.domainEvent?.eventType || "");
const streamDelta = responseBody?.artifacts?.domainEvent?.payload?.streamDelta || null;
const streamOp = String(streamDelta?.op || "");
const summary = {
url: record.url,
commandName: responseBody?.commandName || "",
eventType,
streamOp,
};
artifacts.push(summary);
if (!responseBody) {
if (String(record.responseText || "").startsWith("<<read_response_failed:")) {
skippedReadFailures += 1;
continue;
}
failures.push({
code: "mindmap_command_apply_artifact_response_unparseable",
record,
});
continue;
}
parsedCount += 1;
if (eventType.startsWith("tree.resource.")) {
failures.push({
code: "mindmap_command_apply_used_tree_resource_event",
summary,
});
}
if (streamOp === "resync_required") {
failures.push({
code: "mindmap_command_apply_used_tree_resync_delta",
summary,
});
}
}
if (parsedCount === 0) {
failures.push({
code: "mindmap_command_apply_artifact_response_all_unparseable",
skippedReadFailures,
});
}
return { checked: parsedCount, skippedReadFailures, artifacts };
}
async function main() {
await fs.mkdir(OUTPUT_DIR, { recursive: true });
const browser = await chromium.launch({ headless: true });
const contextA = await browser.newContext({ viewport: { width: 1440, height: 960 }, locale: "zh-CN" });
const contextB = await browser.newContext({ viewport: { width: 1440, height: 960 }, locale: "zh-CN" });
const pageA = await contextA.newPage();
const pageB = await contextB.newPage();
await installTreeEventRecorder(pageA);
await installTreeEventRecorder(pageB);
await installMindmapLoadingRecorder(pageA);
await installMindmapLoadingRecorder(pageB);
const records = [];
attachNetworkCapture(pageA, "browser-a", records);
attachNetworkCapture(pageB, "browser-b", records);
const screenshots = [];
const createdIds = [];
const failures = [];
const result = {
ok: false,
task: TASK,
baseUrl: BASE_URL,
documentId: null,
workspaceId: null,
mindmapId: null,
liveWaitMs: LIVE_WAIT_MS,
failures,
network: records,
screenshots,
};
try {
await ensureAuthenticated(pageA, contextA.request);
await ensureAuthenticated(pageB, contextB.request);
const doc = await createTempDocument(contextA.request, null);
createdIds.push(doc.documentId);
const awayDoc = await createTempDocument(contextA.request, null);
createdIds.push(awayDoc.documentId);
result.documentId = doc.documentId;
result.workspaceId = doc.workspaceId;
await renameDocument(contextA.request, doc.workspaceId, doc.documentId, `task169-mindmap-live-${Date.now().toString().slice(-6)}`);
await renameDocument(contextA.request, awayDoc.workspaceId, awayDoc.documentId, `task169-mindmap-away-${Date.now().toString().slice(-6)}`);
await openDocument(pageA, doc.workspaceId, doc.documentId);
await openDocument(pageB, doc.workspaceId, doc.documentId);
result.browserBBeforeInsert = await readPageState(pageB);
await insertMindmapThroughSlash(pageA);
result.mindmapId = await waitForMindmapId(pageA);
result.documentMindmapBlockAfterInsert = await assertDocumentMindmapBlockUsesReferenceSource(
contextA.request,
doc.documentId,
doc.workspaceId,
result.mindmapId,
failures,
"after-insert",
);
result.fileTreeMindmapOpenAfterInsert = await assertFileTreeMindmapOpenUsesObjectShell(
pageA,
doc.documentId,
result.mindmapId,
failures,
);
result.fileTreeMindmapRowsAfterInsert = await assertSingleMindmapAssetRowStable(
pageA,
doc.documentId,
result.mindmapId,
null,
failures,
"after-insert",
);
const stableMindmapObjectIdentity = result.fileTreeMindmapRowsAfterInsert.objectIdentity || "";
await waitForMindmapReady(pageA, "after-filetree-mindmap-open");
await pageA.waitForTimeout(1000);
screenshots.push(await screenshot(pageA, "01-browser-a-after-insert"));
result.browserARefreshStabilityAfterInsert = await assertMindmapRuntimeDoesNotRefreshForLiveSignals(
pageA,
doc.documentId,
result.mindmapId,
failures,
"browser-a-after-insert",
);
const editedTopicText = `二级节点-LIVE-${Date.now().toString().slice(-6)}`;
result.topicEditAttempt = await editTopicTextThroughRuntime(pageA, result.mindmapId, editedTopicText);
result.browserAAfterTopicEdit = await readPageState(pageA);
screenshots.push(await screenshot(pageA, "03-browser-a-after-topic-edit"));
await openDocument(pageA, awayDoc.workspaceId, awayDoc.documentId);
result.browserAOnAwayDocument = await readPageState(pageA);
await openDocument(pageA, doc.workspaceId, doc.documentId);
await waitForMindmapReady(pageA, "return-after-topic-edit");
await waitForMindmapProjectionText(contextA.request, doc.documentId, result.mindmapId, editedTopicText);
result.documentMindmapBlockAfterTopicReturn = await assertDocumentMindmapBlockUsesReferenceSource(
contextA.request,
doc.documentId,
doc.workspaceId,
result.mindmapId,
failures,
"after-topic-return",
);
result.browserAAfterReturnToMindmapDocument = await readPageState(pageA);
result.fileTreeMindmapRowsAfterTopicReturn = await assertSingleMindmapAssetRowStable(
pageA,
doc.documentId,
result.mindmapId,
stableMindmapObjectIdentity,
failures,
"after-topic-return",
);
await openDocument(pageA, doc.workspaceId, doc.documentId);
await waitForMindmapReady(pageA, "return-after-topic-row-check");
result.runtimeTextAfterReturnToMindmapDocument = await readMindmapRuntimeTextState(
pageA,
result.mindmapId,
editedTopicText,
);
result.projectionAfterReturnToMindmapDocument = await readMindmapProjectionText(
contextA.request,
doc.documentId,
result.mindmapId,
);
if (!result.runtimeTextAfterReturnToMindmapDocument.includesExpectedText) {
failures.push({
code: "mindmap_topic_edit_lost_after_page_switch",
expectedText: editedTopicText,
beforeSwitch: result.browserAAfterTopicEdit,
afterReturn: result.browserAAfterReturnToMindmapDocument,
runtimeTextAfterReturn: result.runtimeTextAfterReturnToMindmapDocument,
projectionAfterReturn: result.projectionAfterReturnToMindmapDocument,
});
}
if (result.browserAAfterReturnToMindmapDocument.mindmapLoadingEvents.length > 0) {
failures.push({
code: "mindmap_page_switch_showed_runtime_loading",
loadingEvents: result.browserAAfterReturnToMindmapDocument.mindmapLoadingEvents,
afterReturn: result.browserAAfterReturnToMindmapDocument,
});
}
screenshots.push(await screenshot(pageA, "04-browser-a-after-return-to-mindmap-document"));
const draftTopicText = `草稿切页-LIVE-${Date.now().toString().slice(-6)}`;
result.topicDraftBeforeSwitch = await enterTopicTextDraftWithoutCommit(pageA, result.mindmapId, draftTopicText);
await openDocument(pageA, awayDoc.workspaceId, awayDoc.documentId);
result.browserAOnAwayAfterDraftEdit = await readPageState(pageA);
await openDocument(pageA, doc.workspaceId, doc.documentId);
await waitForMindmapReady(pageA, "return-after-draft-topic-edit");
await waitForMindmapProjectionText(contextA.request, doc.documentId, result.mindmapId, draftTopicText);
result.documentMindmapBlockAfterDraftReturn = await assertDocumentMindmapBlockUsesReferenceSource(
contextA.request,
doc.documentId,
doc.workspaceId,
result.mindmapId,
failures,
"after-draft-return",
);
result.runtimeTextAfterDraftReturn = await readMindmapRuntimeTextState(pageA, result.mindmapId, draftTopicText);
result.fileTreeMindmapRowsAfterDraftReturn = await assertSingleMindmapAssetRowStable(
pageA,
doc.documentId,
result.mindmapId,
stableMindmapObjectIdentity,
failures,
"after-draft-return",
);
await openDocument(pageA, doc.workspaceId, doc.documentId);
await waitForMindmapReady(pageA, "return-after-draft-row-check");
if (!result.runtimeTextAfterDraftReturn.includesExpectedText) {
failures.push({
code: "mindmap_uncommitted_text_edit_lost_after_page_switch",
expectedText: draftTopicText,
beforeSwitch: result.topicDraftBeforeSwitch,
afterReturn: result.runtimeTextAfterDraftReturn,
});
}
result.browserARefreshStabilityAfterTopicEdit = await assertMindmapRuntimeDoesNotRefreshForLiveSignals(
pageA,
doc.documentId,
result.mindmapId,
failures,
"browser-a-after-topic-edit",
);
result.commandApplyArtifactSemantics = assertMindmapCommandApplyArtifactsDoNotTouchTreeResource(records, failures);
result.longLanguageEdit = await assertLongLanguageEditSurvivesLiveRefresh(
pageA,
contextA.request,
doc.documentId,
result.mindmapId,
failures,
);
await openDocument(pageA, awayDoc.workspaceId, awayDoc.documentId);
result.indexOpenAfterLongLanguageEdit = await assertFileTreePageMarkdownOpenUsesPageAggregate(
pageA,
doc.documentId,
result.mindmapId,
failures,
);
result.fileTreeMindmapReopenAfterIndex = await assertFileTreeMindmapOpenUsesObjectShell(
pageA,
doc.documentId,
result.mindmapId,
failures,
);
result.fileTreeMindmapRowsAfterLongLanguageEdit = await assertSingleMindmapAssetRowStable(
pageA,
doc.documentId,
result.mindmapId,
stableMindmapObjectIdentity,
failures,
"after-long-language-edit",
);
await openDocument(pageA, doc.workspaceId, doc.documentId);
await waitForMindmapReady(pageA, "return-after-long-language-row-check");
if (result.longLanguageEdit && result.longLanguageEdit.longText) {
result.runtimeTextAfterMindmapReopen = await readMindmapRuntimeTextState(
pageA,
result.mindmapId,
result.longLanguageEdit.longText,
);
if (!result.runtimeTextAfterMindmapReopen.includesExpectedText) {
failures.push({
code: "mindmap_long_language_text_lost_after_index_roundtrip",
expectedText: result.longLanguageEdit.longText,
indexOpen: result.indexOpenAfterLongLanguageEdit,
reopen: result.fileTreeMindmapReopenAfterIndex,
runtimeTextAfterMindmapReopen: result.runtimeTextAfterMindmapReopen,
});
}
}
const badRecord = findBadRecord(records);
if (badRecord) {
failures.push({ code: "mindmap_network_error_or_validator_leak", badRecord });
}
result.ok = failures.length === 0;
await writeResult(result);
if (!result.ok) {
throw new Error(`mindmap_realtime_failed:${JSON.stringify(failures, null, 2).slice(0, 6000)}`);
}
} catch (error) {
result.error = error instanceof Error ? error.stack || error.message : String(error);
screenshots.push(await screenshot(pageA, "99-failure-browser-a").catch(() => null));
screenshots.push(await screenshot(pageB, "99-failure-browser-b").catch(() => null));
await writeResult(result);
throw error;
} finally {
await cleanupDocuments(contextA.request, createdIds).catch(() => undefined);
await contextA.close().catch(() => undefined);
await contextB.close().catch(() => undefined);
await browser.close();
}
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});