2026-06-01 09:29:12 +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 ,
ensureAuthenticated ,
} = require ( "./tree-shell-smoke-helpers" );
const TASK = "task520-page-ai-raw-resource-target-smoke" ;
const OUT_DIR = path . join ( process . cwd (), "tmp" , TASK );
const RESULT_PATH = path . join ( OUT_DIR , "result.json" );
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 } ` ;
}
function localMdDocumentId ( relativePath ) {
return `local-md: ${ relativePath . replaceAll ( "/" , "~2F" ) } ` ;
}
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" ,
);
}
function documentUrl ( root , relativePath ) {
const url = new URL ( ` ${ BASE_URL } /documents/ ${ encodeURIComponent ( localMdDocumentId ( relativePath )) } ` );
url . searchParams . set ( "sourceKind" , "local_folder" );
url . searchParams . set ( "rootUri" , fileUrl ( root ));
url . searchParams . set ( "treeView" , "filetree" );
return url . toString ();
}
async function quickLogin ( page , request ) {
await ensureAuthenticated ( page , request );
}
async function waitFiletreeRow ( page , relativePath ) {
return await page . waitForFunction (
( expectedRelativePath ) => Array . from ( document . querySelectorAll ( "#sidebar-file-tree-root .tree-row[data-shell-mode=\"filetree\"]" ))
. some (( row ) => row . getAttribute ( "data-local-relative-path" ) === expectedRelativePath ),
relativePath ,
{ timeout : UI_TIMEOUT_MS },
);
}
async function clickFiletreeOpen ( page , relativePath ) {
const handle = await page . waitForFunction (
( expectedRelativePath ) => {
const row = Array . from ( document . querySelectorAll ( "#sidebar-file-tree-root .tree-row[data-shell-mode=\"filetree\"]" ))
. find (( candidate ) => candidate . getAttribute ( "data-local-relative-path" ) === expectedRelativePath );
return row ? . querySelector ( '[data-rust-action="open"], .tree-link' ) || null ;
},
relativePath ,
{ timeout : UI_TIMEOUT_MS },
);
await handle . asElement (). click ();
}
async function expandFiletreeFolder ( page , relativePath ) {
await waitFiletreeRow ( page , relativePath );
const expanded = await page . evaluate (( expectedRelativePath ) => {
const row = Array . from ( document . querySelectorAll ( "#sidebar-file-tree-root .tree-row[data-shell-mode=\"filetree\"]" ))
. find (( candidate ) => candidate . getAttribute ( "data-local-relative-path" ) === expectedRelativePath );
return row ? . getAttribute ( "aria-expanded" ) === "true" ;
}, relativePath );
if ( ! expanded ) {
await clickFiletreeOpen ( page , relativePath );
}
await page . waitForFunction (
( expectedRelativePath ) => {
const row = Array . from ( document . querySelectorAll ( "#sidebar-file-tree-root .tree-row[data-shell-mode=\"filetree\"]" ))
. find (( candidate ) => candidate . getAttribute ( "data-local-relative-path" ) === expectedRelativePath );
return row ? . getAttribute ( "aria-expanded" ) === "true" ;
},
relativePath ,
{ timeout : UI_TIMEOUT_MS },
);
}
async function saveScreenshot ( page , name ) {
const target = path . join ( OUT_DIR , ` ${ name } .png` );
await page . screenshot ({ path : target , fullPage : false });
return target ;
}
2026-06-01 10:30:42 +08:00
function cleanupRoot ( root ) {
for ( let attempt = 0 ; attempt < 5 ; attempt += 1 ) {
try {
fs . rmSync ( root , { recursive : true , force : true , maxRetries : 3 , retryDelay : 100 });
return ;
} catch ( error ) {
if ( attempt === 4 ) throw error ;
}
}
}
2026-06-01 09:29:12 +08:00
async function main () {
fs . mkdirSync ( OUT_DIR , { recursive : true });
const actorId = "mnote-e2e" ;
const workspaceId = `local-ws: ${ actorId } :task520` ;
const root = fs . mkdtempSync ( path . join ( os . tmpdir (), "mnote-task520-raw-target-" ));
const rootUri = fileUrl ( root );
const pagePath = "Page.md" ;
2026-06-01 10:30:42 +08:00
const expectOcrContext = process . env . TASK520_OCR_CONTEXT === "1" ;
const rawPath = expectOcrContext ? "Page/photo.png" : "Page/notes.txt" ;
const ocrPath = "Page.ocr/photo.png.ocr.md" ;
2026-06-01 09:29:12 +08:00
const documentId = localMdDocumentId ( pagePath );
const captured = [];
let caughtError = null ;
fs . mkdirSync ( path . join ( root , "Page" ), { recursive : true });
writeWorkspaceManifest ( root , actorId , workspaceId );
fs . writeFileSync ( path . join ( root , pagePath ), "# Page\n\nTask520 page\n" , "utf8" );
2026-06-01 10:30:42 +08:00
if ( expectOcrContext ) {
fs . writeFileSync ( path . join ( root , rawPath ), Buffer . from ([ 0x89 , 0x50 , 0x4e , 0x47 , 0x0d , 0x0a ]));
fs . mkdirSync ( path . join ( root , "Page.ocr" ), { recursive : true });
fs . writeFileSync (
path . join ( root , ocrPath ),
"---\nmnote_ocr_version: 1\nprovider: mock\nmodel_version: vlm\nowner_document: ../Page.md\nsource_path: ./Page/photo.png\nsource_root_relative_path: Page/photo.png\nsource_size: 6\nsource_mtime_ms: 1\nstatus: done\ncreated_at: 1\nupdated_at: 1\n---\n\nTask520 OCR sidecar context text\n" ,
"utf8" ,
);
fs . writeFileSync (
path . join ( root , ".mnote" , "ocr-index.json" ),
` ${ JSON . stringify ({
version : 1 ,
entries : {
[ rawPath ] : {
jobId : "ocr_task520" ,
ownerDocumentId : documentId ,
ownerDocumentPath : pagePath ,
sourceRootRelativePath : rawPath ,
ocrRootRelativePath : ocrPath ,
provider : "mock" ,
modelVersion : "vlm" ,
status : "done" ,
sourceSize : 6 ,
sourceMtimeMs : 1 ,
createdAtMs : 1 ,
updatedAtMs : 1 ,
plainTextPreview : "Task520 OCR sidecar context text" ,
} ,
},
}, null, 2)}\n` ,
"utf8" ,
);
} else {
fs . writeFileSync ( path . join ( root , rawPath ), "Task520 raw resource target\n" , "utf8" );
}
2026-06-01 09:29:12 +08:00
const browser = await chromium . launch ({
headless : process . env . HEADFUL !== "1" ,
...( CHROMIUM_EXECUTABLE_PATH ? { executablePath : CHROMIUM_EXECUTABLE_PATH } : {}),
});
const context = await browser . newContext ({
viewport : { width : 1440 , height : 960 },
locale : "zh-CN" ,
extraHTTPHeaders : {
"x-mnote-actor-id" : actorId ,
"x-mnote-actor-type" : "user" ,
},
});
const page = await context . newPage ();
try {
await page . route ( "**/api/user/access-policy**" , async ( route ) => {
await route . fulfill ({
status : 200 ,
headers : { "content-type" : "application/json" },
body : JSON . stringify ({
ok : true ,
grants : [{
id : "grant_task520" ,
userId : actorId ,
workspaceId ,
rootUri ,
rootPath : root ,
permission : "write" ,
recursive : true ,
capabilities : [ "ai" , "markdown_edit" ],
source : "user" ,
status : "active" ,
}],
}),
});
});
await page . route ( "**/api/ui/preferences**" , async ( route ) => {
captured . push ({ kind : "ui-preferences" , method : route . request (). method (), body : route . request (). postData () || "" });
await route . fulfill ({
status : 200 ,
headers : { "content-type" : "application/json" },
body : JSON . stringify ({ ok : true , owner : "mnote-web" , result : {} }),
});
});
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 }, profile : { name : "reasonix" , modelConfigured : true , apiKeyConfigured : true } }),
});
});
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/ai/agent-profiles**" , async ( route ) => {
await route . fulfill ({
status : 200 ,
headers : { "content-type" : "application/json" },
body : JSON . stringify ({ ok : true , agentId : "reasonix" , profiles : [] }),
});
});
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 : "mnoteai" , profiles : [{ name : "mnoteai" , label : "MNote AI" , modelConfigured : true , apiKeyConfigured : true }] }),
});
});
await page . route ( "**/api/hermes/client/skills**" , async ( route ) => {
await route . fulfill ({
status : 200 ,
headers : { "content-type" : "application/json" },
body : JSON . stringify ({ ok : true , categories : [], archived : [] }),
});
});
2026-06-05 23:00:53 +08:00
await page . route ( "**/api/hermes/client/capabilities**" , async ( route ) => {
await route . fulfill ({
status : 200 ,
headers : { "content-type" : "application/json" },
body : JSON . stringify ({ ok : true , runtime : "mnote" , categories : [], archived : [] }),
});
});
2026-06-01 09:29:12 +08:00
await page . route ( "**/api/hermes/client/sessions" , async ( route ) => {
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 , sessionId : "mnote_task520" , title : "task520" , traceId : "trace_task520" }),
});
});
await page . route ( "**/api/hermes/client/runs" , async ( route ) => {
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 , sessionId : "mnote_task520" , runId : "run_task520" , events : [], traceId : "trace_run_task520" }),
});
});
await page . route ( "**/api/hermes/client/events/*" , async ( route ) => {
await route . fulfill ({
status : 200 ,
headers : { "content-type" : "text/event-stream; charset=utf-8" },
body : `data: ${ JSON . stringify ({ event : "message.delta" , run_id : "run_task520" , delta : "Task520 response" } )}\n\n`
+ `data: ${ JSON . stringify ({ event : "run.completed" , run_id : "run_task520" , output : "Task520 response" } )}\n\n` ,
});
});
await quickLogin ( page , context . request );
const response = await page . goto ( documentUrl ( root , pagePath ), { waitUntil : "domcontentloaded" , timeout : UI_TIMEOUT_MS });
assert ( response && response . status () === 200 , `文档页状态码异常: ${ response && response . status () } ` );
await page . locator ( ".document-pane[data-pane-role=\"primary\"] .editor-surface .ProseMirror" ). first (). waitFor ({
state : "visible" ,
timeout : UI_TIMEOUT_MS ,
});
await expandFiletreeFolder ( page , "Page" );
await waitFiletreeRow ( page , rawPath );
await clickFiletreeOpen ( page , rawPath );
await page . waitForFunction (
( expectedPath ) => {
const snapshot = window . __mnoteDocumentPaneRuntime ? . getOpenEditorsSnapshot ? .() || window . __mnoteOpenEditorsSnapshot || null ;
return ( snapshot ? . resourceEditors || []). some (( entry ) => entry ? . path === expectedPath && entry ? . active === true );
},
rawPath ,
{ timeout : UI_TIMEOUT_MS },
);
await page . getByTestId ( "wolai-floating-ai" ). click ({ timeout : UI_TIMEOUT_MS });
await page . locator ( '[data-testid="wolai-page-ai-drawer"]' ). waitFor ({ state : "visible" , timeout : UI_TIMEOUT_MS });
const targetChip = page . locator ( "[data-page-ai-target-chip]" );
await targetChip . waitFor ({ state : "visible" , timeout : UI_TIMEOUT_MS });
const chipText = ( await targetChip . innerText ({ timeout : UI_TIMEOUT_MS })). trim ();
2026-06-01 10:30:42 +08:00
const expectedName = expectOcrContext ? "photo.png" : "notes.txt" ;
assert ( chipText . includes ( expectedName ), `raw resource 打开后 target chip 应指向 ${ expectedName } : ${ chipText } ` );
2026-06-01 09:29:12 +08:00
await page . locator ( "[data-page-ai-input]" ). fill ( "Task520 raw target" , { timeout : UI_TIMEOUT_MS });
await page . locator ( '[data-page-ai-action="send"]' ). click ({ timeout : UI_TIMEOUT_MS });
await page . waitForFunction (
() => ( document . querySelector ( '[data-testid="wolai-page-ai-drawer"]' ) ? . textContent || "" ). includes ( "Task520 response" ),
null ,
{ timeout : UI_TIMEOUT_MS },
);
const runs = captured . filter (( item ) => item . kind === "run" );
assert ( runs . length >= 1 , "未捕获 Page AI run payload" );
const runBody = JSON . parse ( runs [ runs . length - 1 ]. body || "{}" );
const activeEditorRef = runBody . contextRefs ? . find (( item ) => item . kind === "active_editor" );
assert ( activeEditorRef , `run payload 应包含 active_editor contextRef: ${ JSON . stringify ( runBody . contextRefs ) } ` );
assert . strictEqual ( activeEditorRef . relativePath , rawPath , `active_editor 应指向 raw resource: ${ JSON . stringify ( activeEditorRef ) } ` );
assert . strictEqual ( activeEditorRef . resourceKind , "attachment" , `raw resource contextRef 应保留 attachment resourceKind: ${ JSON . stringify ( activeEditorRef ) } ` );
assert . strictEqual ( activeEditorRef . objectIdentity , "resource:file:" + rootUri + ":" + rawPath , `active_editor objectIdentity 不应退化为 [object Object]: ${ JSON . stringify ( activeEditorRef ) } ` );
assert . strictEqual ( runBody . targetPackage ? . schema , "mnote.agent_target_package.v1" , "run payload 应携带 targetPackage" );
assert . strictEqual ( runBody . targetPackage ? . primaryTargetId , "resource:file:" + rootUri + ":" + rawPath , `targetPackage 应冻结 raw resource objectIdentity: ${ JSON . stringify ( runBody . targetPackage ) } ` );
assert . strictEqual ( runBody . targetPackage ? . objectIdentity , "resource:file:" + rootUri + ":" + rawPath , `targetPackage objectIdentity 不应退化为 [object Object]: ${ JSON . stringify ( runBody . targetPackage ) } ` );
assert . strictEqual ( runBody . targetPackage ? . resourceKind , "attachment" , `targetPackage 应保留 raw resource kind: ${ JSON . stringify ( runBody . targetPackage ) } ` );
assert . strictEqual ( runBody . targetPackage ? . currentFile ? . relativePath , rawPath , `targetPackage currentFile 应指向 raw resource: ${ JSON . stringify ( runBody . targetPackage ) } ` );
assert . strictEqual ( runBody . targetPackage ? . currentFile ? . objectIdentity , "resource:file:" + rootUri + ":" + rawPath , `currentFile objectIdentity 不应退化为 [object Object]: ${ JSON . stringify ( runBody . targetPackage ? . currentFile ) } ` );
assert ( runBody . targetPackage ? . allowedFiles ? . includes ( rawPath ), `allowedFiles 应只包含 raw resource: ${ JSON . stringify ( runBody . targetPackage ? . allowedFiles ) } ` );
2026-06-01 10:30:42 +08:00
if ( expectOcrContext ) {
assert . strictEqual ( activeEditorRef . ocrContext ? . source , "local_ocr_sidecar" , `active_editor 应携带 OCR sidecar context: ${ JSON . stringify ( activeEditorRef ) } ` );
assert . strictEqual ( activeEditorRef . ocrContext ? . ocrRootRelativePath , ocrPath , `active_editor OCR path 不正确: ${ JSON . stringify ( activeEditorRef . ocrContext ) } ` );
assert ( activeEditorRef . ocrContext ? . plainTextPreview ? . includes ( "Task520 OCR sidecar context text" ), `active_editor OCR preview 缺失: ${ JSON . stringify ( activeEditorRef . ocrContext ) } ` );
assert . strictEqual ( runBody . targetPackage ? . ocrContext ? . ocrRootRelativePath , ocrPath , `targetPackage 应携带 OCR sidecar context: ${ JSON . stringify ( runBody . targetPackage ) } ` );
assert . strictEqual ( runBody . targetPackage ? . currentFile ? . ocrRootRelativePath , ocrPath , `currentFile 应携带 OCR sidecar path: ${ JSON . stringify ( runBody . targetPackage ? . currentFile ) } ` );
}
2026-06-01 09:29:12 +08:00
const screenshot = await saveScreenshot ( page , "01-raw-resource-target" );
2026-06-01 10:30:42 +08:00
const result = { ok : true , task : TASK , baseUrl : BASE_URL , root , rootUri , documentId , rawPath , ocrPath : expectOcrContext ? ocrPath : "" , screenshot , captured };
2026-06-01 09:29:12 +08:00
fs . writeFileSync ( RESULT_PATH , ` ${ JSON . stringify ( result , null , 2 ) } \n` , "utf8" );
console . log ( JSON . stringify ( result , null , 2 ));
} catch ( error ) {
caughtError = error ;
await saveScreenshot ( page , "failure" ). catch (() => undefined );
} finally {
await page . close (). catch (() => undefined );
await context . close (). catch (() => undefined );
await browser . close (). catch (() => undefined );
2026-06-01 10:30:42 +08:00
cleanupRoot ( root );
2026-06-01 09:29:12 +08:00
}
if ( caughtError ) {
throw caughtError ;
}
}
if ( require . main === module ) {
main (). catch (( error ) => {
console . error ( error );
process . exit ( 1 );
});
}