a
    x“<j$z  ã                   @   sN  d Z ddlZddlZddlZddlZddlZddlZddlZddlZddl	Z	ddl
mZmZ ej ej e¡¡Zej ed¡Zej ed¡Zej ed¡ZdZdd	„ ZG d
d„ deƒZdd„ ZdZdZdd„ Zd*dd„Zd+dd„Zd,dd„Zdd„ Z dd„ Z!dd„ Z"dd „ Z#d!d"„ Z$d-d#d$„Z%d%d&„ Z&d'd(„ Z'e(d)krJe )e *e!ƒ ¡¡ dS ).u…  
iframe-coalesced-events spike â€” 2-test validator.

Tests whether the headless-vs-headful `getCoalescedEvents()` discriminator
(the signal shipped as `coalesced_mouse_synthesis` in detect-bot Phase 3)
HOLDS UP INSIDE IFRAMES.

Per NIGHTLY-FINDINGS-2026-06-24, the existing signal tests getCoalescedEvents()
in the main thread only. The Crawlex article (cited in NIGHTLY-FINDINGS-2026-06-25)
notes that iframe/worker fingerprinting is "a family, not a single signal".
This spike asks: does stealth framework mouse synthesis leak through the
iframe's realm, or is the patch realm-aware?

For each browser (vanilla headless Chromium, patchright headless Chromium)
the runner:
  1. Loads probe.html (which hosts an srcdoc iframe with its own target).
  2. Runs two 30-move sequences â€” one over #mainTarget in MAIN, one over
     #iframeTarget inside the IFRAME.
  3. Reads the coalesced sample buffers from both realms.
  4. Computes per-context summary stats (count, min, max, avg, median,
     single-entry count, multi-entry count, multi-pct).

Pass conditions:
  - In ALL contexts: max <= 1, 100% single-entry (headless synthesis).
  - OR: BOTH contexts show the same pattern (e.g., both leak multi-entry
    batches, both are clean). The signal transfers across realms.

Real human: max > 1, 80%+ multi-entry (per shipped signal baseline).
Headless: max <= 1, all single-entry.

If vanilla headless shows max<=1 in BOTH main and iframe AND patchright
headless shows the SAME â€” signal transfers cleanly across iframe boundary.
If patchright shows multi-entry in main but single-entry in iframe (or
vice versa) â€” there's a cross-realm leak in the stealth patch.
é    N)Ú
HTTPServerÚSimpleHTTPRequestHandlerz
probe.htmlzRESULTS.jsonzVALIDATION-REPORT.mdi  c                  C   s*   t   ¡ } |  d¡ |  ¡ d }|  ¡  |S )N)ú	127.0.0.1r   é   )ÚsocketÚbindÚgetsocknameÚclose)ÚsÚp© r   úG/home/huxley/projects/detect-bot/spikes/iframe-coalesced/test_runner.pyÚ_find_free_port8   s
    
r   c                       s$   e Zd Z‡ fdd„Zdd„ Z‡  ZS )Ú_Handlerc                    s   t ƒ j|dti|¤Ž d S )NZ	directory)ÚsuperÚ__init__Ú	SPIKE_DIR©ÚselfÚargsÚkwargs©Ú	__class__r   r   r   A   s    z_Handler.__init__c                 O   s   d S ©Nr   r   r   r   r   Úlog_messageD   s    z_Handler.log_message)Ú__name__Ú
__module__Ú__qualname__r   r   Ú__classcell__r   r   r   r   r   @   s   r   c                 C   s*   t d| ftƒ}tj|jdd}| ¡  |S )Nr   T)ÚtargetÚdaemon)r   r   Ú	threadingÚThreadÚserve_foreverÚstart)ÚportÚhttpdÚtr   r   r   Ústart_serverH   s    r(   é   é   c           	      Ã   sì   |d |d d  }|d |d d  }t |d |d ƒd }ttƒD ] }|t }||d|d dkrhd	nd
 d|d dkr~d	nd
    }||d|d d	kr d	nd
 d|d dkr¶d	nd
    }| j ||¡I dH  |  t¡I dH  qFdS )zJJittered sweep across the given bounding box (dict with x,y,width,height).ÚxÚwidthé   ÚyÚheightçffffffÖ?ç333333ã?r   r   éÿÿÿÿç333333Ó?é   ç      à?çš™™™™™Ù?é   N©ÚminÚrangeÚMOVES_PER_TARGETÚmouseÚmoveÚwait_for_timeoutÚSTEP_MS)	ÚpageÚboxÚcxÚcyÚspanÚir'   r+   r.   r   r   r   Ú	move_overW   s    88rF   Ú c              
   Ã   s<  |   d¡I dH  z|  d¡I dH  W n8 ty\ } z td|› d|› ƒ W Y d}~n
d}~0 0 |  d¡ ¡ I dH }|du rŠdd|› d	iS | j |d
 d |d d ¡I dH  |  d¡I dH  t| |ƒI dH  |  d¡I dH  |   d¡I dH }|  d¡ ¡ I dH }|du rdd|› diS | d¡ ¡ I dH }|du rJdd|› diS |d
 |d  }}	||d
  |	|d  |d |d dœ}
| j |
d
 d |
d d ¡I dH  |  d¡I dH  t| |
ƒI dH  |  d¡I dH  g }z|  d¡I dH }W n: ty( } z td|› d|› ƒ W Y d}~n
d}~0 0 ||||||
dœS )zh
    Run the 30-move sweep over MAIN target, then over IFRAME target.
    Return per-context stats.
    ú$() => { window.__mainSamples = []; }Nú&() => { window.__iframeSamples = []; }ú  [ú(] WARN: could not reset iframe samples: ú#mainTargetÚerrorú[ú!] #mainTarget has no bounding boxr+   r   r.   é2   é–   ú () => window.__mainSamples || []ú#probeFrameú!] #probeFrame has no bounding boxú#iframeTargetú3] #iframeTarget has no bounding box (inside iframe)r,   r/   ©r+   r.   r,   r/   ú,() => (window.__iframeSamples || []).slice()ú'] WARN: could not read iframe samples: ©ÚmainSamplesÚiframeSamplesÚmainBoxÚiframeBoxDocÚiframeBoxPageÚtargetBoxViewport)	ÚevaluateÚ	ExceptionÚprintÚlocatorÚbounding_boxr<   r=   r>   rF   ©r@   ÚframeÚlabelÚeZmain_boxZmain_samplesZiframe_box_pageZiframe_target_box_docZfxZfyZtarget_box_viewportZiframe_samplesr   r   r   Úmeasuree   sR    *$	



ü	$*úrj   c                 C   sø   | s|dddœS dd„ | D ƒ}|s.|dddœS t |ƒ}|t|ƒt|ƒt|ƒt|ƒt|ƒ |t|ƒd  tdd	„ |D ƒƒtd
d	„ |D ƒƒ|r¤tdd	„ |D ƒƒt|ƒ ndt|ƒdkrÆ|tt|ƒd ƒ n|d t|ƒdkrì|tt|ƒd ƒ n|d dœS )Nr   ú
no samples)ÚcontextÚcountrM   c                 S   s   g | ]}t |tƒr|‘qS r   )Ú
isinstanceÚint©Ú.0Úvr   r   r   Ú
<listcomp>­   ó    zsummarize.<locals>.<listcomp>zno integer samplesr-   c                 s   s   | ]}|d krd V  qdS )r   Nr   rp   r   r   r   Ú	<genexpr>¸   rt   zsummarize.<locals>.<genexpr>c                 s   s   | ]}|d krdV  qdS ©r-   r   Nr   rp   r   r   r   ru   ¹   rt   c                 s   s   | ]}|d krdV  qdS rv   r   rp   r   r   r   ru   º   rt   g        r   gÍÌÌÌÌÌì?g®Gáz®ï?)rl   rm   r9   ÚmaxÚavgÚmedianÚonesÚmultisÚmultiPctZp90Zp99)ÚsortedÚlenr9   rw   Úsumro   )Zsamplesrh   r
   Zs_sortedr   r   r   Ú	summarizeª   s$     &&õr€   c           	   
   Ã   s0  d|v rdnd}|› |› d}| j |dddI dH  z| jdd	d
I dH  W nT ty  } z<|  ¡ I dH dd… }dd|› d|› d|› iW  Y d}~S d}~0 0 |  d¡j}|du rÆdd|› diS t| ||dI dH }d|v ræ|S t|d ddt|d dd|d |d |d |d dœ|d |d dœS )z?Load the probe, arm it, run the move sequence, collect results.ú?ú&úauto=1Údomcontentloadedé N  ©Ú
wait_untilÚtimeoutNúwindow.__dbReady === trueé@  ©rˆ   éX  rM   rN   z] __dbReady never set: ú; body[:600]=rS   z] could not get iframe frame©rh   r[   Úmainr\   Úiframer]   r^   r_   r`   ©r]   r^   r_   r`   ©r   r   ÚgeometryÚrawMainÚ	rawIframe)ÚgotoÚwait_for_functionrb   ÚcontentÚframe_locatorrg   rj   r€   )	r@   Úurlrh   ÚsepÚ	armed_urlri   Úbodyrg   Úrawr   r   r   Ú	run_probeÀ   s0    0üörŸ   c                 Ã   s  t d|› dƒ z$|ƒ }t |¡r.|I dH }n|}W n4 tyh } z|d|› dœW  Y d}~S d}~0 0 zh| ¡ I dH }| ¡ I dH }t|||dI dH }	| ¡ I dH  W z| ¡ I dH  W n tyÐ   Y n0 n*z| ¡ I dH  W n t yú   Y n0 0 d|	v rt d|	d › ƒ nè|	d	 }
|	d
 }t d|
 d¡d›d|
 d¡› d|
 dd¡d›d|
 d¡› d|
 d¡› d|
 d¡› d|
 dd¡d d›dƒ t d| d¡d›d| d¡› d| dd¡d›d| d¡› d| d¡› d| d¡› d| dd¡d d›dƒ ||	d œS )!z?Spawn a browser using browser_factory() and return its payload.ú
=== ú ===Nzlaunch failed: ©ÚnamerM   rŽ   rM   ú	  ERROR: r   r   ú  MAIN:   count=rm   ú>3ú  max=rw   ú  avg=rx   r   ú.2fú	  median=ry   ú  ones=rz   ú  multi=r{   ú  (r|   éd   ú.1fú%)ú  IFRAME: count=©r£   Úresult)	rc   ÚasyncioÚiscoroutinerb   Únew_contextÚnew_pagerŸ   r	   Úget)r   r£   Zbrowser_factoryrš   ÚmaybeÚbrowserri   Úctxr@   r³   ÚmrE   r   r   r   Úrun_one_browseræ   s8    
&ý
llr½   c            
   	   ƒ   sê  t dƒ t dƒ t dƒ t dt› ƒ t dt› ƒ tƒ } t| ƒ}d| › d‰t dˆ› ƒ ‡ fdd	„}d
dlm} zR|ƒ 4 I d H š*‰ tˆ d|ˆƒI d H }W d   ƒI d H  qÄ1 I d H sº0    Y  W n0 t 	¡  
d ‡fdd„¡I d H }z| ¡  W n ty   Y n0 dt d¡||gdœ}ttdƒ"}tj||dtd W d   ƒ n1 sT0    Y  t dt› ƒ t||ƒ\}}	t ƒ  t dƒ t d|› ƒ t d|	› ƒ t dƒ t|||	ƒ t dt› ƒ |dkrÔd
S |dkrâdS dS d S )NzF======================================================================u2   IFRAME-COALESCED-EVENTS SPIKE â€” 2-test validatorzProbe: zWorking dir: zhttp://127.0.0.1:z/probe.htmlzLocal server: c                      s   ˆ j jdg d¢dS )NT)ú--no-sandboxz--disable-setuid-sandboxú--disable-dev-shm-usage©Úheadlessr   )ÚchromiumÚlaunchr   )r   r   r   Úvanilla  s    þzmain.<locals>.vanillar   )Úasync_playwrightzvanilla-headlessc                      s   t ˆ ƒS r   )Ú_run_patchright_in_threadr   )Ú	probe_urlr   r   Ú<lambda>"  rt   zmain.<locals>.<lambda>ziframe-coalesced-eventsz%Y-%m-%d %H:%M:%S)ZspikeÚranAtÚtestsÚwr-   )ÚindentÚdefaultz
Wrote zOVERALL VERDICT: zREASON: zWrote ÚPASSÚREJECTr   )rc   Ú
PROBE_PATHr   r   r(   Úplaywright.async_apirÅ   r½   r´   Úget_event_loopÚrun_in_executorÚshutdownrb   ÚtimeÚstrftimeÚopenÚRESULTS_PATHÚjsonÚdumpÚstrÚ_compute_verdictÚ_write_reportÚREPORT_PATH)
r%   r&   rÄ   rÅ   Úr1Úr2ÚpayloadÚfÚoverallÚreasonr   )r   rÇ   r   r     sT    @ÿ
ý2

r   c              
   C   s  d| v rd| d fS | d }|  di ¡}|  di ¡}|  d¡sH|  d¡rhdd|  d¡› d|  d¡› fS |  d¡r||  d¡sœdd	|  d¡› d|  d¡› fS |d
 dko²|d dk}|d
 dkoÊ|d dk}|d
 dkoâ|d dk}|d
 dkoú|d dk}|r$|r$dd|d
 › d|d
 › fS |rn|rndd|d
 › d|d d d›d|d
 › d|d d d›d	fS |rž|sždd|d
 › d|d d d›dfS |sÎ|rÎdd|d
 › d|d d d›dfS dd |d
 › d|d d d›d|d
 › d|d d d›d	fS )!zOReturn (verdict, reason) for a single browser result dict {name, result/error}.rM   ÚERRORr³   r   r   zmain=z iframe=rm   zno samples: main=rw   r   r|   r   r5   úHEADLESS-BOTHz%both contexts single-entry: main max=z iframe max=úHUMAN-LIKE-BOTHz$both contexts multi-entry: main max=z multi=r®   r¯   z% / iframe max=ú%zLEAK-MAIN-ONLYz1main single-entry but iframe is multi-entry (max=ú, z% multi)zLEAK-IFRAME-ONLYz1iframe single-entry but main is multi-entry (max=ZMIXEDzinconsistent: main max=)r¸   )ÚrÚresr¼   rE   Zmain_singleZiframe_singleZ
main_multiZiframe_multir   r   r   Ú_verdict_for_oneF  s,      >$$rì   c                 C   s  t | ƒ\}}t |ƒ\}}d|v r<d|v r<dd|› d|› fS d|v rRdd|› fS d|v rndd|› d|› fS |d	krŠdd
|› d|› fS |d	kr¨dd|› d|› dfS | d¡rÎdd|› d|› d|› dfS |dkrêdd|› d|› fS dd|› d|› d|› fS )u‹  
    Combine the two browser verdicts into an overall verdict.
    PASS: vanilla=HEADLESS-BOTH AND patchright behaves the same way.
          â†’ signal transfers cleanly across iframe boundary; no realm leak.
    PASS-WITH-CAVEAT: vanilla=HEADLESS-BOTH AND patchright=LEAK-* 
          â†’ stealth patch leaks multi-entry batches in one context but not the
            other â†’ cross-realm discriminator IS the signal.
    MARGINAL: both browsers show LEAK-* â€” signal is universal (probe broken
              in some way or both contexts coalesce identically).
    REJECT:   any ERROR, or vanilla shows non-headless behavior (control broken).
    rå   rÏ   zboth runs errored: vanilla=z; patchright=zvanilla headless errored: ÚMARGINALzvanilla=z; patchright errored: ræ   z8vanilla headless should be HEADLESS-BOTH (control), got z: rÎ   z, patchright=u‘   : signal transfers cleanly across iframe boundary â€” headless synthesis is realm-agnostic, no stealth cross-realm leak detected for this surfacezLEAK-z (control), patchright=u4   : stealth framework shows cross-realm asymmetry â€” zP. THIS IS A DETECTION SIGNAL: the iframe realm reveals the stealth patch's leak.rç   z%vanilla=HEADLESS-BOTH but patchright=z· (both contexts multi-entry). Either patchright is implementing coalescing differently than vanilla, or the mouse injection is hitting real hardware in patchright's CDP path. Reason: )rì   Ú
startswith)rß   rà   Zv1Zm1Zv2Zm2r   r   r   rÜ   a  s6    ÿ
ÿÿýÿrÜ   c                 C   s  d}t d|› dƒ zÀddlm} |ƒ –}|jjdddgd	}| ¡ }| ¡ }d
| v r^dnd
}| › |› d}|j|ddd z|jddd W nd t	 yö }	 zJ| 
¡ d d… }
| ¡  |d|	› d|
› dœW  Y d }	~	W  d   ƒ W S d }	~	0 0 | d¡j}|d u r0| ¡  |ddœW  d   ƒ W S t|||d}| ¡  d|v rj||dœW  d   ƒ W S t|d ddt|d dd|d  |d! |d" |d# d$œ|d |d d%œ}W d   ƒ n1 sÊ0    Y  W n6 t	y }	 z|d&|	› dœW  Y d }	~	S d }	~	0 0 d|v r,t d'|d › ƒ nè|d }|d }t d(| d)¡d*›d+| d,¡› d-| d.d¡d/›d0| d1¡› d2| d3¡› d4| d5¡› d6| d7d¡d8 d9›d:ƒ t d;| d)¡d*›d+| d,¡› d-| d.d¡d/›d0| d1¡› d2| d3¡› d4| d5¡› d6| d7d¡d8 d9›d:ƒ ||dœS )<Nzpatchright-headlessr    r¡   r   )Úsync_playwrightTr¾   r¿   rÀ   r   r‚   rƒ   r„   r…   r†   r‰   rŠ   r‹   rŒ   z__dbReady never set: r   r¢   rS   zcould not get iframe framerŽ   rM   r²   r[   r   r\   r   r]   r^   r_   r`   r‘   r’   zpatchright launch/run failed: r¤   r¥   rm   r¦   r§   rw   r¨   rx   r©   rª   ry   r«   rz   r¬   r{   r­   r|   r®   r¯   r°   r±   )rc   Zpatchright.sync_apirï   rÂ   rÃ   r¶   r·   r–   r—   rb   r˜   r	   r™   rg   Ú_measure_syncr€   r¸   )rš   r£   rï   r   rº   r»   r@   r›   rœ   ri   r   rg   rž   r³   r¼   rE   r   r   r   rÆ   ‘  s^    
þ<

üö*&
llrÆ   c              
   C   sà  |   d¡ z|  d¡ W n8 tyP } z td|› d|› ƒ W Y d}~n
d}~0 0 |  d¡ ¡ }|du rxdd|› d	iS | j |d
 d |d d ¡ |  d¡ t| |ƒ |  d¡ |   d¡}|  d¡ ¡ }|du rädd|› diS | d¡ ¡ }|du rdd|› diS |d
 |d  }}	||d
  |	|d  |d |d dœ}
| j |
d
 d |
d d ¡ |  d¡ t| |
ƒ |  d¡ g }z|  d¡}W n: tyÌ } z td|› d|› ƒ W Y d}~n
d}~0 0 ||||||
dœS )zSync-API twin of measure().rH   rI   rJ   rK   NrL   rM   rN   rO   r+   r   r.   rP   rQ   rR   rS   rT   rU   rV   r,   r/   rW   rX   rY   rZ   )	ra   rb   rc   rd   re   r<   r=   r>   Ú_move_over_syncrf   r   r   r   rð   Î  sR    
*






ü


*úrð   c                 C   sØ   |d |d d  }|d |d d  }t |d |d ƒd }ttƒD ]Œ}||d|d dkr`d	nd
 d|d dkrvd	nd
    }||d|d d	kr˜d	nd
 d|d dkr®d	nd
    }| j ||¡ |  t¡ qFd S )Nr+   r,   r-   r.   r/   r0   r1   r   r   r2   r3   r4   r5   r6   r7   r8   )r@   rA   rB   rC   rD   rE   r+   r.   r   r   r   rñ     s    88rñ   c                 C   sÈ  | d }g }|  d¡ |  d¡ |  d| d › ¡ |  d|› d¡ |  d|› ¡ |  d¡ |  d	¡ |  d¡ |  d
¡ |  d¡ |  d¡ |  d¡ |  d¡ |  d¡ |  d¡ |  d¡ |  d¡ |  d¡ |  d¡ |  d¡ |  d¡ |  d¡ |  d¡ |  d¡ |  d¡ |  d¡ |  d¡ |  d¡ |  d¡ |  d¡ |  d¡ |  d¡ |  d¡ |  d¡ |  d¡ |  d¡ |  d ¡ |  d¡ |  d!¡ |  d"¡ |  d#¡ |  d¡ |  d$¡ |  d¡ |D ]F}|  d%|d& › d'¡ |  d¡ d(|v r4|  d)|d( › d'¡ |  d¡ qà|d* }| d+i ¡}| d,i ¡}|  d-¡ |  d.¡ d+|fd,|ffD ]²\}	}
d(|
v sœ|
 d/d0¡d0kr¾|  d1|	› d2|
 d(d3¡› d4¡ nj|  d1|	› d5|
d/ › d5|
d6 › d7|
d8 › d9|
d: d;›d5|
d< › d5|
d= › d5|
d> › d5|
d? d@ dA›dB¡ qx|  d¡ t|ƒ\}}|  dC|› dD|› ¡ |  d¡ | dE¡rô| dF¡rô|  dG¡ |  d¡ |  dHdI dJdK„ |dE dLd … D ƒ¡ d' ¡ |  dMdI dNdK„ |dF dLd … D ƒ¡ d' ¡ |  d¡ dO|v rà|dO }| dPi ¡}| dQi ¡}| dRi ¡}|  dS¡ |  d¡ |  dT| dUdV¡dA›dW| dXdV¡dA›dY| dZdV¡dA›d[| d\dV¡dA›¡ |  d]| dUdV¡dA›dW| dXdV¡dA›dY| dZdV¡dA›d[| d\dV¡dA›¡ |  d^| dUdV¡dA›dW| dXdV¡dA›dY| dZdV¡dA›d[| d\dV¡dA›¡ |  d¡ qà|  d_¡ |  d¡ |d`krPda|v rÌ|  db¡ |  dc¡ |  dd¡ |  de¡ |  df¡ |  dg¡ |  dh¡ |  di¡ |  dj¡ |  dk¡ |  dl¡ |  dm¡ n‚|  dn¡ |  do¡ |  dp¡ |  dq¡ |  dr¡ |  ds¡ |  dt¡ |  du¡ |  dv¡ |  dw¡ |  dx¡ |  dy¡ |  dz¡ np|d{krŽ|  d|¡ |  d}¡ |  d~¡ |  d¡ |  d€¡ n2|  d¡ |  d‚¡ |  dƒ¡ |  d„¡ |  d…¡ |  d¡ |  d†¡ |  d¡ |  d‡¡ |  dˆ¡ |  d‰¡ |  dŠ¡ |  d‹¡ |  dŒ¡ |  d¡ |  d¡ |  d¡ |  dŽ¡ |  d¡ |  d¡ |  d‘¡ |  d¡ |  d’¡ |  d“¡ |  d¡ ttd”ƒ }| d• |¡¡ W d   ƒ n1 sº0    Y  d S )–NrÊ   u/   # iframe-coalesced-events â€” VALIDATION REPORTrG   z**Generated:** rÉ   z**Overall verdict:** **z**z**Reason:** z## HypothesiszRPer `NIGHTLY-FINDINGS-2026-06-24`, the existing `coalesced_mouse_synthesis` signalzP(shipped in detect-bot Phase 3) tests `getCoalescedEvents()` in the MAIN thread.zOThe Crawlex article (cited in NIGHTLY-FINDINGS-2026-06-25) confirms that iframezEand worker context fingerprinting is "a family, not a single signal".zN**Question:** do stealth frameworks (patchright, puppeteer-stealth) ALSO patchzLthe mouse-event coalescing behavior inside cross-origin iframes, or does thez#patch only apply to the main frame?zL**Sub-hypothesis:** A naive stealth patch that modifies `dispatchMouseEvent`zI(or similar) at the main-realm level will leak through to the iframe: thezLmain frame will produce single-entry `getCoalescedEvents()` batches (stealthzNsynthesis) while the iframe produces multi-entry batches (real hardware path),zFor vice versa. Either asymmetry is a high-confidence detection signal.zD**Real-human baseline** (from prior spike): max>1, 80%+ multi-entry.z1**Headless baseline:** max<=1, 100% single-entry.z	## MethodzU`probe.html` hosts a same-origin `<iframe sandbox="allow-same-origin allow-scripts">`zMwith `srcdoc`. Both the parent and the iframe contain a target element with azN`pointermove` listener that records `e.getCoalescedEvents().length`. The probezBexposes both buffers via `window.__mainSamples` (parent realm) anduH   `window.__iframeSamples` (iframe realm â€” read via `frame.evaluate()`).zFThe test runner uses Playwright `page.mouse.move()` over each target'szIviewport bounding box (30 jittered moves at ~60Hz), then collects samplesz.from both contexts and computes summary stats.zL`</script>` inside the iframe srcdoc is escaped as `<\/script>` to avoid thezMHTML parser bug where the inner `</script>` would prematurely close the outerzN`<script>` block (the same pitfall noted in the `cross-realm-tostring` spike).z## Testsz### `r£   ú`rM   z**ERROR:** `r³   r   r   zW| Context | count | min | max | avg | median | single-entry | multi-entry | multi-pct |z%|---|---|---|---|---|---|---|---|---|rm   r   z| u.    | â€” | â€” | â€” | â€” | â€” | â€” | â€” | `rk   z` |z | r9   z | **rw   z** | rx   r©   ry   rz   r{   r|   r®   r¯   z% |z**Per-test classification:** `u   ` â€” r”   r•   zLast 10 samples per context:z	- main: `ré   c                 s   s   | ]}t |ƒV  qd S r   ©rÛ   rp   r   r   r   ru   [  rt   z _write_report.<locals>.<genexpr>iöÿÿÿz- iframe: `c                 s   s   | ]}t |ƒV  qd S r   ró   rp   r   r   r   ru   \  rt   r“   r]   r_   r`   zGeometry (viewport coords, px):z- main target box: x=r+   r   z y=r.   z w=r,   z h=r/   z- iframe viewport box: x=z)- iframe target box (viewport coords): x=z## InterpretationrÎ   zcross-realm asymmetryzJ- **Vanilla headless** is HEADLESS-BOTH (single-entry in main AND iframe).zL- **patchright headless** shows asymmetric coalescing between the two realmsuO     â€” one context emits multi-entry batches while the other emits single-entry.zH  This is the **cross-realm leak signal**: the stealth framework's mousez"  synthesis patch is realm-scoped.z@- **Detection recommendation:** in the production probe, capturezD  coalesced-event sizes in BOTH the main frame AND an iframe target.zI  If the patterns diverge (one side multi-entry, the other single-entry),zJ  it is highly likely that a stealth framework is patching mouse synthesisz  on the main realm only.zJ- **False-positive risk:** low. The asymmetry requires a stealth frameworkuM     AND a deliberate cross-realm fingerprint â€” well above random noise floor.zL- **Vanilla headless** AND **patchright headless** both produce single-entryzG  `getCoalescedEvents()` batches in BOTH the main frame and the iframe.zK- This means the headless-mouse synthesis signal **transfers cleanly acrossuM     the iframe realm boundary** â€” the discriminator works inside iframes justz   as it does in the main thread.zM- **Detection implication:** the existing `coalesced_mouse_synthesis` findingzK  can be extended to capture coalesced counts inside iframes. Bots that tryzJ  to evade by injecting their synthetic mouse through an iframe will stillz  be flagged.zG- **Caveat:** this surface detects headless synthesis, not specificallyzJ  patchright. To detect patchright specifically, a different probe surfacez;  is needed (e.g., the `cross-realm-tostring` spike detectsz)  `Function.prototype.toString` masking).rí   zI- The two browsers behaved differently than expected. Most likely causes:zL  - patchright is implementing coalescing differently (HUMAN-LIKE-BOTH case)zF  - the iframe cross-realm didn't get a chance to install its listenerz:  - the mouse injection is bypassing the iframe's documentz>- See the per-test classification and geometry sections above.z=- The probe or its control failed. See per-test errors above.z- Common causes:z&  - iframe srcdoc didn't load (timing)z?  - mouse coords landed outside the iframe target viewport areaz  - patchright driver mismatchz## Verdict keyuL   - **PASS** â€” vanilla=HEADLESS-BOTH (control). patchright either also showszJ  HEADLESS-BOTH (clean transfer), OR shows asymmetric coalescing (LEAK-*),z>  in which case we have a confirmed cross-realm discriminator.uK   - **MARGINAL** â€” vanilla=HEADLESS-BOTH but patchright=HUMAN-LIKE-BOTH (orzB  some other unexpected combo). Probe surface needs investigation.uS   - **REJECT** â€” vanilla isn't HEADLESS-BOTH (control broken) or both runs errored.z## Reproductionz```bashz#cd /home/huxley/projects/detect-botz@bash _env.sh -c "python3 spikes/iframe-coalesced/test_runner.py"z```zFThe probe is served by an in-process HTTP server on a random free portzP(see `_find_free_port`). The probe path is `spikes/iframe-coalesced/probe.html`.rË   Ú
)Úappendr¸   rì   Újoinr×   rÞ   Úwrite)rá   rã   rä   rÊ   Úlinesr'   rê   r¼   rE   r»   r
   Zverdict_ctxZ
reason_ctxÚgZmbZibÚtbrâ   r   r   r   rÝ     s6   














































"2ÿÿÿÿÿ



..



JJJ
























































rÝ   Ú__main__)rG   )rG   )rG   )rG   )+Ú__doc__r´   rÙ   Úosr   Ú
subprocessÚsysr!   rÕ   ZshutilZhttp.serverr   r   ÚpathÚdirnameÚabspathÚ__file__r   rö   rÐ   rØ   rÞ   ZDEFAULT_PORTr   r   r(   r;   r?   rF   rj   r€   rŸ   r½   r   rì   rÜ   rÆ   rð   rñ   rÝ   r   ÚexitÚrunr   r   r   r   Ú<module>   sF   #
E

&A0=
7  
