@@ -4,6 +4,7 @@ vi.mock("@jsonbored/gittensory-engine", async () => {
44 return import ( "../../packages/gittensory-engine/src/index" ) ;
55} ) ;
66
7+ import { MAX_FOCUS_MANIFEST_BYTES } from "../../packages/gittensory-engine/src/index" ;
78import { fetchSelfReviewContext } from "../../packages/gittensory-miner/lib/self-review-context.js" ;
89
910function jsonResponse ( body : unknown , status = 200 ) {
@@ -24,6 +25,40 @@ function textResponse(text: string, status = 200) {
2425 } ;
2526}
2627
28+ function oversizedContentLengthResponse ( ) {
29+ return {
30+ ok : true ,
31+ status : 200 ,
32+ headers : new Headers ( { "content-length" : String ( MAX_FOCUS_MANIFEST_BYTES + 1 ) } ) ,
33+ json : async ( ) => null ,
34+ text : async ( ) => {
35+ throw new Error ( "oversized manifest should not be materialized" ) ;
36+ } ,
37+ } ;
38+ }
39+
40+ function chunkedManifestResponse ( chunks : Uint8Array [ ] , onCancel : ( ) => void ) {
41+ let index = 0 ;
42+ return {
43+ ok : true ,
44+ status : 200 ,
45+ headers : new Headers ( ) ,
46+ body : {
47+ getReader : ( ) => ( {
48+ read : async ( ) => ( index < chunks . length ? { done : false , value : chunks [ index ++ ] } : { done : true , value : undefined } ) ,
49+ cancel : async ( ) => {
50+ onCancel ( ) ;
51+ } ,
52+ releaseLock : ( ) => { } ,
53+ } ) ,
54+ } ,
55+ json : async ( ) => null ,
56+ text : async ( ) => {
57+ throw new Error ( "streaming manifest should not use response.text()" ) ;
58+ } ,
59+ } ;
60+ }
61+
2762const REPO_PAYLOAD = {
2863 name : "widgets" ,
2964 full_name : "acme/widgets" ,
@@ -283,6 +318,43 @@ describe("fetchSelfReviewContext (#5145)", () => {
283318 expect ( result . manifest . gate ) . toBeDefined ( ) ;
284319 } ) ;
285320
321+ it ( "rejects oversized manifest responses from content-length before reading the body" , async ( ) => {
322+ let manifestRequests = 0 ;
323+ const fetchImpl = async ( url : string ) => {
324+ if ( url . includes ( "raw.githubusercontent.com" ) ) {
325+ manifestRequests += 1 ;
326+ return oversizedContentLengthResponse ( ) ;
327+ }
328+ if ( url . includes ( "/repos/acme/widgets/issues" ) ) return jsonResponse ( [ ] ) ;
329+ if ( url . includes ( "/repos/acme/widgets/pulls" ) ) return jsonResponse ( [ ] ) ;
330+ if ( url . includes ( "/repos/acme/widgets" ) ) return jsonResponse ( REPO_PAYLOAD ) ;
331+ if ( url . includes ( "api.gittensor.io/miners" ) ) return jsonResponse ( [ ] ) ;
332+ return jsonResponse ( null , 404 ) ;
333+ } ;
334+
335+ const result = await fetchSelfReviewContext ( "acme/widgets" , { fetchImpl : fetchImpl as never } ) ;
336+ expect ( manifestRequests ) . toBe ( 4 ) ;
337+ expect ( result . manifest . present ) . toBe ( false ) ;
338+ expect ( result . manifest . warnings ) . toEqual ( [ ] ) ;
339+ } ) ;
340+
341+ it ( "cancels chunked manifest responses once the byte cap is exceeded" , async ( ) => {
342+ const chunks = [ new Uint8Array ( MAX_FOCUS_MANIFEST_BYTES ) , new Uint8Array ( [ 123 ] ) ] ;
343+ let cancelCount = 0 ;
344+ const fetchImpl = async ( url : string ) => {
345+ if ( url . includes ( "raw.githubusercontent.com" ) ) return chunkedManifestResponse ( chunks , ( ) => { cancelCount += 1 ; } ) ;
346+ if ( url . includes ( "/repos/acme/widgets/issues" ) ) return jsonResponse ( [ ] ) ;
347+ if ( url . includes ( "/repos/acme/widgets/pulls" ) ) return jsonResponse ( [ ] ) ;
348+ if ( url . includes ( "/repos/acme/widgets" ) ) return jsonResponse ( REPO_PAYLOAD ) ;
349+ if ( url . includes ( "api.gittensor.io/miners" ) ) return jsonResponse ( [ ] ) ;
350+ return jsonResponse ( null , 404 ) ;
351+ } ;
352+
353+ const result = await fetchSelfReviewContext ( "acme/widgets" , { fetchImpl : fetchImpl as never } ) ;
354+ expect ( cancelCount ) . toBe ( 4 ) ;
355+ expect ( result . manifest . present ) . toBe ( false ) ;
356+ } ) ;
357+
286358 it ( "stops at the first manifest candidate that resolves, skipping the rest" , async ( ) => {
287359 let requestedPaths : string [ ] = [ ] ;
288360 const fetchImpl = async ( url : string ) => {
0 commit comments