feat: consolidate local-first mnote web runtime
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user