@@ -25,32 +25,32 @@ vi.mock("openclaw/plugin-sdk/ssrf-runtime", () => ({
2525 fetchWithSsrFGuard : vi . fn ( ) ,
2626} ) ) ;
2727
28+ const saveResponseMediaMock = vi . hoisted ( ( ) =>
29+ vi . fn (
30+ async (
31+ response : Response ,
32+ options ?: { fallbackContentType ?: string ; maxBytes ?: number ; readIdleTimeoutMs ?: number } ,
33+ ) => {
34+ const length = Number ( response . headers . get ( "content-length" ) ) ;
35+ if ( Number . isFinite ( length ) && options ?. maxBytes !== undefined && length > options . maxBytes ) {
36+ throw new Error ( "content length exceeds maxBytes" ) ;
37+ }
38+ return {
39+ path : "/tmp/saved.png" ,
40+ contentType : options ?. fallbackContentType ?? "image/png" ,
41+ } ;
42+ } ,
43+ ) ,
44+ ) ;
45+
2846vi . mock ( "../runtime.js" , ( ) => ( {
2947 getMSTeamsRuntime : vi . fn ( ( ) => ( {
3048 media : {
3149 detectMime : vi . fn ( async ( ) => "image/png" ) ,
3250 } ,
3351 channel : {
3452 media : {
35- saveResponseMedia : vi . fn (
36- async (
37- response : Response ,
38- options ?: { fallbackContentType ?: string ; maxBytes ?: number } ,
39- ) => {
40- const length = Number ( response . headers . get ( "content-length" ) ) ;
41- if (
42- Number . isFinite ( length ) &&
43- options ?. maxBytes !== undefined &&
44- length > options . maxBytes
45- ) {
46- throw new Error ( "content length exceeds maxBytes" ) ;
47- }
48- return {
49- path : "/tmp/saved.png" ,
50- contentType : options ?. fallbackContentType ?? "image/png" ,
51- } ;
52- } ,
53- ) ,
53+ saveResponseMedia : saveResponseMediaMock ,
5454 saveMediaBuffer : vi . fn ( async ( _buf : Buffer , ct : string ) => ( {
5555 path : "/tmp/saved.png" ,
5656 contentType : ct ?? "image/png" ,
@@ -170,6 +170,14 @@ describe("downloadMSTeamsGraphMedia hosted content $value fallback", () => {
170170 ) ;
171171 expect ( result . media . length ) . toBeGreaterThan ( 0 ) ;
172172 expect ( result . hostedCount ) . toBe ( 1 ) ;
173+ // Regression guard: the hostedContents $value save must forward the same
174+ // idle-read bound as the sibling Bot Framework/remote-media save
175+ // callers, or a stalled Graph hosted-content body can hang this path
176+ // indefinitely.
177+ expect ( saveResponseMediaMock ) . toHaveBeenCalledWith (
178+ expect . any ( Response ) ,
179+ expect . objectContaining ( { readIdleTimeoutMs : 30_000 } ) ,
180+ ) ;
173181 } ) ;
174182
175183 it ( "skips hosted content when the list item has no id" , async ( ) => {
0 commit comments