@@ -10,6 +10,7 @@ import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../plug
1010
1111let LocalMediaAccessError : typeof import ( "./web-media.js" ) . LocalMediaAccessError ;
1212let loadWebMedia : typeof import ( "./web-media.js" ) . loadWebMedia ;
13+ let loadWebMediaRaw : typeof import ( "./web-media.js" ) . loadWebMediaRaw ;
1314let optimizeImageToJpeg : typeof import ( "./web-media.js" ) . optimizeImageToJpeg ;
1415
1516const TINY_PNG_BASE64 =
@@ -39,7 +40,8 @@ function installCanvasMediaResolver() {
3940}
4041
4142beforeAll ( async ( ) => {
42- ( { LocalMediaAccessError, loadWebMedia, optimizeImageToJpeg } = await import ( "./web-media.js" ) ) ;
43+ ( { LocalMediaAccessError, loadWebMedia, loadWebMediaRaw, optimizeImageToJpeg } =
44+ await import ( "./web-media.js" ) ) ;
4345 fixtureRoot = await fs . mkdtemp ( path . join ( resolvePreferredOpenClawTmpDir ( ) , "web-media-core-" ) ) ;
4446 tinyPngFile = path . join ( fixtureRoot , "tiny.png" ) ;
4547 await fs . writeFile ( tinyPngFile , Buffer . from ( TINY_PNG_BASE64 , "base64" ) ) ;
@@ -75,6 +77,47 @@ afterAll(async () => {
7577} ) ;
7678
7779describe ( "loadWebMedia" , ( ) => {
80+ function makeStallingFetch ( firstChunk : Uint8Array ) {
81+ return vi . fn (
82+ async ( ) =>
83+ new Response (
84+ new ReadableStream < Uint8Array > ( {
85+ start ( controller ) {
86+ controller . enqueue ( firstChunk ) ;
87+ } ,
88+ } ) ,
89+ {
90+ status : 200 ,
91+ headers : { "content-type" : "application/pdf" } ,
92+ } ,
93+ ) ,
94+ ) ;
95+ }
96+
97+ async function expectWebMediaIdleTimeout (
98+ createLoadPromise : ( ) => Promise < unknown > ,
99+ idleTimeoutMs : number ,
100+ ) {
101+ vi . useFakeTimers ( ) ;
102+ try {
103+ const outcome = createLoadPromise ( ) . then (
104+ ( ) => ( { status : "resolved" as const } ) ,
105+ ( error : unknown ) => ( { status : "rejected" as const , error } ) ,
106+ ) ;
107+ await vi . advanceTimersByTimeAsync ( idleTimeoutMs + 5 ) ;
108+ await expect (
109+ Promise . race ( [ outcome , Promise . resolve ( { status : "pending" as const } ) ] ) ,
110+ ) . resolves . toMatchObject ( { status : "rejected" } ) ;
111+ const result = await outcome ;
112+ expect ( result . status ) . toBe ( "rejected" ) ;
113+ if ( result . status === "rejected" ) {
114+ expect ( String ( result . error ) ) . toMatch ( / s t a l l e d | n o d a t a r e c e i v e d / i) ;
115+ }
116+ } finally {
117+ vi . useRealTimers ( ) ;
118+ }
119+ }
120+
78121 function createLocalWebMediaOptions ( ) {
79122 return {
80123 maxBytes : 1024 * 1024 ,
@@ -689,6 +732,43 @@ describe("loadWebMedia", () => {
689732 }
690733 } ) ;
691734
735+ it ( "applies the shared remote read idle timeout for raw web media loads" , async ( ) => {
736+ const readIdleTimeoutMs = 20 ;
737+ const fetchImpl = makeStallingFetch ( new Uint8Array ( [ 0x25 , 0x50 , 0x44 , 0x46 ] ) ) ;
738+
739+ await expectWebMediaIdleTimeout (
740+ ( ) =>
741+ loadWebMediaRaw ( "https://example.test/stalled.pdf" , {
742+ maxBytes : 1024 * 1024 ,
743+ fetchImpl,
744+ readIdleTimeoutMs,
745+ ssrfPolicy : { allowedHostnames : [ "example.test" ] } ,
746+ } ) ,
747+ readIdleTimeoutMs ,
748+ ) ;
749+ } ) ;
750+
751+ it ( "loads a valid remote PDF when the raw web media read stays active" , async ( ) => {
752+ const fetchImpl = vi . fn (
753+ async ( ) =>
754+ new Response ( Buffer . from ( "%PDF-1.4\n%%EOF" ) , {
755+ status : 200 ,
756+ headers : { "content-type" : "application/pdf" } ,
757+ } ) ,
758+ ) ;
759+
760+ const result = await loadWebMediaRaw ( "https://example.test/ok.pdf" , {
761+ maxBytes : 1024 * 1024 ,
762+ fetchImpl,
763+ readIdleTimeoutMs : 20 ,
764+ ssrfPolicy : { allowedHostnames : [ "example.test" ] } ,
765+ } ) ;
766+
767+ expect ( result . kind ) . toBe ( "document" ) ;
768+ expect ( result . contentType ) . toBe ( "application/pdf" ) ;
769+ expect ( result . buffer . toString ( ) ) . toContain ( "%PDF-1.4" ) ;
770+ } ) ;
771+
692772 it ( "rejects unsupported media store URI locations" , async ( ) => {
693773 await expectLoadWebMediaErrorCode (
694774 loadWebMedia ( "media://outbound/tiny.png" ) ,
0 commit comments