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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user