@@ -129,3 +129,86 @@ describe('dynamic workflow daemon contracts', () => {
129129 expect ( dynamicClient . dynamicWorkflowMode . value ) . toBe ( false ) ;
130130 } ) ;
131131} ) ;
132+
133+ describe ( 'provider daemon contracts' , ( ) => {
134+ it ( 'adds a provider through one config patch and reads it back' , async ( ) => {
135+ const provider = {
136+ id : 'openai-responses' ,
137+ type : 'openai_responses' ,
138+ base_url : 'https://api.example.test/v1' ,
139+ default_model : 'gpt_5-mini' ,
140+ has_api_key : true ,
141+ status : 'connected' ,
142+ models : [ 'openai-responses/gpt-5-mini' ] ,
143+ } ;
144+ const fetchMock = vi . fn ( )
145+ . mockResolvedValueOnce ( okEnvelope ( { } ) )
146+ . mockResolvedValueOnce ( okEnvelope ( provider ) ) ;
147+ vi . stubGlobal ( 'fetch' , fetchMock ) ;
148+
149+ await expect ( api ( ) . addProvider ( {
150+ type : 'openai_responses' ,
151+ apiKey : 'sk-test' ,
152+ baseUrl : 'https://api.example.test/v1' ,
153+ defaultModel : 'gpt_5-mini' ,
154+ } ) ) . resolves . toMatchObject ( { id : 'openai-responses' , defaultModel : 'gpt_5-mini' } ) ;
155+
156+ expect ( fetchMock ) . toHaveBeenCalledTimes ( 2 ) ;
157+ expect ( fetchMock . mock . calls [ 0 ] ! [ 0 ] ) . toBe ( 'http://example.test:58627/api/v1/config' ) ;
158+ expect ( fetchMock . mock . calls [ 0 ] ! [ 1 ] ) . toMatchObject ( { method : 'POST' } ) ;
159+ expect ( JSON . parse ( ( fetchMock . mock . calls [ 0 ] ! [ 1 ] as RequestInit ) . body as string ) ) . toEqual ( {
160+ providers : {
161+ 'openai-responses' : {
162+ type : 'openai_responses' ,
163+ api_key : 'sk-test' ,
164+ base_url : 'https://api.example.test/v1' ,
165+ default_model : 'gpt_5-mini' ,
166+ } ,
167+ } ,
168+ models : {
169+ 'openai-responses/gpt-5-mini' : {
170+ provider : 'openai-responses' ,
171+ model : 'gpt_5-mini' ,
172+ max_context_size : 262_144 ,
173+ } ,
174+ } ,
175+ default_model : 'openai-responses/gpt-5-mini' ,
176+ } ) ;
177+ expect ( fetchMock . mock . calls [ 1 ] ! [ 0 ] ) . toBe (
178+ 'http://example.test:58627/api/v1/providers/openai-responses' ,
179+ ) ;
180+ expect ( fetchMock . mock . calls [ 1 ] ! [ 1 ] ) . toMatchObject ( { method : 'GET' } ) ;
181+ } ) ;
182+
183+ it ( 'deletes a provider through the provider resource route' , async ( ) => {
184+ const fetchMock = vi . fn ( ) . mockResolvedValueOnce ( okEnvelope ( { deleted : true } ) ) ;
185+ vi . stubGlobal ( 'fetch' , fetchMock ) ;
186+
187+ await expect ( api ( ) . deleteProvider ( 'openai/custom' ) ) . resolves . toEqual ( { deleted : true } ) ;
188+
189+ expect ( fetchMock . mock . calls [ 0 ] ! [ 0 ] ) . toBe (
190+ 'http://example.test:58627/api/v1/providers/openai%2Fcustom' ,
191+ ) ;
192+ expect ( fetchMock . mock . calls [ 0 ] ! [ 1 ] ) . toMatchObject ( { method : 'DELETE' } ) ;
193+ expect ( ( fetchMock . mock . calls [ 0 ] ! [ 1 ] as RequestInit ) . body ) . toBeUndefined ( ) ;
194+ } ) ;
195+
196+ it ( 'refreshes a provider by reading the provider resource' , async ( ) => {
197+ const fetchMock = vi . fn ( ) . mockResolvedValueOnce ( okEnvelope ( {
198+ id : 'openai' ,
199+ type : 'openai' ,
200+ has_api_key : true ,
201+ status : 'connected' ,
202+ models : [ 'openai/gpt-5' ] ,
203+ } ) ) ;
204+ vi . stubGlobal ( 'fetch' , fetchMock ) ;
205+
206+ await expect ( api ( ) . refreshProvider ( 'openai' ) ) . resolves . toMatchObject ( { id : 'openai' } ) ;
207+
208+ expect ( fetchMock . mock . calls [ 0 ] ! [ 0 ] ) . toBe (
209+ 'http://example.test:58627/api/v1/providers/openai' ,
210+ ) ;
211+ expect ( fetchMock . mock . calls [ 0 ] ! [ 1 ] ) . toMatchObject ( { method : 'GET' } ) ;
212+ expect ( ( fetchMock . mock . calls [ 0 ] ! [ 1 ] as RequestInit ) . body ) . toBeUndefined ( ) ;
213+ } ) ;
214+ } ) ;
0 commit comments