@@ -179,6 +179,72 @@ describe("small adapters and normalizers", () => {
179179 expect ( profile . topLanguages ) . toContain ( "Go" ) ;
180180 } ) ;
181181
182+ it ( "REGRESSION (#8891): a repos-list JSON-parse failure keeps the fetched user and only clears topLanguages" , async ( ) => {
183+ // HTTP non-ok already degraded this way; a truncated/invalid JSON body after reposResponse.ok previously
184+ // discarded the whole profile as source: "unavailable".
185+ vi . stubGlobal ( "fetch" , async ( input : RequestInfo | URL ) => {
186+ const url = input . toString ( ) ;
187+ if ( url . endsWith ( "/users/parsefail" ) ) {
188+ return Response . json ( {
189+ login : "parsefail" ,
190+ name : "Parse Fail" ,
191+ bio : "still here" ,
192+ company : "Acme" ,
193+ public_repos : 4 ,
194+ followers : 2 ,
195+ created_at : "2026-01-01T00:00:00Z" ,
196+ updated_at : "2026-02-01T00:00:00Z" ,
197+ } ) ;
198+ }
199+ if ( url . includes ( "/users/parsefail/repos?" ) ) {
200+ return new Response ( "{not-json" , {
201+ status : 200 ,
202+ headers : { "content-type" : "application/json" } ,
203+ } ) ;
204+ }
205+ return new Response ( "not found" , { status : 404 } ) ;
206+ } ) ;
207+
208+ const profile = await fetchPublicContributorProfile ( "parsefail" ) ;
209+ expect ( profile ) . toMatchObject ( {
210+ login : "parsefail" ,
211+ name : "Parse Fail" ,
212+ bio : "still here" ,
213+ company : "Acme" ,
214+ publicRepos : 4 ,
215+ followers : 2 ,
216+ createdAt : "2026-01-01T00:00:00Z" ,
217+ updatedAt : "2026-02-01T00:00:00Z" ,
218+ topLanguages : [ ] ,
219+ source : "github" ,
220+ } ) ;
221+ expect ( profile . source ) . not . toBe ( "unavailable" ) ;
222+ } ) ;
223+
224+ it ( "stops paginating when a later repos page returns unparseable JSON (#8891)" , async ( ) => {
225+ vi . stubGlobal ( "fetch" , async ( input : RequestInfo | URL ) => {
226+ const url = input . toString ( ) ;
227+ if ( url . endsWith ( "/users/pageparse" ) ) return Response . json ( { login : "pageparse" , public_repos : 200 } ) ;
228+ if ( url . includes ( "/pageparse/repos?" ) && ! url . includes ( "page=2" ) ) {
229+ return Response . json (
230+ Array . from ( { length : 100 } , ( ) => ( { language : "Go" } ) ) ,
231+ { headers : { link : '<https://api.github.com/users/pageparse/repos?page=2>; rel="next"' } } ,
232+ ) ;
233+ }
234+ if ( url . includes ( "/pageparse/repos?" ) && url . includes ( "page=2" ) ) {
235+ return new Response ( "{truncated" , {
236+ status : 200 ,
237+ headers : { "content-type" : "application/json" } ,
238+ } ) ;
239+ }
240+ return new Response ( "not found" , { status : 404 } ) ;
241+ } ) ;
242+
243+ const profile = await fetchPublicContributorProfile ( "pageparse" ) ;
244+ expect ( profile . source ) . toBe ( "github" ) ;
245+ expect ( profile . topLanguages ) . toContain ( "Go" ) ;
246+ } ) ;
247+
182248 it ( "authenticates public profile requests with GITHUB_PUBLIC_TOKEN to lift the rate ceiling (#790)" , async ( ) => {
183249 const authHeaders : Array < string | null > = [ ] ;
184250 vi . stubGlobal ( "fetch" , async ( input : RequestInfo | URL , init ?: RequestInit ) => {
0 commit comments