chore: 收口 review 执行清单与 runtime 验证

- 补齐 design/10-review 执行清单、验收标准与相关设计治理记录

- 迁移已完成的 tree、mindmap、runtime fallback、AI kernel 等设计和缺陷条目

- 推进 Rust Web runtime、tree/sidebar、page aggregate、mindmap 与 OnlyOffice 路由侧验证支撑

- 增加 task177-task180 smoke/audit 脚本及前端相关测试覆盖
This commit is contained in:
lix-2026
2026-05-14 05:52:08 +08:00
parent b4a452a8b7
commit 96e03645f7
69 changed files with 4780 additions and 979 deletions
+205 -2
View File
@@ -126,7 +126,7 @@ function attachNetworkCapture(page, label, records) {
url,
status,
requestBody: request.postData() || null,
responseText: responseText ? responseText.slice(0, 4000) : null,
responseText: responseText ? responseText.slice(0, 12000) : null,
});
});
page.on("requestfailed", (request) => {
@@ -178,6 +178,28 @@ async function readPageState(page) {
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),
@@ -212,6 +234,7 @@ async function readPageState(page) {
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__ || [],
};
});
@@ -260,6 +283,24 @@ 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 {
@@ -271,7 +312,7 @@ function rectCenter(rect) {
function centerDistance(a, b) {
const ca = rectCenter(a);
const cb = rectCenter(b);
if (!ca || !cb) return Number.POSITIVE_INFINITY;
if (!ca || !cb) return 0;
return Math.hypot(ca.x - cb.x, ca.y - cb.y);
}
@@ -324,6 +365,7 @@ async function dispatchSyntheticMindmapTreeSignal(page, documentId, mindmapId, e
}
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) {
@@ -551,6 +593,56 @@ async function assertFileTreeMindmapOpenUsesObjectShell(page, documentId, mindma
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 assertFileTreeIndexOpenUsesPageAggregate(page, documentId, mindmapId, failures) {
await openFilesystemView(page);
const opened = await page.evaluate(
@@ -942,6 +1034,77 @@ function findBadRecord(records) {
});
}
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 });
@@ -1005,6 +1168,15 @@ async function main() {
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"));
@@ -1034,6 +1206,16 @@ async function main() {
"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,
@@ -1078,6 +1260,16 @@ async function main() {
"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",
@@ -1093,6 +1285,7 @@ async function main() {
failures,
"browser-a-after-topic-edit",
);
result.commandApplyArtifactSemantics = assertMindmapCommandApplyArtifactsDoNotTouchTreeResource(records, failures);
result.longLanguageEdit = await assertLongLanguageEditSurvivesLiveRefresh(
pageA,
contextA.request,
@@ -1113,6 +1306,16 @@ async function main() {
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,