2026-07-11 20:39:18 +08:00
#!/usr/bin/env node
"use strict" ;
2026-07-28 17:04:27 +08:00
const { loginViaAuthForm } = require ( './lib/browser-auth-login' );
2026-07-11 20:39:18 +08:00
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 = process . env . MNOTE_UI_BASE_URL || "http://127.0.0.1:3000" ;
const TIMEOUT = Number . parseInt ( process . env . UI_TIMEOUT_MS || "120000" , 10 );
const STAMP = Date . now ();
const OUT = process . env . MNOTE_PI_WARMUP_BINDING_OUT || path . join ( os . tmpdir (), `mnote-pi-warmup-binding- ${ STAMP } ` );
const ROOT_PATH = process . env . MNOTE_PI_WARMUP_BINDING_ROOT_PATH || "/mnt/Data1T/Mnote_data/users/mnote-e2e/workspaces/my-space" ;
const ROOT_URI = process . env . MNOTE_PI_WARMUP_BINDING_ROOT_URI || `file:// ${ ROOT_PATH } ` ;
const WORKSPACE_ID = process . env . MNOTE_PI_WARMUP_BINDING_WORKSPACE_ID || "local-ws:mnote-e2e:my-space" ;
const PAGE_PATH = `pi-warmup-binding- ${ STAMP } .md` ;
const CHROMIUM_EXECUTABLE = process . env . PLAYWRIGHT_CHROMIUM_EXECUTABLE
|| ( fs . existsSync ( "/usr/bin/chromium-browser" ) ? "/usr/bin/chromium-browser" : "" )
|| ( fs . existsSync ( "/usr/bin/chromium" ) ? "/usr/bin/chromium" : "" )
|| ( fs . existsSync ( "/usr/bin/google-chrome" ) ? "/usr/bin/google-chrome" : "" );
async function requestJson ( page , url , options = {}) {
const response = await page . request . fetch ( ` ${ BASE }${ url } ` , {
... options ,
headers : {
accept : "application/json" ,
"content-type" : "application/json" ,
...( options . headers || {}),
},
timeout : options . timeout || TIMEOUT ,
});
const text = await response . text ();
let body = {};
try {
body = text ? JSON . parse ( text ) : {};
} catch {
body = { raw : text };
}
return { response , body };
}
async function quickLogin ( page ) {
2026-07-28 17:04:27 +08:00
// 7-76 P0: 标准表单登录(无测试快速登录按钮)
const base =
( typeof BASE_URL !== "undefined" && BASE_URL ) ||
( typeof baseUrl !== "undefined" && baseUrl ) ||
process . env . MNOTE_UI_BASE_URL ||
"http://127.0.0.1:3000" ;
const timeout =
( typeof UI_TIMEOUT_MS !== "undefined" && UI_TIMEOUT_MS ) ||
( typeof TIMEOUT !== "undefined" && TIMEOUT ) ||
30_000 ;
if ( ! String ( page . url () || "" ). includes ( "/auth" )) {
await page . goto ( String ( base ). replace ( /\/+$/ , "" ) + "/auth" , {
waitUntil : "commit" ,
timeout ,
});
}
await loginViaAuthForm ( page , {
baseUrl : base ,
timeoutMs : timeout ,
gotoAuth : false ,
});
await page
. waitForURL (( url ) => ! String ( url ). includes ( "/auth" ), { timeout })
. catch (() => {});
2026-07-11 20:39:18 +08:00
}
async function cleanupPreviousSmokeSession ( page ) {
const status = await requestJson ( page , "/api/page-ai/pi/status" );
assert ( status . response . ok (), `status before cleanup should be OK, got ${ status . response . status () } ` );
const session = status . body . session || {};
const sessionId = status . body . sessionId || session . sessionId ;
const pagePath = String ( session . pagePath || "" );
if ( ! sessionId || ! pagePath . startsWith ( "pi-warmup-binding-" )) return ;
await requestJson ( page , "/api/page-ai/pi/abort" , {
method : "POST" ,
data : { sessionId },
}). catch (() => null );
await requestJson ( page , `/api/page-ai/pi/sessions/ ${ encodeURIComponent ( sessionId ) } ` , {
method : "DELETE" ,
}). catch (() => null );
}
async function main () {
fs . mkdirSync ( OUT , { recursive : true });
fs . mkdirSync ( ROOT_PATH , { recursive : true });
fs . writeFileSync ( path . join ( ROOT_PATH , PAGE_PATH ), "# Pi warmup binding\n\nWarmup binding smoke.\n" , "utf8" );
const browser = await chromium . launch ({
headless : process . env . MNOTE_PI_WARMUP_BINDING_HEADED === "1" ? false : true ,
executablePath : CHROMIUM_EXECUTABLE || undefined ,
});
const context = await browser . newContext ({ viewport : { width : 1440 , height : 960 } });
await context . addInitScript (( payload ) => {
window . __MNOTE_TEST_PAGE_CONTEXT__ = payload ;
}, {
rootUri : ROOT_URI ,
workspaceId : WORKSPACE_ID ,
pagePath : PAGE_PATH ,
pageTitle : "Pi warmup binding" ,
});
const page = await context . newPage ();
const requests = [];
const result = {
base : BASE ,
out : OUT ,
rootUri : ROOT_URI ,
workspaceId : WORKSPACE_ID ,
pagePath : PAGE_PATH ,
statusBefore : null ,
statusAfter : null ,
startRequest : null ,
startBeforeSend : false ,
coldStartToastVisible : false ,
};
try {
await quickLogin ( page );
await cleanupPreviousSmokeSession ( page );
const statusBefore = await page . request . get ( ` ${ BASE } /api/page-ai/pi/status` );
assert ( statusBefore . ok (), `status before should be OK, got ${ statusBefore . status () } ` );
result . statusBefore = await statusBefore . json ();
assert . equal ( result . statusBefore . warmupRunning , true , "dev:hot warmup runtime should be running before UI send" );
assert . equal ( result . statusBefore . sessionId || null , null , "warmup session must not be exposed as current page session" );
await page . route ( "**/api/page-ai/pi/send" , async ( route ) => {
const request = route . request ();
requests . push ({ url : request . url (), body : request . postDataJSON () });
await route . fulfill ({
status : 200 ,
contentType : "application/json" ,
body : JSON . stringify ({
ok : true ,
schema : "mnote.page_ai_pi.send.v1" ,
sessionId : request . postDataJSON (). sessionId ,
accepted : true ,
intercepted : true ,
}),
});
});
const documentId = `local-md: ${ PAGE_PATH . replaceAll ( "/" , "~2F" ) } ` ;
await page . goto (
` ${ BASE } /documents/ ${ encodeURIComponent ( documentId ) } ?sourceKind=local_folder&rootUri= ${ encodeURIComponent ( ROOT_URI ) } &workspaceId= ${ encodeURIComponent ( WORKSPACE_ID ) } &path= ${ encodeURIComponent ( PAGE_PATH ) } ` ,
{ waitUntil : "commit" , timeout : TIMEOUT },
);
await page . locator ( "[data-page-ai-pi-lab-launcher]" ). waitFor ({ state : "visible" , timeout : TIMEOUT });
const startResponsePromise = page . waitForResponse (
( response ) => response . url (). includes ( "/api/page-ai/pi/start" ) && response . request (). method () === "POST" ,
{ timeout : TIMEOUT },
);
await page . locator ( "[data-page-ai-pi-lab-launcher]" ). click ();
await page . locator ( '[data-page-ai-pi-lab="drawer"]' ). waitFor ({ state : "visible" , timeout : TIMEOUT });
const startResponse = await startResponsePromise ;
result . startBeforeSend = true ;
assert ( startResponse . ok (), `start response should be OK, got ${ startResponse . status () } ` );
const startBody = await startResponse . json ();
result . startRequest = startResponse . request (). postDataJSON ();
assert . equal ( startBody . ok , true , "start body should be ok" );
assert ( startBody . session ? . sessionId , "start should return a current page session" );
assert ( ! String ( startBody . session . sessionId ). startsWith ( "pi_lab_dev_warm_" ), "current page session should not reuse warmup prompt session id" );
assert . equal ( startBody . session . pagePath , PAGE_PATH , "current page session should bind current page path" );
await page . waitForFunction (
() => document . querySelector ( "[data-page-ai-pi-lab-status-text]" ) ? . textContent ? . includes ( "ready" )
|| document . querySelector ( "[data-page-ai-pi-lab-status-text]" ) ? . textContent ? . includes ( "streaming" ),
null ,
{ timeout : TIMEOUT },
);
await page . locator ( "[data-page-ai-pi-lab-input]" ). fill ( "warmup binding smoke" );
await page . locator ( "[data-page-ai-pi-lab-btn-send]" ). click ();
result . coldStartToastVisible = await page . locator ( "[data-page-ai-pi-lab-toast]" , { hasText : "正在绑定当前页 Pi 会话,完成后自动发送" }). isVisible ({ timeout : 500 }). catch (() => false );
assert . equal ( result . coldStartToastVisible , false , "send should not show current-page binding toast after drawer-open prestart" );
const sendWaitStartedAt = Date . now ();
while ( requests . length === 0 && Date . now () - sendWaitStartedAt < 5000 ) {
await page . waitForTimeout ( 100 );
}
assert . equal ( requests . length , 1 , "send should be intercepted once after runtime binding" );
assert . equal ( requests [ 0 ]. body . sessionId , startBody . session . sessionId , "send should use bound current page session" );
const statusAfter = await page . request . get ( ` ${ BASE } /api/page-ai/pi/status` );
assert ( statusAfter . ok (), `status after should be OK, got ${ statusAfter . status () } ` );
result . statusAfter = await statusAfter . json ();
assert . equal ( result . statusAfter . warmupRunning , true , "warmup diagnostics should remain visible after current session starts" );
assert . equal ( result . statusAfter . sessionId , startBody . session . sessionId , "status should now expose the current page session" );
fs . writeFileSync ( path . join ( OUT , "result.json" ), JSON . stringify ( result , null , 2 ), "utf8" );
console . log ( `✅ Pi warmup binding browser smoke passed: ${ path . join ( OUT , "result.json" ) } ` );
} finally {
await browser . close ();
}
}
main (). catch (( error ) => {
console . error ( error );
process . exit ( 1 );
});