2026-10-28 18:54:57 +02:00
/** Offline synthetic UI + actual Electron preload smoke. No providers/accounts.
* Run: node scripts/smoke-file-paths.mjs
* App/profile/auth/OS-backed fixture files are temporary; only this script's
* processes are stopped. Browser requests outside the owned origin are blocked.
*/
import assert from "node:assert/strict" ;
import { spawn } from "node:child_process" ;
import { once } from "node:events" ;
import fs from "node:fs" ;
import net from "node:net" ;
import { createRequire } from "node:module" ;
import os from "node:os" ;
import path from "node:path" ;
import puppeteer from "puppeteer-core" ;
const root = path . resolve ( import . meta . dirname , ".." );
const home = fs . mkdtempSync ( path . join ( os . tmpdir (), "lh-file-paths-" ));
const require = createRequire ( import . meta . url );
const checks = [];
const errors = [];
const visualFailures = [];
const coverageFailures = [];
const posts = [];
const blocked = [];
const networkFailures = [];
const consoleErrors = [];
const children = [];
const browsers = [];
const screenshots = [];
let serverLogs = "" ;
let electronLogs = "" ;
let page ;
let origin ;
let shuttingDown = false ;
const fixtureCatalog = (() => {
const model = { id : "synthetic" , name : "Synthetic (no calls)" , provider : "fixture" , input : [ "text" ], reasoning : false , supportedLevels : [ "off" ], cost : { input : 0 , output : 0 , cacheRead : 0 , cacheWrite : 0 }, contextWindow : 8192 , maxTokens : 1024 };
const slot = { model : { provider : model . provider , id : model . id }, thinkingLevel : "off" };
return { models : [ model ], providers : [{ id : "fixture" , label : "Fixture" , keyPresent : true }], defaults : null , slots : { strong : slot , cheap : slot , vision : slot }, rolePins : {} };
})();
async function freePort () {
const listener = net . createServer ();
listener . listen ( 0 , "127.0.0.1" );
await once ( listener , "listening" );
const port = listener . address (). port ;
await new Promise ( resolve => listener . close ( resolve ));
return port ;
}
async function stop ( child ) {
if ( ! child || child . exitCode !== null || child . signalCode !== null ) return ;
const exited = once ( child , "exit" );
child . kill ( "SIGTERM" );
const timer = setTimeout (() => child . kill ( "SIGKILL" ), 5000 ); timer . unref ();
await exited ; clearTimeout ( timer );
}
async function startup ( child , getLogs , marker , timeout = 90000 ) {
await new Promise (( resolve , reject ) => {
const timer = setTimeout (() => finish ( new Error ( `Startup timed out: ${ getLogs () } ` )), timeout );
const inspect = () => { if ( marker . test ( getLogs ())) finish (); };
const exited = code => finish ( new Error ( `Process exited ${ code } : ${ getLogs () } ` ));
function finish ( error ) {
clearTimeout ( timer ); child . stdout . off ( "data" , inspect ); child . stderr . off ( "data" , inspect ); child . off ( "exit" , exited );
if ( error ) reject ( error ); else resolve ();
}
child . stdout . on ( "data" , inspect ); child . stderr . on ( "data" , inspect ); child . once ( "exit" , exited ); inspect ();
});
}
async function instrument ( target ) {
target . setDefaultTimeout ( 20000 );
target . on ( "pageerror" , error => { if ( ! shuttingDown ) errors . push ( error . message ); });
target . on ( "requestfailed" , request => { if ( ! shuttingDown ) networkFailures . push ({ url : request . url (), error : request . failure () ? . errorText }); });
target . on ( "console" , message => { if ( message . type () === "error" ) consoleErrors . push ( message . text ()); });
await target . setRequestInterception ( true );
target . on ( "request" , async request => {
try {
const url = new URL ( request . url ());
if ( url . protocol === "data:" || url . protocol === "about:" ) return await request . continue ();
if ( url . origin !== origin ) { blocked . push ( request . url ()); return await request . abort (); }
const respond = body => request . respond ({ status : 200 , contentType : "application/json" , body : JSON . stringify ( body ) });
if ( url . pathname === "/api/models" ) return await respond ( fixtureCatalog );
if ( url . pathname === "/api/events" ) return await request . respond ({ status : 204 });
// Never forward agent, answer or import requests, even if a UI bug
// submits while dropping. Record their complete synthetic bodies.
if ( request . method () === "POST" && ( /^\/api\/(chat|answer)(\/|$)/ . test ( url . pathname ) || /\/(answer|import|turn|retry)(\/|$)/ . test ( url . pathname ))) {
posts . push ({ pathname : url . pathname , body : JSON . parse ( request . postData () || "{}" ) });
return await request . respond ({ status : 503 , contentType : "application/json" , body : JSON . stringify ({ code : "models_not_configured" , missing : [ "strong" ] }) });
}
if ( /^\/api\/chats\/[^/]+\/stream$/ . test ( url . pathname )) return await request . respond ({ status : 204 });
return await request . continue ();
} catch ( error ) {
if ( ! shuttingDown && ! /Target closed|Session closed|Invalid InterceptionId|already handled/ . test ( String ( error ))) errors . push ( String ( error ));
}
});
}
const inputSelector = '[data-file-path-input]' ;
async function open ( target , route ) {
await target . goto ( origin + route , { waitUntil : "domcontentloaded" , timeout : 120000 });
await target . waitForSelector ( inputSelector );
await target . waitForFunction (() => ! document . querySelector ( '[data-testid="setup-cta"], [data-testid="setup-cta-keys"]' ));
await target . waitForFunction (() => document . querySelector ( '[data-testid="file-path-drop-zone"]' ));
}
async function read ( target , selector = inputSelector ) {
return target . $eval ( selector , element => [... element . childNodes ]. map ( node => node . nodeType === 1 ? node . getAttribute ( "data-file-path-source" ) ?? node . textContent : node . textContent ). join ( "" ));
}
async function expectValue ( target , expected , selector = inputSelector ) {
await target . waitForFunction (( selector , expected ) => {
const element = document . querySelector ( selector );
return element && [... element . childNodes ]. map ( node => node . nodeType === 1 ? node . getAttribute ( "data-file-path-source" ) ?? node . textContent : node . textContent ). join ( "" ) === expected ;
}, {}, selector , expected );
}
async function fill ( target , value , selector = inputSelector ) {
await target . $eval ( selector , ( element , value ) => {
element . focus (); element . textContent = value ;
const selection = document . getSelection (); selection . collapse ( element , element . childNodes . length );
element . dispatchEvent ( new InputEvent ( "input" , { bubbles : true , inputType : "insertText" }));
}, value );
await expectValue ( target , value , selector );
}
async function caret ( target , offset , selector = inputSelector ) {
await target . $eval ( selector , ( element , offset ) => {
element . focus (); let rest = offset ;
for ( const node of element . childNodes ) {
const source = node . nodeType === 1 ? node . getAttribute ( "data-file-path-source" ) : null ;
const length = source ? . length ?? node . textContent . length ;
if ( rest <= length ) {
if ( source !== null ) {
const index = [... element . childNodes ]. indexOf ( node );
document . getSelection (). collapse ( element , index + ( rest > 0 ? 1 : 0 ));
} else document . getSelection (). collapse ( node , rest );
document . dispatchEvent ( new Event ( "selectionchange" )); return ;
}
rest -= length ;
}
document . getSelection (). collapse ( element , element . childNodes . length );
document . dispatchEvent ( new Event ( "selectionchange" ));
}, offset );
}
async function shortcut ( target , key , shift = false ) {
await target . keyboard . down ( "Meta" ); if ( shift ) await target . keyboard . down ( "Shift" );
await target . keyboard . press ( key );
if ( shift ) await target . keyboard . up ( "Shift" ); await target . keyboard . up ( "Meta" );
}
async function clipboardCommand ( target , command , key , modifiers = 4 ) {
// Headless Chromium does not run the macOS menu accelerator for Meta+C/V.
// CDP's native edit command triggers the real browser clipboard event.
const session = await target . createCDPSession ();
try { await session . send ( "Input.dispatchKeyEvent" , { type : "rawKeyDown" , key : key ?? ( command === "copy" ? "c" : "v" ), modifiers , commands : [ command ] }); }
finally { await session . detach (); }
}
async function syntheticDrop ( target , paths , selector = '[data-testid="file-path-drop-zone"]' ) {
return target . $eval ( selector , ( zone , paths ) => {
const transfer = new DataTransfer ();
window . __syntheticPaths = new WeakMap ();
window . lh = { platform : "darwin" , getPathForFile ( file ) { return window . __syntheticPaths . get ( file ) ?? "" ; } };
for ( const [ index , path ] of paths . entries ()) {
const file = new File ([ "never read" ], `synthetic-display- ${ index } .pdf` , { type : "application/pdf" });
window . __syntheticPaths . set ( file , path );
for ( const method of [ "text" , "arrayBuffer" , "stream" ]) Object . defineProperty ( file , method , { value () { throw new Error ( "File bytes must not be read" ); } });
transfer . items . add ( file );
}
const event = new DragEvent ( "drop" , { bubbles : true , cancelable : true , dataTransfer : transfer });
zone . dispatchEvent ( event ); return event . defaultPrevented ;
}, paths );
}
async function blur ( target ) { await target . evaluate (() => { document . activeElement ? . blur (); }); }
function seedChatFixtures () {
const usage = { input : 0 , output : 0 , cacheRead : 0 , cacheWrite : 0 , totalTokens : 0 , cost : { input : 0 , output : 0 , cacheRead : 0 , cacheWrite : 0 , total : 0 } };
const transcript = Array . from ({ length : 100 }, ( _ , index ) => `Synthetic lesson paragraph ${ index } : one and one make two. These are offline fixture notes.` ). join ( "\n\n" );
const assistant = ( id , content ) => ({ id , role : "assistant" , api : "anthropic-messages" , provider : "anthropic" , model : "synthetic" , timestamp : 1 , stopReason : "toolUse" , usage , content });
const call = ( id , name , args ) => ({ type : "toolCall" , id , name , arguments : args });
const result = ( id , toolCallId , toolName , details ) => ({ id , role : "toolResult" , toolCallId , toolName , content : [{ type : "text" , text : "Synthetic tool completed." }], ...( details ? { details } : {}), isError : false , timestamp : 2 });
const quiz = { type : "check" , nodeId : "addition" , question : "What is 1 + 1?" , options : [{ value : "two" , label : "2" }, { value : "three" , label : "3" }], explanation : "Adding one to one gives two." , correctAnswer : "two" };
const records = [
{ id : "path_teach" , messages : [ assistant ( "m_probe" , [{ type : "text" , text : transcript }, call ( "call_probe" , "probe_done" , { assessment : { strands : [{ strand : "Addition" , assessment : "Ready for a check." }] } })]), result ( "m_probe_result" , "call_probe" , "probe_done" , { ok : true }), assistant ( "m_quiz" , [ call ( "call_quiz" , "quiz" , quiz )])] },
{ id : "path_probe" , messages : [ assistant ( "m_probe_question" , [{ type : "text" , text : transcript }, call ( "call_probe_question" , "probe" , quiz )])] },
{ id : "path_practice" , kind : "practice" , messages : [ assistant ( "m_problem" , [{ type : "text" , text : transcript }, call ( "call_problem" , "problem" , { statement : "Compute 1 + 1. Explain your reasoning." , mode : "recall" , modelSolution : "1 + 1 = 2." })]), result ( "m_problem_result" , "call_problem" , "problem" )] },
];
for ( const record of records ) {
record . messages . unshift ({ id : "sent_user" , role : "user" , content : "Attached `/synthetic/transcript notes.pdf`" , timestamp : 0 });
fs . writeFileSync ( path . join ( home , "data" , "chats" , ` ${ record . id } .json` ), JSON . stringify ({ version : 1 , title : `Synthetic ${ record . id } ` , createdAt : "2026-10-28T00:00:00.000Z" , updatedAt : "2026-10-28T00:00:00.000Z" , ... record }));
}
}
async function foldDock ( target , barSelector ) {
// Let growth/geometry muting settle, then use a real wheel gesture so
// stick-to-bottom records user intent instead of a programmatic clamp.
// The explicit reveal owns a bounded 2s fold-suppression window.
await target . evaluate (() => new Promise ( resolve => setTimeout ( resolve , 2250 )));
const log = await target . $ ( '[role="log"]' ); const box = await log . boundingBox ();
await target . mouse . move ( box . x + box . width / 2 , box . y + 100 );
// Multiple real wheel frames let stick-to-bottom publish isAtBottom
// before DockOverlay evaluates the next away-from-bottom scroll event.
for ( const deltaY of [ - 420 , - 420 , - 840 ]) {
await target . mouse . wheel ({ deltaY });
await target . evaluate (() => new Promise ( resolve => setTimeout ( resolve , 100 )));
}
try { await target . waitForSelector ( barSelector , { visible : true , timeout : 5000 }); return true ; }
catch {
coverageFailures . push ({ url : target . url (), case : barSelector , message : "Native wheel did not expose collapsed bar" , scrollers : await target . evaluate (() => [... document . querySelectorAll ( '[role="log"], [role="log"] *' )]. filter ( element => element . scrollHeight > element . clientHeight + 100 && getComputedStyle ( element ). overflowY !== "visible" ). map ( element => ({ tag : element . tagName , className : element . className , scrollTop : element . scrollTop , scrollHeight : element . scrollHeight , clientHeight : element . clientHeight }))) });
return false ;
}
}
try {
fs . mkdirSync ( path . join ( home , "data" , "chats" ), { recursive : true });
fs . writeFileSync ( path . join ( home , "auth.json" ), "{}\n" );
seedChatFixtures ();
const { getFontAxes } = require ( "next/dist/compiled/@next/font/dist/google/get-font-axes.js" );
const { getGoogleFontsUrl } = require ( "next/dist/compiled/@next/font/dist/google/get-google-fonts-url.js" );
const fonts = Object . fromEntries ([ "JetBrains Mono" , "Source Sans 3" ]. map ( family => [ getGoogleFontsUrl ( family , getFontAxes ( family , [ "variable" ], [ "normal" ]), "swap" ), `@font-face { font-family: ' ${ family } '; src: local('Arial'); font-weight: 100 900; }` ]));
fs . writeFileSync ( path . join ( home , "fonts.cjs" ), `module.exports = ${ JSON . stringify ( fonts ) } ;\n` );
const port = await freePort (); origin = `http://127.0.0.1: ${ port } ` ;
const env = { PATH : process . env . PATH , HOME : home , TMPDIR : home , NODE_ENV : "development" , LH_DATA_DIR : path . join ( home , "data" ), LH_AUTH_FILE : path . join ( home , "auth.json" ), LH_IGNORE_ENV_KEYS : "1" , NEXT_TELEMETRY_DISABLED : "1" , NEXT_FONT_GOOGLE_MOCKED_RESPONSES : path . join ( home , "fonts.cjs" ) };
const server = spawn ( process . execPath , [ "node_modules/next/dist/bin/next" , "dev" , "--hostname" , "127.0.0.1" , "--port" , String ( port )], { cwd : root , env , stdio : [ "ignore" , "pipe" , "pipe" ] }); children . push ( server );
const captureServer = data => { serverLogs = ( serverLogs + data ). slice ( - 28000 ); };
server . stdout . on ( "data" , captureServer ); server . stderr . on ( "data" , captureServer );
await startup ( server , () => serverLogs , /Ready in/ );
if ( ! process . argv . includes ( "--desktop-only" )) {
const chrome = await puppeteer . launch ({ executablePath : process . env . LH_TEST_CHROME || "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" , headless : true , userDataDir : path . join ( home , "chrome-profile" ), env , args : [ "--disable-background-networking" , "--disable-sync" , "--no-first-run" , "--disable-default-apps" ] }); browsers . push ( chrome );
page = await chrome . newPage (); await page . setViewport ({ width : 1280 , height : 900 }); await instrument ( page );
await chrome . defaultBrowserContext (). overridePermissions ( origin , [ "clipboard-read" , "clipboard-write" , "clipboard-sanitized-write" ]);
const token = "`/synthetic/course notes.pdf`" ;
for ( const route of [ "/" , "/chat/new" ]) {
await open ( page , route );
// Sidebar drops prevent browser navigation but must not stage paths or
// activate the right-content attachment indicator.
assert . equal ( await page . $eval ( "aside" , sidebar => {
const transfer = new DataTransfer (); transfer . items . add ( new File ([ "not uploaded" ], "sidebar.pdf" ));
const event = new DragEvent ( "drop" , { bubbles : true , cancelable : true , dataTransfer : transfer }); sidebar . dispatchEvent ( event ); return event . defaultPrevented ;
}), true );
assert . equal ( await page . $ ( '[data-testid="file-path-drop-indicator"]' ), null );
await expectValue ( page , "" );
// Browser mode has no bridge: prevent file navigation with a clear notice.
const prevented = await page . $eval ( '[data-testid="file-path-drop-zone"]' , zone => {
const transfer = new DataTransfer (); transfer . items . add ( new File ([ "not uploaded" ], "notes.pdf" ));
const event = new DragEvent ( "drop" , { bubbles : true , cancelable : true , dataTransfer : transfer }); zone . dispatchEvent ( event ); return event . defaultPrevented ;
});
assert . equal ( prevented , true );
await page . waitForFunction (() => document . querySelector ( '[role="status"]' ) ? . textContent . includes ( "desktop app" ));
await fill ( page , "read " ); await page . keyboard . type ( token + " now" );
await expectValue ( page , `read ${ token } now` );
assert . equal ( await page . $$eval ( '[data-file-path-input] [role="img"]' , elements => elements . length ), 1 );
// Native browser arrows cross the atomic host without entering its label.
await caret ( page , 5 ); await page . keyboard . press ( "ArrowRight" );
assert . equal ( await page . evaluate (() => !! document . getSelection (). anchorNode ? . parentElement ? . closest ( '[role="img"]' )), false );
await page . keyboard . press ( "Backspace" ); await expectValue ( page , "read now" );
await shortcut ( page , "KeyZ" ); await expectValue ( page , `read ${ token } now` );
await shortcut ( page , "KeyZ" , true ); await expectValue ( page , "read now" );
await shortcut ( page , "KeyZ" ); await caret ( page , 5 ); await page . keyboard . press ( "Delete" ); await expectValue ( page , "read now" );
await shortcut ( page , "KeyZ" ); await expectValue ( page , `read ${ token } now` );
// Select the actual rendered label; copy must emit the full source token.
await page . $eval ( '[data-file-path-input] [role="img"] span' , label => {
const range = document . createRange (); range . selectNodeContents ( label ); const selection = document . getSelection (); selection . removeAllRanges (); selection . addRange ( range ); document . dispatchEvent ( new Event ( "selectionchange" ));
});
await clipboardCommand ( page , "copy" );
assert . equal ( await page . evaluate (() => navigator . clipboard . readText ()), token );
await clipboardCommand ( page , "cut" , "Delete" , 8 );
await expectValue ( page , "read now" );
assert . equal ( await page . evaluate (() => navigator . clipboard . readText ()), token , "Shift+Delete is native cut, not editor-only deletion" );
await shortcut ( page , "KeyZ" ); await expectValue ( page , `read ${ token } now` );
// Clipboard image-only input must not erase the current selected path.
await page . $eval ( '[data-file-path-input]' , editor => {
const host = editor . querySelector ( '[data-file-path-source]' ); const range = document . createRange (); range . selectNode ( host ); const selection = document . getSelection (); selection . removeAllRanges (); selection . addRange ( range ); document . dispatchEvent ( new Event ( "selectionchange" ));
const data = new DataTransfer (); data . items . add ( new File ([ "image fixture" ], "image.png" , { type : "image/png" }));
editor . dispatchEvent ( new ClipboardEvent ( "paste" , { bubbles : true , cancelable : true , clipboardData : data }));
});
await expectValue ( page , `read ${ token } now` );
// Pasting HTML-bearing clipboard content is plain text, at the range.
await page . evaluate (() => navigator . clipboard . writeText ( '<img src=x onerror="alert(1)">\n中' ));
await clipboardCommand ( page , "paste" );
await expectValue ( page , 'read <img src=x onerror="alert(1)">\n中 now' );
assert . equal ( await page . $ ( '[data-file-path-input] img' ), null );
await fill ( page , "readthis" ); await caret ( page , 4 );
assert . equal ( await syntheticDrop ( page , [ "/synthetic/one.pdf" , "~/two`final.pdf" ]), true );
await expectValue ( page , "read `/synthetic/one.pdf` ``~/two`final.pdf`` this" );
assert . equal ( posts . length , 0 , "typing/paste/drop must never submit" );
await fill ( page , "one visible line" ); await caret ( page , "one visible line" . length );
const beforeNewline = await page . $eval ( inputSelector , element => element . getBoundingClientRect (). height );
await page . keyboard . down ( "Shift" ); await page . keyboard . press ( "Enter" ); await page . keyboard . up ( "Shift" );
await expectValue ( page , "one visible line\n" );
const afterNewline = await page . $eval ( inputSelector , element => {
const range = document . getSelection (). getRangeAt ( 0 ); const rect = range . getBoundingClientRect ();
return { height : element . getBoundingClientRect (). height , caret : { top : rect . top , height : rect . height }, html : element . innerHTML };
});
// Record this independently so a visual editor regression does not hide
// the higher-priority actual Electron/target-routing evidence below.
try { assert . ok ( afterNewline . height > beforeNewline , `Trailing newline must occupy a visible blank line: ${ JSON . stringify ({ beforeNewline , afterNewline } )}` ); }
catch ( error ) { visualFailures . push ({ route , message : error . message }); }
await page . keyboard . type ( "second line" );
const afterTyping = await read ( page );
try { assert . equal ( afterTyping , "one visible line\nsecond line" , "Typing after Shift+Enter must stay after the newline" ); }
catch ( error ) { visualFailures . push ({ route , message : error . message }); }
assert . equal ( posts . length , 0 , "Shift+Enter is a newline, never submit" );
await fill ( page , "composer fallback" ); await blur ( page );
await syntheticDrop ( page , [ "/synthetic/default.pdf" ]);
await expectValue ( page , "composer fallback `/synthetic/default.pdf`" );
// Small viewport: wrapped multiline content stays scrollable inside its editor.
await page . setViewport ({ width : 900 , height : 520 });
await fill ( page , ( ` ${ token } a long synthetic line wrapping at narrow widths.\n` ). repeat ( 24 ));
const sizing = await page . $eval ( inputSelector , element => ({ height : element . clientHeight , scrollHeight : element . scrollHeight , overflowY : getComputedStyle ( element ). overflowY , bodyWidth : document . body . scrollWidth , viewport : innerWidth }));
assert . ok ( sizing . scrollHeight > sizing . height && sizing . height > 0 , JSON . stringify ( sizing ));
assert . ok ([ "auto" , "scroll" ]. includes ( sizing . overflowY ));
assert . ok ( sizing . bodyWidth <= sizing . viewport + 1 , JSON . stringify ( sizing ));
const screenshot = path . join ( home , `chrome- ${ route === "/" ? "home" : "new" } .png` ); await page . screenshot ({ path : screenshot }); screenshots . push ({ surface : route , bytes : fs . statSync ( screenshot ). size , sizing });
await page . setViewport ({ width : 1280 , height : 900 });
checks . push ( ` ${ route } : native typing/chips/arrows/deletion/undo-redo/full-source clipboard/plaintext paste/caret drop/newlines/wrap-scroll` );
}
// Only an explicit Enter may initiate a send; agent endpoint is refused.
await fill ( page , "explicit synthetic send `/synthetic/one.pdf`" );
await page . keyboard . press ( "Enter" );
await page . waitForFunction (() => /^ \ / chat \ / chat_ / . test ( location . pathname ));
await new Promise (( resolve , reject ) => {
const deadline = Date . now () + 15000 ;
const inspect = () => { if ( posts . length ) resolve (); else if ( Date . now () > deadline ) reject ( new Error ( "Explicit Enter did not send" )); else setTimeout ( inspect , 50 ); }; inspect ();
});
assert . equal ( posts . length , 1 ); assert . equal ( posts [ 0 ]. body . input . text , "explicit synthetic send `/synthetic/one.pdf`" );
checks . push ( "Explicit Enter submitted exactly one full-source prompt; endpoint intercepted/refused" );
// These raw JSON fixtures are synthetic native records, never store imports.
const reading = '[data-testid="reading-thinking-field"] [data-file-path-input]' ;
const question = '[data-testid="thinking-field"] [data-file-path-input]' ;
const practice = '[data-testid="practice-thinking-field"] [data-file-path-input]' ;
await open ( page , "/chat/path_teach" );
await page . waitForSelector ( '[data-testid="teach-check-reveal"]' , { visible : true });
assert . equal ( await page . $eval ( '[data-testid="user-message"] [role="img"]' , chip => chip . title ), "/synthetic/transcript notes.pdf" );
assert . equal ( await page . $eval ( '[data-testid="user-message"] [role="img"]' , chip => chip . textContent ), "transcript notes.pdf" );
checks . push ( "Seeded sent learner message renders a filename chip with full-path title; raw record retains quoted full path" );
await blur ( page ); await syntheticDrop ( page , [ "/synthetic/reading.pdf" ]);
await expectValue ( page , "`/synthetic/reading.pdf`" , reading );
assert . equal ( await read ( page , question ), "" );
checks . push ( "Unrevealed teaching check: unfocused drop lands in reading-thinking via dictation registry" );
await page . locator ( '[data-testid="teach-check-reveal"]' ). click ();
await page . waitForSelector ( question , { visible : true });
await fill ( page , "AB" , reading ); await caret ( page , 1 , reading );
await syntheticDrop ( page , [ "/synthetic/focused-reading.pdf" ]);
await expectValue ( page , "A `/synthetic/focused-reading.pdf` B" , reading );
assert . equal ( await read ( page , question ), "" , "focused reading field overrides opened question default" );
await blur ( page ); await syntheticDrop ( page , [ "/synthetic/question.pdf" ]);
await expectValue ( page , "`/synthetic/question.pdf`" , question );
checks . push ( "Opened teaching check: current reading focus wins; otherwise default routes to question-thinking" );
if ( await foldDock ( page , '[data-testid="question-dock-bar"]' )) {
await blur ( page ); await syntheticDrop ( page , [ "/synthetic/collapsed-question.pdf" ]);
await expectValue ( page , "`/synthetic/question.pdf` `/synthetic/collapsed-question.pdf`" , question );
await page . locator ( '[data-testid="question-dock-bar"]' ). click ();
await page . waitForSelector ( question , { visible : true });
assert . equal ( await read ( page , question ), "`/synthetic/question.pdf` `/synthetic/collapsed-question.pdf`" );
checks . push ( "Collapsed revealed teaching dock keeps question-thinking claim/state through expansion" );
}
await open ( page , "/chat/path_probe" ); await page . waitForSelector ( question , { visible : true });
await blur ( page ); await syntheticDrop ( page , [ "/synthetic/probe.pdf" ]);
await expectValue ( page , "`/synthetic/probe.pdf`" , question );
checks . push ( "Probe-phase pending question default routes to question-thinking" );
await open ( page , "/chat/path_practice" ); await page . waitForSelector ( practice , { visible : true });
await blur ( page ); await syntheticDrop ( page , [ "/synthetic/practice.pdf" ]);
await expectValue ( page , "`/synthetic/practice.pdf`" , practice );
checks . push ( "Expanded practice thinking is the unfocused drop default" );
if ( await foldDock ( page , '[data-testid="practice-dock-bar"]' )) {
await blur ( page ); await syntheticDrop ( page , [ "/synthetic/collapsed-practice.pdf" ]);
// Practice keeps the owning hook/state mounted, but hides its field
// by returning the bar. Inspect the controlled text after expansion.
2026-10-28 19:52:07 +02:00
assert . equal ( await page . $ ( '[role="status"]' ), null , "successful attachment has no confirmation bubble" );
2026-10-28 18:54:57 +02:00
await page . locator ( '[data-testid="practice-dock-bar"]' ). click ();
await page . waitForSelector ( practice , { visible : true });
await expectValue ( page , "`/synthetic/practice.pdf` `/synthetic/collapsed-practice.pdf`" , practice );
checks . push ( "Collapsed practice thinking keeps its claim/state through expansion without submission" );
}
assert . equal ( posts . length , 1 , "all claim/drop cases stage text only" );
}
// Actual Electron main + actual sandboxed preload. Temporary launcher sets
// userData BEFORE production main appends its -dev suffix or takes a lock.
const debugPort = await freePort ();
const launcher = path . join ( home , "electron-launcher.cjs" );
fs . mkdirSync ( path . join ( home , "desktop-profile" ), { recursive : true });
fs . mkdirSync ( path . join ( home , "desktop-profile-dev" ), { recursive : true });
fs . writeFileSync ( launcher , `const {app,session}=require('electron');\napp.setPath('home', ${ JSON . stringify ( home ) } );\napp.setPath('userData', ${ JSON . stringify ( path . join ( home , "desktop-profile" )) } );\napp.commandLine.appendSwitch('remote-debugging-port', ${ JSON . stringify ( String ( debugPort )) } );\napp.commandLine.appendSwitch('disable-background-networking');\napp.commandLine.appendSwitch('disable-sync');\napp.whenReady().then(()=>session.defaultSession.webRequest.onBeforeRequest((details,cb)=>{let u;try{u=new URL(details.url)}catch{return cb({cancel:true})}cb({cancel:!( ${ JSON . stringify ( origin ) } ===u.origin||(u.protocol==='ws:'&&u.host=== ${ JSON . stringify ( new URL ( origin ). host ) } )||['about:','data:','devtools:'].includes(u.protocol))});}));\nrequire( ${ JSON . stringify ( path . join ( root , "electron" , "main.cjs" )) } );\nconsole.log('LH_FILE_PATH_PROFILE '+JSON.stringify({home:app.getPath('home'),userData:app.getPath('userData')}));\n` );
const electron = spawn ( require ( "electron" ), [ launcher ], { cwd : root , env : { ... env , LH_DEV_URL : origin }, stdio : [ "ignore" , "pipe" , "pipe" ] }); children . push ( electron );
const captureElectron = data => { electronLogs = ( electronLogs + data ). slice ( - 24000 ); }; electron . stdout . on ( "data" , captureElectron ); electron . stderr . on ( "data" , captureElectron );
await startup ( electron , () => electronLogs , /DevTools listening on (ws:\/\/[^\s]+)/ , 45000 );
const profile = JSON . parse ( electronLogs . match ( /LH_FILE_PATH_PROFILE ([^\r\n]+)/ )[ 1 ]);
assert . equal ( profile . home , home ); assert . equal ( profile . userData , path . join ( home , "desktop-profile-dev" ));
const desktopLaunch = { executable : require ( "electron" ), args : [ launcher ], LH_DEV_URL : origin , HOME : home , userDataBeforeMain : path . join ( home , "desktop-profile" ), actualProfile : profile , cleanup : "all temporary profiles/data/fixtures removed on exit" };
const endpoint = electronLogs . match ( /DevTools listening on (ws:\/\/[^\s]+)/ )[ 1 ];
const desktop = await puppeteer . connect ({ browserWSEndpoint : endpoint }); browsers . push ( desktop );
const desktopPage = await desktop . waitForTarget ( target => target . type () === "page" ). then ( target => target . page ());
await instrument ( desktopPage ); page = desktopPage ;
await open ( desktopPage , "/chat/new" );
assert . equal ( await desktopPage . evaluate (() => typeof window . lh ? . getPathForFile ), "function" );
const fixturePath = path . join ( home , "actual OS-backed α `notes`.pdf" ); fs . writeFileSync ( fixturePath , "%PDF-1.7\nsynthetic offline fixture\n" );
await desktopPage . evaluate (() => { const input = document . createElement ( "input" ); input . type = "file" ; input . id = "os-backed-fixture" ; document . body . append ( input ); });
await ( await desktopPage . $ ( "#os-backed-fixture" )). uploadFile ( fixturePath );
const resolved = await desktopPage . evaluate (() => {
const input = document . querySelector ( "#os-backed-fixture" );
return { count : input . files . length , actual : window . lh . getPathForFile ( input . files [ 0 ]), virtual : window . lh . getPathForFile ( new File ([ "virtual" ], "/not/a/backing/path.pdf" )) };
});
assert . equal ( resolved . count , 1 ); assert . equal ( resolved . actual , fixturePath ); assert . equal ( resolved . virtual , "" );
await fill ( desktopPage , "readthis" ); await caret ( desktopPage , 4 );
const beforeDesktopDrop = posts . length ;
assert . equal ( await desktopPage . evaluate (() => {
const file = document . querySelector ( "#os-backed-fixture" ). files [ 0 ];
for ( const method of [ "text" , "arrayBuffer" , "stream" ]) Object . defineProperty ( file , method , { value () { throw new Error ( "No file content read" ); } });
const transfer = new DataTransfer (); transfer . items . add ( file );
const event = new DragEvent ( "drop" , { bubbles : true , cancelable : true , dataTransfer : transfer });
document . querySelector ( '[data-testid="file-path-drop-zone"]' ). dispatchEvent ( event ); return event . defaultPrevented ;
}), true );
const expectedQuoted = "``" + fixturePath + "``" ;
await expectValue ( desktopPage , `read ${ expectedQuoted } this` );
assert . equal ( await desktopPage . $eval ( '[data-file-path-input] [role="img"]' , chip => chip . title ), fixturePath );
assert . equal ( posts . length , beforeDesktopDrop , "actual OS-backed desktop drop must stage only" );
2026-10-28 19:52:07 +02:00
assert . equal ( await desktopPage . $ ( '[role="status"]' ), null , "successful desktop attachment has no confirmation bubble" );
2026-10-28 18:54:57 +02:00
assert . ok ( fs . existsSync ( fixturePath ));
assert . deepEqual ( fs . readdirSync ( path . join ( home , "data" )). filter ( name => / upload | attachment / i . test ( name )), []);
checks . push ( "Actual Electron production preload: Puppeteer-selected OS-backed File resolves exact Unicode/backtick full path; virtual File returns empty; real DataTransfer drop inserts at caret without submit or file reads" );
assert . deepEqual ( errors , []);
console . log ( JSON . stringify ({ checks , visualFailures , coverageFailures , screenshots , desktopLaunch , postsIntercepted : posts . length , blockedExternalRequests : blocked , isolation : "temporary HOME/data/auth/browser+Electron profiles; synthetic catalog; agent POSTs refused; no providers" , origin }, null , 2 ));
assert . deepEqual ( visualFailures , [], "Visible trailing-newline/caret projection must pass" );
assert . deepEqual ( coverageFailures , [], "Required collapsed target coverage must run" );
} catch ( error ) {
console . error ( error );
console . error ( JSON . stringify ({ checks , visualFailures , coverageFailures , errors , consoleErrors , networkFailures : networkFailures . slice ( - 30 ), posts , origin , url : page ? . url (), dom : await page ? . evaluate (() => ({ inputs : [... document . querySelectorAll ( '[data-file-path-input]' )]. map ( element => ({ placeholder : element . getAttribute ( "data-placeholder" ), text : element . textContent , html : element . innerHTML . slice ( 0 , 1200 ) })), text : document . body . innerText . slice ( - 2600 ) })). catch (() => null ), serverLogs , electronLogs }, null , 2 ));
process . exitCode = 1 ;
} finally {
shuttingDown = true ;
for ( const browser of browsers . reverse ()) { try { await browser . close (); } catch {} }
for ( const child of children . reverse ()) await stop ( child );
fs . rmSync ( home , { recursive : true , force : true });
}