feat: consolidate local-first mnote web runtime

This commit is contained in:
lix-2026
2026-05-28 22:01:44 +08:00
parent 7354807ee9
commit 39b9a0183a
154 changed files with 13591 additions and 12728 deletions
@@ -256,6 +256,9 @@ async function main() {
output: scenario.message,
agentAudit: {
eventId: `audit_local_changed_${currentScenario}_${suffix}`,
actorId,
actorType: "user",
agentKind: "reasonix",
rootUri,
diffSummary: "1 changed file(s)",
changedFiles: [
@@ -263,6 +266,10 @@ async function main() {
path: scenario.relativePath,
changeType: "modified",
summary: `追加 ${scenario.marker}`,
hashBefore: "111",
hashAfter: "222",
modifiedBeforeMs: 10,
modifiedAfterMs: 20,
},
],
},
@@ -308,6 +315,8 @@ async function main() {
&& card.text.includes("agent.changed_files")
&& card.text.includes("README.md")
&& card.text.includes(marker)
&& card.text.includes("hash 111→222")
&& card.text.includes("reasonix/user/user_real")
),
`本地 AI changed files 工具卡未显示 README.md 与 diff 摘要: ${JSON.stringify(cards)}`,
);
@@ -337,8 +346,70 @@ async function main() {
assert.equal(runBody.documentId, documentId, `Hermes run 应携带 local documentId: ${JSON.stringify(runBody)}`);
assert.equal(runBody.sourceKind, "local_folder", `Hermes run 应携带 local_folder sourceKind: ${JSON.stringify(runBody)}`);
assert.equal(runBody.rootUri, rootUri, `Hermes run 应携带 rootUri: ${JSON.stringify(runBody)}`);
assert.equal(runBody.editorTarget?.schema, "mnote.ai_editor_target.v1", `Hermes run 应携带 AI editor target: ${JSON.stringify(runBody)}`);
assert.equal(runBody.editorTarget?.source, "open_editors_snapshot", `AI editor target 应来自 OpenEditorsSnapshot: ${JSON.stringify(runBody.editorTarget)}`);
assert.equal(runBody.editorTarget?.documentId, documentId, `AI editor target 应指向当前文档: ${JSON.stringify(runBody.editorTarget)}`);
assert.equal(runBody.runTargetSnapshot?.schema, "mnote.page_ai_run_target_snapshot.v1", `Hermes run 应携带冻结 target snapshot: ${JSON.stringify(runBody.runTargetSnapshot)}`);
assert.equal(runBody.runTargetSnapshot?.editorTarget?.documentId, documentId, `冻结 target snapshot 应指向发起 run 时的文档: ${JSON.stringify(runBody.runTargetSnapshot)}`);
assert.equal(runBody.runTargetSnapshot?.editorTarget?.workspacePath?.rootUri, rootUri, `冻结 target snapshot 应保留发起 run 时的 rootUri: ${JSON.stringify(runBody.runTargetSnapshot)}`);
assert.equal(runBody.pageContext?.aiContext?.runTargetSnapshot?.editorTarget?.documentId, documentId, `pageContext.aiContext 应保留冻结 target snapshot: ${JSON.stringify(runBody.pageContext?.aiContext)}`);
assert.equal(runBody.pageContext?.aiContext?.activeEditorTarget?.source, "open_editors_snapshot", `pageContext.aiContext 应包含 activeEditorTarget: ${JSON.stringify(runBody.pageContext?.aiContext)}`);
assert.equal(runBody.pageContext?.aiContext?.openEditorsSnapshot?.activeEditor?.documentId, documentId, `aiContext.openEditorsSnapshot 应包含 active editor: ${JSON.stringify(runBody.pageContext?.aiContext?.openEditorsSnapshot)}`);
assert(runBody.pageContext?.aiContext?.openEditorsSnapshot?.groups?.primary, `aiContext.openEditorsSnapshot 应保留 primary group: ${JSON.stringify(runBody.pageContext?.aiContext?.openEditorsSnapshot)}`);
assert(runBody.pageContext?.aiContext?.openEditorsSnapshot?.groups?.secondary, `aiContext.openEditorsSnapshot 应保留 secondary group: ${JSON.stringify(runBody.pageContext?.aiContext?.openEditorsSnapshot)}`);
const cleanCapturedKinds = captured.map((entry) => entry.kind);
captured.length = 0;
await page.evaluate(({ otherRootUri, otherWorkspaceId }) => {
const runtime = window.__mnoteDocumentPaneRuntime;
if (!runtime || typeof runtime.getOpenEditorsSnapshot !== "function") {
throw new Error("missing_open_editors_snapshot_runtime");
}
window.__task453OriginalOpenEditorsSnapshot = runtime.getOpenEditorsSnapshot.bind(runtime);
runtime.getOpenEditorsSnapshot = () => {
const snapshot = window.__task453OriginalOpenEditorsSnapshot();
const cloned = JSON.parse(JSON.stringify(snapshot || {}));
const poisonEditor = (entry) => {
if (!entry || typeof entry !== "object") return;
entry.workspaceId = otherWorkspaceId;
entry.workspacePath = Object.assign({}, entry.workspacePath || {}, {
workspaceId: otherWorkspaceId,
rootUri: otherRootUri,
sourceKind: "local_folder",
});
};
poisonEditor(cloned.activeEditor);
(cloned.editors || []).forEach((entry) => {
if (entry && entry.active) poisonEditor(entry);
});
Object.values(cloned.groups || {}).forEach((group) => {
(group.editors || []).forEach((entry) => {
if (entry && entry.active) poisonEditor(entry);
});
});
return cloned;
};
}, { otherRootUri: "file:///tmp/mnote-task453-other-root", otherWorkspaceId: "local-ws:user_real:other" });
await page.locator("[data-page-ai-input]").fill(`请错误写入其他 workspace ${marker}`, { timeout: UI_TIMEOUT_MS });
await page.locator('[data-page-ai-action="send"]').click({ timeout: UI_TIMEOUT_MS });
await page.waitForFunction(
() => {
const drawerText = document.querySelector('[data-testid="wolai-page-ai-drawer"]')?.textContent || "";
return drawerText.includes("AI target 与当前本地工作区 rootUri 不一致")
|| drawerText.includes("AI target 与当前 workspaceId 不一致");
},
null,
{ timeout: UI_TIMEOUT_MS },
);
assert(!captured.some((entry) => entry.kind === "run"), `跨 workspace target 不应发起 Hermes run: ${JSON.stringify(captured)}`);
await page.evaluate(() => {
const runtime = window.__mnoteDocumentPaneRuntime;
if (runtime && window.__task453OriginalOpenEditorsSnapshot) {
runtime.getOpenEditorsSnapshot = window.__task453OriginalOpenEditorsSnapshot;
}
delete window.__task453OriginalOpenEditorsSnapshot;
});
currentScenario = "dirty";
captured.length = 0;
const dirtyUrl = new URL(`${BASE_URL}/documents/${encodeURIComponent(dirtyDocumentId)}`);
@@ -353,49 +424,31 @@ async function main() {
await page.waitForFunction(
() => {
const drawerText = document.querySelector('[data-testid="wolai-page-ai-drawer"]')?.textContent || "";
return drawerText.includes("agent.changed_files") && drawerText.includes("Dirty.md");
return drawerText.includes("目标文档存在未保存或外部变更状态");
},
null,
{ timeout: UI_TIMEOUT_MS },
);
await waitForEditorStatus(page, "external-change-conflict");
await page.locator('[data-testid="mnote-editor-conflict-panel"]').waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await page.waitForFunction(
(expected) => {
const panel = document.querySelector('[data-testid="mnote-editor-conflict-panel"]');
return (panel?.textContent || "").includes(expected);
},
`agent run ${dirtyRunId}`,
{ timeout: UI_TIMEOUT_MS },
);
const envelope = await conflictEnvelope(page, dirtyDocumentId);
assert(envelope, "dirty AI 写入应生成冲突信封");
assert("externalActor" in envelope, `冲突信封应包含 externalActor: ${JSON.stringify(envelope)}`);
assert("dirtyState" in envelope, `冲突信封应包含 dirtyState: ${JSON.stringify(envelope)}`);
assert("bufferFileVersion" in envelope, `冲突信封应包含 bufferFileVersion: ${JSON.stringify(envelope)}`);
await page.locator('[data-testid="mnote-conflict-open-diff"]').click({ timeout: UI_TIMEOUT_MS });
await page.locator('[data-testid="mnote-conflict-diff-panel"]').waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await page.waitForFunction(
({ localToken, aiToken }) => {
const panel = document.querySelector('[data-testid="mnote-conflict-diff-panel"]');
const text = panel?.textContent || "";
return text.includes(localToken) && text.includes(aiToken);
},
{ localToken: dirtyLocalToken, aiToken: dirtyMarker },
{ timeout: UI_TIMEOUT_MS },
);
const diffText = await page.locator('[data-testid="mnote-conflict-diff-panel"]').innerText({ timeout: UI_TIMEOUT_MS });
assert(diffText.includes(dirtyLocalToken), `dirty diff 应包含本地未保存内容: ${diffText}`);
assert(diffText.includes(dirtyMarker), `dirty diff 应包含 AI 写盘内容: ${diffText}`);
assert(!captured.some((entry) => entry.kind === "run"), `dirty buffer 下 Page AI 不应发起 Hermes run: ${JSON.stringify(captured)}`);
const dirtyDiskText = fs.readFileSync(dirtyPath, "utf8");
assert(!dirtyDiskText.includes(dirtyMarker), "dirty buffer 阻断后磁盘不应出现 AI 写入标记");
const dirtyEditorState = await page.evaluate(() => {
const root = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"]');
const editor = document.querySelector('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror');
const conflictPanel = document.querySelector('[data-testid="mnote-editor-conflict-panel"]');
return {
status: root?.getAttribute("data-runtime-editor-status") || "",
text: editor?.textContent || "",
conflictVisible: Boolean(conflictPanel && conflictPanel.getClientRects().length > 0),
};
});
assert(dirtyEditorState.text.includes(dirtyLocalToken), `dirty buffer 阻断后本地未保存内容应仍在编辑器中: ${JSON.stringify(dirtyEditorState)}`);
assert.equal(dirtyEditorState.conflictVisible, false, `dirty buffer 阻断不应生成冲突面板: ${JSON.stringify(dirtyEditorState)}`);
const dirtyAggregate = await fetchPageAggregate(page, dirtyDocumentId, rootUri);
assert(
JSON.stringify(dirtyAggregate.payload || {}).includes(dirtyMarker),
`dirty Page Aggregate 应读回 AI 写入标记: ${JSON.stringify(dirtyAggregate)}`,
!JSON.stringify(dirtyAggregate.payload || {}).includes(dirtyMarker),
`dirty buffer 阻断后 Page Aggregate 应读回 AI 写入标记: ${JSON.stringify(dirtyAggregate)}`,
);
const dirtyRunBody = JSON.parse(captured.find((entry) => entry.kind === "run")?.body || "{}");
assert.equal(dirtyRunBody.documentId, dirtyDocumentId, `dirty Hermes run 应携带 local documentId: ${JSON.stringify(dirtyRunBody)}`);
assert.equal(dirtyRunBody.sourceKind, "local_folder", `dirty Hermes run 应携带 local_folder sourceKind: ${JSON.stringify(dirtyRunBody)}`);
assert.equal(dirtyRunBody.rootUri, rootUri, `dirty Hermes run 应携带 rootUri: ${JSON.stringify(dirtyRunBody)}`);
const result = {
ok: true,
@@ -409,8 +462,8 @@ async function main() {
dirtyDocumentId,
dirtyRunId,
dirtyMarker,
dirtyConflictStatus: "external-change-conflict",
dirtyEnvelope: envelope,
dirtyBlocked: true,
dirtyEditorStatus: dirtyEditorState.status,
capturedKinds: cleanCapturedKinds,
dirtyCapturedKinds: captured.map((entry) => entry.kind),
resultPath: RESULT_PATH,