2026-05-19 10:22:01 +08:00
#!/usr/bin/env node
"use strict" ;
const assert = require ( "node:assert" );
const fs = require ( "node:fs" );
const os = require ( "node:os" );
const path = require ( "node:path" );
const { chromium } = require ( "playwright" );
const {
BASE_URL ,
UI_TIMEOUT_MS ,
} = require ( "./tree-shell-smoke-helpers" );
2026-05-23 23:38:42 +08:00
const TASK = "task453-local-folder-page-ai-changed-files-smoke" ;
const OUTPUT_DIR = path . join ( process . cwd (), "tmp" , TASK );
const RESULT_PATH = path . join ( OUTPUT_DIR , "result.json" );
2026-05-19 10:22:01 +08:00
const CHROMIUM_EXECUTABLE_PATH = process . env . PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH
|| [ "/usr/bin/google-chrome-stable" , "/usr/bin/google-chrome" , "/snap/bin/chromium" , "/usr/bin/chromium" ]
. find (( candidate ) => fs . existsSync ( candidate ));
function fileUrl ( localPath ) {
return `file:// ${ localPath } ` ;
}
2026-05-23 23:38:42 +08:00
function localMdDocumentId ( relativePath ) {
return `local-md: ${ relativePath . replaceAll ( "/" , "~2F" ) } ` ;
}
2026-05-19 10:22:01 +08:00
function writeWorkspaceManifest ( root , ownerId , workspaceId ) {
fs . mkdirSync ( path . join ( root , ".mnote" ), { recursive : true });
fs . writeFileSync (
path . join ( root , ".mnote" , "workspace.json" ),
` ${ JSON . stringify ({
workspaceId ,
ownerId ,
createdAt : new Date (). toISOString (),
capabilities : [ "local_files" , "ai_sessions" , "markdown_edit" ],
} , null, 2)}\n` ,
"utf8" ,
);
}
2026-05-23 23:38:42 +08:00
async function fetchPageAggregate ( page , documentId , rootUri ) {
return await page . evaluate ( async ({ id , uri }) => {
const url = new URL ( `/api/page-aggregate/ ${ encodeURIComponent ( id ) } ` , window . location . origin );
url . searchParams . set ( "sourceKind" , "local_folder" );
url . searchParams . set ( "rootUri" , uri );
const response = await fetch ( url . toString (), { headers : { accept : "application/json" } });
return {
ok : response . ok ,
status : response . status ,
payload : await response . json (). catch (() => null ),
};
}, { id : documentId , uri : rootUri });
}
async function waitForEditorText ( page , expected ) {
await page . waitForFunction (
( text ) => {
const editor = document . querySelector ( '.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror' );
return ( editor ? . textContent || "" ). includes ( text );
},
expected ,
{ timeout : UI_TIMEOUT_MS },
);
}
async function typeDirtyText ( page , text ) {
const editor = page . locator ( '.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror' ). first ();
await editor . click ({ timeout : UI_TIMEOUT_MS });
await page . keyboard . type ( text , { delay : 8 });
await waitForEditorText ( page , text . trim ());
}
async function waitForEditorStatus ( page , status ) {
await page . waitForFunction (
( expected ) => {
const root = document . querySelector ( '[data-testid="mnote-leptos-tiptap-island-editor-root"]' );
return root ? . getAttribute ( "data-runtime-editor-status" ) === expected ;
},
status ,
{ timeout : UI_TIMEOUT_MS },
);
}
async function conflictEnvelope ( page , documentId ) {
return await page . evaluate (( docId ) => {
const snapshot = window . __mnoteDebugDocumentSessions ? . snapshot ? .();
if ( ! snapshot ) return null ;
const session = snapshot . sessions . find (( item ) => item . documentId === docId );
return session ? . lastExternalConflictEnvelope || null ;
}, documentId );
}
2026-05-19 10:22:01 +08:00
async function main () {
2026-05-23 23:38:42 +08:00
fs . mkdirSync ( OUTPUT_DIR , { recursive : true });
2026-05-19 10:22:01 +08:00
const suffix = Date . now (). toString ( 36 );
const root = fs . mkdtempSync ( path . join ( os . tmpdir (), "mnote-local-ai-changed-files-" ));
2026-05-23 23:38:42 +08:00
const documentId = localMdDocumentId ( "README.md" );
const dirtyDocumentId = localMdDocumentId ( "Dirty.md" );
2026-05-19 10:22:01 +08:00
const actorId = "user_real" ;
const sessionId = `mnote_local_ai_changed_ ${ suffix } ` ;
const runId = `run_local_ai_changed_ ${ suffix } ` ;
2026-05-23 23:38:42 +08:00
const dirtySessionId = `mnote_local_ai_dirty_ ${ suffix } ` ;
const dirtyRunId = `run_local_ai_dirty_ ${ suffix } ` ;
2026-05-19 10:22:01 +08:00
const marker = `LOCAL-AI-CHANGED-FILES- ${ suffix } ` ;
2026-05-23 23:38:42 +08:00
const dirtyMarker = `LOCAL-AI-DIRTY-FILES- ${ suffix } ` ;
const dirtyLocalToken = `LOCAL-UNSAVED-DIRTY- ${ suffix } ` ;
2026-05-19 10:22:01 +08:00
const readmePath = path . join ( root , "README.md" );
2026-05-23 23:38:42 +08:00
const dirtyPath = path . join ( root , "Dirty.md" );
2026-05-19 10:22:01 +08:00
const captured = [];
2026-05-23 23:38:42 +08:00
let currentScenario = "clean" ;
2026-05-19 10:22:01 +08:00
const browser = await chromium . launch ({
headless : true ,
...( CHROMIUM_EXECUTABLE_PATH ? { executablePath : CHROMIUM_EXECUTABLE_PATH } : {}),
});
const context = await browser . newContext ({
viewport : { width : 1440 , height : 960 },
extraHTTPHeaders : {
"x-mnote-actor-id" : actorId ,
"x-mnote-actor-type" : "user" ,
},
});
const page = await context . newPage ();
try {
const workspaceId = `local-ws: ${ actorId } :task453` ;
writeWorkspaceManifest ( root , actorId , workspaceId );
fs . writeFileSync ( readmePath , `# Local AI Changed Files\n初始内容 ${ suffix } \n` , "utf8" );
2026-05-23 23:38:42 +08:00
fs . writeFileSync ( dirtyPath , `# Dirty AI Changed Files\n初始 dirty 内容 ${ suffix } \n` , "utf8" );
2026-05-19 10:22:01 +08:00
const rootUri = fileUrl ( root );
2026-05-23 23:38:42 +08:00
const scenarioConfig = () => currentScenario === "dirty"
? {
sessionId : dirtySessionId ,
runId : dirtyRunId ,
documentId : dirtyDocumentId ,
filePath : dirtyPath ,
relativePath : "Dirty.md" ,
marker : dirtyMarker ,
message : "已修改本地 Dirty。" ,
}
: {
sessionId ,
runId ,
documentId ,
filePath : readmePath ,
relativePath : "README.md" ,
marker ,
message : "已修改本地 README。" ,
};
2026-05-19 10:22:01 +08:00
await page . route ( "**/api/ai-agent/run" , async ( route ) => {
throw new Error ( `页面 AI 不应请求旧 /api/ai-agent/run: ${ route . request (). url () } ` );
});
2026-05-23 23:38:42 +08:00
await page . route ( "**/api/documents/save" , async ( route ) => {
throw new Error ( `local-first AI smoke 不应请求 compat /api/documents/save: ${ route . request (). url () } ` );
});
2026-05-19 10:22:01 +08:00
await page . route ( "**/api/hermes/client/gateway/health**" , async ( route ) => {
await route . fulfill ({
status : 200 ,
headers : { "content-type" : "application/json" },
body : JSON . stringify ({
ok : true ,
gateway : { ok : true , status : "mocked" },
profile : { name : "reasonix" , modelConfigured : true , apiKeyConfigured : true },
suggestions : [],
}),
});
});
await page . route ( "**/api/hermes/client/tools**" , async ( route ) => {
await route . fulfill ({
status : 200 ,
headers : { "content-type" : "application/json" },
body : JSON . stringify ({ ok : true , tools : [] }),
});
});
await page . route ( "**/api/hermes/client/profiles" , async ( route ) => {
await route . fulfill ({
status : 200 ,
headers : { "content-type" : "application/json" },
body : JSON . stringify ({
ok : true ,
active : "reasonix" ,
profiles : [{ name : "reasonix" , label : "Reasonix" , modelConfigured : true , apiKeyConfigured : true }],
}),
});
});
await page . route ( "**/api/hermes/client/sessions" , async ( route ) => {
2026-05-23 23:38:42 +08:00
const scenario = scenarioConfig ();
2026-05-19 10:22:01 +08:00
captured . push ({ kind : "session" , method : route . request (). method (), body : route . request (). postData () || "" });
await route . fulfill ({
status : 200 ,
headers : { "content-type" : "application/json" },
body : JSON . stringify ({
ok : true ,
2026-05-23 23:38:42 +08:00
sessionId : scenario . sessionId ,
2026-05-19 10:22:01 +08:00
title : "本地 changed files" ,
traceId : `trace_local_changed_ ${ suffix } ` ,
persistence : "local_ai_session_jsonl" ,
sessionStorage : "local_private" ,
}),
});
});
2026-05-23 23:38:42 +08:00
await page . route ( "**/api/hermes/client/sessions/*/resume" , async ( route ) => {
const scenario = scenarioConfig ();
2026-05-19 10:22:01 +08:00
captured . push ({ kind : "session-resume" , method : route . request (). method (), body : "" });
await route . fulfill ({
status : 200 ,
headers : { "content-type" : "application/json" },
body : JSON . stringify ({
ok : true ,
2026-05-23 23:38:42 +08:00
sessionId : scenario . sessionId ,
session : { sessionId : scenario . sessionId , messages : [] },
2026-05-19 10:22:01 +08:00
runtime : {
2026-05-23 23:38:42 +08:00
sessionId : scenario . sessionId ,
runId : scenario . runId ,
2026-05-19 10:22:01 +08:00
status : "completed" ,
profile : "reasonix" ,
2026-05-23 23:38:42 +08:00
documentId : scenario . documentId ,
2026-05-19 10:22:01 +08:00
traceId : `trace_local_changed_resume_ ${ suffix } ` ,
},
}),
});
});
await page . route ( "**/api/hermes/client/runs" , async ( route ) => {
2026-05-23 23:38:42 +08:00
const scenario = scenarioConfig ();
2026-05-19 10:22:01 +08:00
captured . push ({ kind : "run" , method : route . request (). method (), body : route . request (). postData () || "" });
await route . fulfill ({
status : 200 ,
headers : { "content-type" : "application/json" },
body : JSON . stringify ({
ok : true ,
2026-05-23 23:38:42 +08:00
sessionId : scenario . sessionId ,
runId : scenario . runId ,
2026-05-19 10:22:01 +08:00
events : [],
traceId : `trace_local_changed_run_ ${ suffix } ` ,
persistence : "local_ai_session_jsonl" ,
sessionStorage : "local_private" ,
}),
});
});
2026-05-23 23:38:42 +08:00
await page . route ( "**/api/hermes/client/events/*" , async ( route ) => {
const scenario = scenarioConfig ();
2026-05-19 10:22:01 +08:00
captured . push ({ kind : "events" , method : route . request (). method (), body : "" });
2026-05-23 23:38:42 +08:00
fs . appendFileSync ( scenario . filePath , `\nAI 写入标记: ${ scenario . marker } \n` , "utf8" );
2026-05-19 10:22:01 +08:00
await route . fulfill ({
status : 200 ,
headers : { "content-type" : "text/event-stream; charset=utf-8" },
body :
2026-05-23 23:38:42 +08:00
`data: ${ JSON . stringify ({ event : "message.delta" , run_id : scenario . runId , session_id : scenario . sessionId , delta : scenario . message } )}\n\n` +
2026-05-19 10:22:01 +08:00
`data: ${ JSON . stringify ({
event : "run.completed" ,
2026-05-23 23:38:42 +08:00
run_id : scenario . runId ,
session_id : scenario . sessionId ,
output : scenario . message ,
2026-05-19 10:22:01 +08:00
agentAudit : {
2026-05-23 23:38:42 +08:00
eventId : `audit_local_changed_ ${ currentScenario } _ ${ suffix } ` ,
2026-05-28 22:01:44 +08:00
actorId ,
actorType : "user" ,
agentKind : "reasonix" ,
2026-05-19 10:22:01 +08:00
rootUri ,
diffSummary : "1 changed file(s)" ,
changedFiles : [
{
2026-05-23 23:38:42 +08:00
path : scenario . relativePath ,
2026-05-19 10:22:01 +08:00
changeType : "modified" ,
2026-05-23 23:38:42 +08:00
summary : `追加 ${ scenario . marker } ` ,
2026-05-28 22:01:44 +08:00
hashBefore : "111" ,
hashAfter : "222" ,
modifiedBeforeMs : 10 ,
modifiedAfterMs : 20 ,
2026-05-19 10:22:01 +08:00
} ,
],
},
})}\n\n` ,
});
});
const documentUrl = new URL ( ` ${ BASE_URL } /documents/ ${ encodeURIComponent ( documentId ) } ` );
documentUrl . searchParams . set ( "sourceKind" , "local_folder" );
documentUrl . searchParams . set ( "rootUri" , rootUri );
await page . goto ( documentUrl . toString (), { waitUntil : "domcontentloaded" , timeout : UI_TIMEOUT_MS });
await page . waitForFunction (
() => ( document . body . textContent || "" ). includes ( "Local AI Changed Files" ),
null ,
{ timeout : UI_TIMEOUT_MS },
);
await page . locator ( '[data-testid="mnote-leptos-tiptap-island-editor-root"]' ). first (). waitFor ({
state : "attached" ,
timeout : UI_TIMEOUT_MS ,
}). catch (() => undefined );
await page . getByTestId ( "wolai-floating-ai" ). click ({ timeout : UI_TIMEOUT_MS });
await page . locator ( "[data-page-ai-input]" ). fill ( `请修改 README 并记录 changed files ${ 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 ( "agent.changed_files" ) && drawerText . includes ( "README.md" );
},
null ,
{ timeout : UI_TIMEOUT_MS },
);
const cards = await page . $$eval ( "[data-page-ai-tool-card]" , ( nodes ) =>
nodes . map (( node ) => ({
status : node . getAttribute ( "data-page-ai-tool-status" ),
text : node . textContent || "" ,
})),
);
assert (
cards . some (( card ) =>
card . status === "completed"
&& card . text . includes ( "agent.changed_files" )
&& card . text . includes ( "README.md" )
&& card . text . includes ( marker )
2026-05-28 22:01:44 +08:00
&& card . text . includes ( "hash 111→222" )
&& card . text . includes ( "reasonix/user/user_real" )
2026-05-19 10:22:01 +08:00
),
`本地 AI changed files 工具卡未显示 README.md 与 diff 摘要: ${ JSON . stringify ( cards ) } ` ,
);
2026-05-23 23:38:42 +08:00
const diskText = fs . readFileSync ( readmePath , "utf8" );
assert ( diskText . includes ( marker ), "本地 README.md 未写入 smoke 标记" );
const aggregate = await fetchPageAggregate ( page , documentId , rootUri );
assert . equal ( aggregate . ok , true , `Page Aggregate 应能读取 local_folder 文档: ${ JSON . stringify ( aggregate ) } ` );
assert (
JSON . stringify ( aggregate . payload || {}). includes ( marker ),
`Page Aggregate 应读回 AI 写入标记: ${ JSON . stringify ( aggregate ) } ` ,
2026-05-19 10:22:01 +08:00
);
2026-05-23 23:38:42 +08:00
await waitForEditorText ( page , marker );
const editorState = 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 . notEqual ( editorState . status , "external-change-conflict" , `clean AI 写入不应触发冲突态: ${ JSON . stringify ( editorState ) } ` );
assert . equal ( editorState . conflictVisible , false , `clean AI 写入不应显示冲突面板: ${ JSON . stringify ( editorState ) } ` );
assert ( captured . some (( entry ) => entry . kind === "run" ), "未捕获 page AI run 请求" );
const runBody = JSON . parse ( captured . find (( entry ) => entry . kind === "run" ) ? . body || "{}" );
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 ) } ` );
2026-05-28 22:01:44 +08:00
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 ) } ` );
2026-05-23 23:38:42 +08:00
const cleanCapturedKinds = captured . map (( entry ) => entry . kind );
2026-05-28 22:01:44 +08:00
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 ;
});
2026-05-23 23:38:42 +08:00
currentScenario = "dirty" ;
captured . length = 0 ;
const dirtyUrl = new URL ( ` ${ BASE_URL } /documents/ ${ encodeURIComponent ( dirtyDocumentId ) } ` );
dirtyUrl . searchParams . set ( "sourceKind" , "local_folder" );
dirtyUrl . searchParams . set ( "rootUri" , rootUri );
await page . goto ( dirtyUrl . toString (), { waitUntil : "domcontentloaded" , timeout : UI_TIMEOUT_MS });
await waitForEditorText ( page , "Dirty AI Changed Files" );
await typeDirtyText ( page , ` ${ dirtyLocalToken } ` );
await page . getByTestId ( "wolai-floating-ai" ). click ({ timeout : UI_TIMEOUT_MS });
await page . locator ( "[data-page-ai-input]" ). fill ( `请修改 Dirty 并记录 changed files ${ dirtyMarker } ` , { 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 || "" ;
2026-05-28 22:01:44 +08:00
return drawerText . includes ( "目标文档存在未保存或外部变更状态" );
2026-05-23 23:38:42 +08:00
},
null ,
{ timeout : UI_TIMEOUT_MS },
);
2026-05-28 22:01:44 +08:00
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 ) } ` );
2026-05-23 23:38:42 +08:00
const dirtyAggregate = await fetchPageAggregate ( page , dirtyDocumentId , rootUri );
assert (
2026-05-28 22:01:44 +08:00
! JSON . stringify ( dirtyAggregate . payload || {}). includes ( dirtyMarker ),
`dirty buffer 阻断后 Page Aggregate 不应读回 AI 写入标记: ${ JSON . stringify ( dirtyAggregate ) } ` ,
2026-05-23 23:38:42 +08:00
);
const result = {
ok : true ,
root ,
documentId ,
sessionId ,
runId ,
marker ,
aggregateRevision : aggregate . payload ? . result ? . body ? . revision ?? aggregate . payload ? . body ? . revision ?? null ,
editorStatus : editorState . status ,
dirtyDocumentId ,
dirtyRunId ,
dirtyMarker ,
2026-05-28 22:01:44 +08:00
dirtyBlocked : true ,
dirtyEditorStatus : dirtyEditorState . status ,
2026-05-23 23:38:42 +08:00
capturedKinds : cleanCapturedKinds ,
dirtyCapturedKinds : captured . map (( entry ) => entry . kind ),
resultPath : RESULT_PATH ,
};
fs . writeFileSync ( RESULT_PATH , ` ${ JSON . stringify ( result , null , 2 ) } \n` , "utf8" );
console . log ( JSON . stringify ( result , null , 2 ));
2026-05-19 10:22:01 +08:00
} finally {
await context . close (). catch (() => undefined );
await browser . close (). catch (() => undefined );
fs . rmSync ( root , { recursive : true , force : true });
}
}
main (). catch (( error ) => {
console . error ( error instanceof Error ? error . stack || error . message : String ( error ));
process . exit ( 1 );
});