1- import { describe , it , expect , vi } from 'vitest' ;
1+ import { describe , it , expect , vi , afterEach } from 'vitest' ;
22import { DocFetchTool , normalizeDocUrl } from './doc-fetch.js' ;
33
4+ const createMockResponse = ( {
5+ content,
6+ contentType = 'text/html' ,
7+ status = 200 ,
8+ statusText = 'OK' ,
9+ } : {
10+ content : string ;
11+ contentType ?: string ;
12+ status ?: number ;
13+ statusText ?: string ;
14+ } ) =>
15+ new Response ( content , {
16+ status,
17+ statusText,
18+ headers : { 'content-type' : contentType } ,
19+ } ) ;
20+
21+ const stubFetch = ( factory : ( ) => Response ) => {
22+ const fetchMock = vi . fn ( ) . mockImplementation ( ( ) => Promise . resolve ( factory ( ) ) ) ;
23+ vi . stubGlobal ( 'fetch' , fetchMock ) ;
24+ return fetchMock ;
25+ } ;
26+
427describe ( 'DocFetchTool' , ( ) => {
528 const tool = new DocFetchTool ( ) ;
629
30+ afterEach ( ( ) => {
31+ vi . clearAllMocks ( ) ;
32+ vi . unstubAllGlobals ( ) ;
33+ } ) ;
34+
735 describe ( 'URL validation' , ( ) => {
836 it ( 'should accept valid HF and Gradio docs URLs' , ( ) => {
937 const validUrls = [
@@ -38,13 +66,30 @@ describe('DocFetchTool', () => {
3866 } ) ;
3967
4068 describe ( 'document chunking' , ( ) => {
69+ it ( 'uses markdown content from host when available' , async ( ) => {
70+ const markdown = '# Heading\nBody content' ;
71+ const fetchMock = stubFetch ( ( ) =>
72+ createMockResponse ( {
73+ content : markdown ,
74+ contentType : 'text/markdown' ,
75+ } ) ,
76+ ) ;
77+
78+ const result = await tool . fetch ( { doc_url : 'https://huggingface.co/docs/test' } ) ;
79+ expect ( fetchMock ) . toHaveBeenCalledWith ( 'https://huggingface.co/docs/test' , {
80+ headers : { accept : 'text/markdown' } ,
81+ } ) ;
82+ expect ( result ) . toBe ( markdown ) ;
83+ } ) ;
84+
4185 it ( 'should return small documents without chunking' , async ( ) => {
4286
4387 // Mock fetch to return HTML that converts to short markdown
44- global . fetch = vi . fn ( ) . mockResolvedValue ( {
45- ok : true ,
46- text : ( ) => Promise . resolve ( '<h1>Short Document</h1><p>This is a short document.</p>' ) ,
47- } ) ;
88+ stubFetch ( ( ) =>
89+ createMockResponse ( {
90+ content : '<h1>Short Document</h1><p>This is a short document.</p>' ,
91+ } ) ,
92+ ) ;
4893
4994 const result = await tool . fetch ( { doc_url : 'https://huggingface.co/docs/test' } ) ;
5095
@@ -57,10 +102,11 @@ describe('DocFetchTool', () => {
57102 // Mock fetch to return HTML that converts to long markdown
58103 const longHtml = '<h1>Long Document</h1>' + '<p>This is a very long sentence that will be repeated many times to create a document that exceeds the 7500 token limit for testing chunking functionality.</p>' . repeat ( 200 ) ;
59104
60- global . fetch = vi . fn ( ) . mockResolvedValue ( {
61- ok : true ,
62- text : ( ) => Promise . resolve ( longHtml ) ,
63- } ) ;
105+ stubFetch ( ( ) =>
106+ createMockResponse ( {
107+ content : longHtml ,
108+ } ) ,
109+ ) ;
64110
65111 const result = await tool . fetch ( { doc_url : 'https://huggingface.co/docs/test' } ) ;
66112
@@ -74,21 +120,51 @@ describe('DocFetchTool', () => {
74120 { in : 'https://gradio.app/guides/x' , out : 'https://www.gradio.app/guides/x' } ,
75121 { in : 'https://www.gradio.app/guides/x' , out : 'https://www.gradio.app/guides/x' } ,
76122 { in : 'https://huggingface.co/docs/transformers' , out : 'https://huggingface.co/docs/transformers' } ,
123+ { in : '/docs/diffusers/index' , out : 'https://huggingface.co/docs/diffusers/index' } ,
124+ { in : './docs/diffusers/index' , out : 'https://huggingface.co/docs/diffusers/index' } ,
77125 { in : 'not a url' , out : 'not a url' } ,
78126 ] ;
79127 for ( const c of cases ) {
80128 expect ( normalizeDocUrl ( c . in ) ) . toBe ( c . out ) ;
81129 }
82130 } ) ;
83131
132+ it ( 'normalizes relative doc paths to the huggingface docs host' , async ( ) => {
133+ const fetchMock = stubFetch ( ( ) =>
134+ createMockResponse ( {
135+ content : '<h1>Title</h1><p>Body</p>' ,
136+ } ) ,
137+ ) ;
138+
139+ const result = await tool . fetch ( { doc_url : '/docs/test' } ) ;
140+ expect ( fetchMock ) . toHaveBeenCalledWith ( 'https://huggingface.co/docs/test' , {
141+ headers : { accept : 'text/markdown' } ,
142+ } ) ;
143+ expect ( result ) . toContain ( '# Title' ) ;
144+ } ) ;
145+
146+ it ( 'normalizes ./docs paths to the huggingface docs host' , async ( ) => {
147+ const fetchMock = stubFetch ( ( ) =>
148+ createMockResponse ( {
149+ content : '<h1>Another Title</h1><p>Body</p>' ,
150+ } ) ,
151+ ) ;
152+
153+ await tool . fetch ( { doc_url : './docs/another' } ) ;
154+ expect ( fetchMock ) . toHaveBeenCalledWith ( 'https://huggingface.co/docs/another' , {
155+ headers : { accept : 'text/markdown' } ,
156+ } ) ;
157+ } ) ;
158+
84159 it ( 'should return subsequent chunks with offset' , async ( ) => {
85160 // Mock fetch to return the same long HTML
86161 const longHtml = '<h1>Long Document</h1>' + '<p>This is a very long sentence that will be repeated many times to create a document that exceeds the 7500 token limit for testing chunking functionality.</p>' . repeat ( 200 ) ;
87162
88- global . fetch = vi . fn ( ) . mockResolvedValue ( {
89- ok : true ,
90- text : ( ) => Promise . resolve ( longHtml ) ,
91- } ) ;
163+ stubFetch ( ( ) =>
164+ createMockResponse ( {
165+ content : longHtml ,
166+ } ) ,
167+ ) ;
92168
93169 // Get first chunk
94170 const firstChunk = await tool . fetch ( { doc_url : 'https://huggingface.co/docs/test' } ) ;
@@ -106,10 +182,11 @@ describe('DocFetchTool', () => {
106182 } ) ;
107183
108184 it ( 'should handle offset beyond document length' , async ( ) => {
109- global . fetch = vi . fn ( ) . mockResolvedValue ( {
110- ok : true ,
111- text : ( ) => Promise . resolve ( '<h1>Short Document</h1><p>This is short.</p>' ) ,
112- } ) ;
185+ stubFetch ( ( ) =>
186+ createMockResponse ( {
187+ content : '<h1>Short Document</h1><p>This is short.</p>' ,
188+ } ) ,
189+ ) ;
113190
114191 const result = await tool . fetch ( { doc_url : 'https://huggingface.co/docs/test' , offset : 10000 } ) ;
115192
0 commit comments