@@ -3,30 +3,13 @@ import { AnyApiClient } from '../src/api.js';
33import { formatCatalogPrice } from '../src/format.js' ;
44import type { FetchLike } from '../src/types.js' ;
55
6- const legacyList = {
7- apis : [ {
8- slug : 'reddit.search' ,
9- category : 'social' ,
10- name : 'Reddit Search' ,
11- description : 'Search Reddit' ,
12- fromCredits : 400 ,
13- baseCredits : 5 ,
14- perItemCredits : 10 ,
15- perItemUnit : 'result' ,
16- providers : [ 'hidden-upstream' ] ,
17- quotes : [
18- { fromCredits : 400 , baseCredits : 5 , perItemCredits : 10 , uptimePct : 99.5 , latencyP50Ms : 240 , requests : 80 } ,
19- { fromCredits : 500 , baseCredits : 10 , perItemCredits : 12 } ,
20- ] ,
21- } ] ,
22- } ;
23-
24- const replacementList = {
6+ const catalogResponse = {
257 apis : [ {
268 id : 'reddit.search' ,
279 slug : 'reddit.search' ,
2810 category : 'social' ,
2911 name : 'Reddit Search' ,
12+ description : 'Search Reddit' ,
3013 provider : 'AnyAPI' ,
3114 pricing : {
3215 from : { model : 'linear' , unit : 'result' , baseUsd : 0.00005 , perUnitUsd : 0.0001 , maxUsd : 0.004 } ,
@@ -36,52 +19,50 @@ const replacementList = {
3619 pricing : { model : 'linear' , unit : 'result' , baseUsd : 0.00005 , perUnitUsd : 0.0001 , maxUsd : 0.004 } ,
3720 health : { window : '30d' , uptimePct : 99.5 , latencyP50Ms : 240 , requests : 80 } ,
3821 } ] ,
22+ tryEligible : true ,
3923 } ] ,
4024} ;
4125
42- describe ( 'discovery compatibility reader' , ( ) => {
43- it ( 'normalizes the legacy browse response into nested USD pricing' , async ( ) => {
44- const client = clientFor ( legacyList ) ;
26+ describe ( 'customer-safe discovery reader' , ( ) => {
27+ it ( 'reads browse responses with discriminated nested USD pricing' , async ( ) => {
28+ let requested = '' ;
29+ const client = clientFor ( catalogResponse , ( url ) => { requested = url ; } ) ;
4530 const response = await client . catalog ( { category : 'social' } ) ;
4631
47- expect ( response . apis [ 0 ] ) . toMatchObject ( {
48- slug : 'reddit.search' ,
49- category : 'social' ,
50- name : 'Reddit Search' ,
51- description : 'Search Reddit' ,
52- provider : 'AnyAPI' ,
53- pricing : replacementList . apis [ 0 ] ! . pricing ,
54- } ) ;
55- expect ( response . apis [ 0 ] ! . lanes ) . toHaveLength ( 2 ) ;
56- expect ( response . apis [ 0 ] ! . lanes ?. [ 0 ] ) . toEqual ( replacementList . apis [ 0 ] ! . lanes ?. [ 0 ] ) ;
32+ const url = new URL ( requested ) ;
33+ expect ( url . pathname ) . toBe ( '/catalog' ) ;
34+ expect ( Object . fromEntries ( url . searchParams ) ) . toEqual ( { category : 'social' } ) ;
35+ expect ( response ) . toEqual ( catalogResponse ) ;
5736 expect ( formatCatalogPrice ( response . apis [ 0 ] ! ) ) . toBe (
5837 'from USD 0.00005 + USD 0.0001/result (max USD 0.0040/request)' ,
5938 ) ;
60- expect ( JSON . stringify ( response ) ) . not . toMatch ( / c r e d i t | h i d d e n - u p s t r e a m / i ) ;
39+ expectCustomerSafe ( response ) ;
6140 } ) ;
6241
63- it ( 'passes the replacement browse response through the same customer-safe model' , async ( ) => {
64- const client = clientFor ( replacementList ) ;
65- const response = await client . catalog ( ) ;
66-
67- expect ( response ) . toEqual ( replacementList ) ;
68- } ) ;
69-
70- it ( 'uses dedicated search and adapts the legacy per-1k result' , async ( ) => {
42+ it ( 'uses dedicated ranked search and accepts only relevance and ranking' , async ( ) => {
7143 let requested = '' ;
7244 const client = clientFor ( {
7345 results : [ {
7446 slug : 'amazon.product' ,
47+ platformId : 'amazon' ,
7548 name : 'Amazon Product' ,
49+ description : 'Get product details' ,
7650 category : 'shopping' ,
77- priceUsdPer1k : 5 ,
78- score : 0.8 ,
51+ provider : 'AnyAPI' ,
52+ pricing : {
53+ from : { model : 'flat' , unit : 'request' , maxUsd : 0.005 } ,
54+ failoverMaxUsd : 0.006 ,
55+ } ,
56+ relevance : 0.92 ,
57+ highlightFields : [ { path : 'items[].price' , type : 'number' } ] ,
7958 } ] ,
8059 total : 1 ,
81- mode : 'usecase ' ,
60+ ranking : 'semantic ' ,
8261 } , ( url ) => { requested = url ; } ) ;
8362
84- const response = await client . search ( { query : 'wireless headphones' , category : 'shopping' , platform : 'amazon' , limit : 10 } ) ;
63+ const response = await client . search ( {
64+ query : 'wireless headphones' , category : 'shopping' , platform : 'amazon' , limit : 10 ,
65+ } ) ;
8566
8667 const url = new URL ( requested ) ;
8768 expect ( url . pathname ) . toBe ( '/catalog/search' ) ;
@@ -91,109 +72,91 @@ describe('discovery compatibility reader', () => {
9172 platform : 'amazon' ,
9273 limit : '10' ,
9374 } ) ;
94- expect ( response ) . toEqual ( {
95- results : [ {
96- slug : 'amazon.product' ,
97- name : 'Amazon Product' ,
98- category : 'shopping' ,
99- provider : 'AnyAPI' ,
100- pricing : {
101- from : { model : 'flat' , unit : 'request' , maxUsd : 0.005 } ,
102- failoverMaxUsd : 0.005 ,
103- } ,
104- relevance : 0.8 ,
105- } ] ,
75+ expect ( response ) . toMatchObject ( {
10676 total : 1 ,
107- ranking : 'usecase' ,
108- } ) ;
109- } ) ;
110-
111- it ( 'reads replacement ranked search results without legacy fields' , async ( ) => {
112- const client = clientFor ( {
77+ ranking : 'semantic' ,
11378 results : [ {
11479 slug : 'amazon.product' ,
115- name : 'Amazon Product' ,
11680 provider : 'AnyAPI' ,
117- pricing : {
118- from : { model : 'flat' , unit : 'request' , maxUsd : 0.005 } ,
119- failoverMaxUsd : 0.006 ,
120- } ,
81+ pricing : { from : { model : 'flat' , unit : 'request' , maxUsd : 0.005 } } ,
12182 relevance : 0.92 ,
122- highlightFields : [ { path : 'items[].price' , type : 'number' } ] ,
12383 } ] ,
124- total : 1 ,
125- ranking : 'semantic' ,
126- } ) ;
127-
128- const response = await client . search ( { query : 'product prices' } ) ;
129-
130- expect ( response . ranking ) . toBe ( 'semantic' ) ;
131- expect ( response . results [ 0 ] ) . toMatchObject ( {
132- slug : 'amazon.product' ,
133- provider : 'AnyAPI' ,
134- pricing : { from : { model : 'flat' , unit : 'request' , maxUsd : 0.005 } } ,
135- relevance : 0.92 ,
136- highlightFields : [ { path : 'items[].price' , type : 'number' } ] ,
13784 } ) ;
85+ expectCustomerSafe ( response ) ;
13886 } ) ;
13987
140- it ( 'normalizes a legacy describe response before customer sanitization ' , async ( ) => {
88+ it ( 'reads authenticated detail responses with schemas ' , async ( ) => {
14189 let authorization = '' ;
142- const client = clientFor ( {
143- id : 'youtube.comments' ,
144- slug : 'youtube.comments' ,
145- name : 'YouTube Comments' ,
146- description : 'Read comments' ,
147- priceCredits : 2000 ,
148- fromCredits : 1500 ,
149- baseCredits : 0 ,
150- perItemCredits : 0 ,
151- inputSchema : { type : 'object' } ,
90+ const body = {
91+ ...catalogResponse . apis [ 0 ] ,
92+ inputSchema : { type : 'object' , properties : { query : { type : 'string' } } } ,
15293 outputSchema : { type : 'array' } ,
153- provider : 'hidden-upstream' ,
154- } , undefined , ( init ) => {
94+ heavy : true ,
95+ } ;
96+ const client = clientFor ( body , undefined , ( init ) => {
15597 authorization = new Headers ( init ?. headers ) . get ( 'Authorization' ) ?? '' ;
15698 } , true ) ;
15799
158- const response = await client . describe ( 'youtube.comments ' ) ;
100+ const response = await client . describe ( 'reddit.search ' ) ;
159101
160102 expect ( authorization ) . toBe ( 'Bearer aa_live_test' ) ;
161- expect ( response ) . toEqual ( {
162- id : 'youtube.comments' ,
163- slug : 'youtube.comments' ,
164- name : 'YouTube Comments' ,
165- description : 'Read comments' ,
166- inputSchema : { type : 'object' } ,
167- outputSchema : { type : 'array' } ,
168- provider : 'AnyAPI' ,
169- pricing : {
170- from : { model : 'flat' , unit : 'request' , maxUsd : 0.015 } ,
171- failoverMaxUsd : 0.02 ,
172- } ,
173- } ) ;
174- expect ( JSON . stringify ( response ) ) . not . toMatch ( / c r e d i t | h i d d e n - u p s t r e a m / i) ;
103+ expect ( response ) . toEqual ( body ) ;
104+ expectCustomerSafe ( response ) ;
175105 } ) ;
176106
177- it ( 'reads a replacement describe response with schemas and discriminated pricing ' , async ( ) => {
107+ it ( 'recursively strips forbidden accounting and provider metadata ' , async ( ) => {
178108 const body = {
179- id : 'youtube.comments' ,
180- slug : 'youtube.comments' ,
181- name : 'YouTube Comments' ,
182- provider : 'AnyAPI' ,
183- pricing : {
184- from : { model : 'flat' , unit : 'request' , maxUsd : 0.015 } ,
185- failoverMaxUsd : 0.02 ,
109+ ...catalogResponse . apis [ 0 ] ,
110+ provider : 'hidden-upstream' ,
111+ inputSchema : {
112+ type : 'object' ,
113+ internalCredits : 500 ,
114+ provider : 'hidden-upstream' ,
115+ providers : [ 'hidden-upstream' ] ,
116+ properties : { query : { type : 'string' } } ,
186117 } ,
187- inputSchema : { type : 'object' } ,
188- outputSchema : { type : 'array' } ,
189- heavy : true ,
190118 } ;
191119 const client = clientFor ( body , undefined , undefined , true ) ;
192120
193- expect ( await client . describe ( 'youtube.comments' ) ) . toEqual ( body ) ;
121+ const response = await client . describe ( 'reddit.search' ) ;
122+
123+ expect ( JSON . stringify ( response ) ) . not . toContain ( 'hidden-upstream' ) ;
124+ expectCustomerSafe ( response ) ;
125+ } ) ;
126+
127+ it ( 'rejects discovery entries without nested pricing' , async ( ) => {
128+ const client = clientFor ( {
129+ apis : [ {
130+ slug : 'reddit.search' ,
131+ category : 'social' ,
132+ name : 'Reddit Search' ,
133+ description : 'Search Reddit' ,
134+ provider : 'AnyAPI' ,
135+ } ] ,
136+ } ) ;
137+
138+ await expect ( client . catalog ( ) ) . rejects . toThrow ( 'Invalid AnyAPI API discovery response.' ) ;
194139 } ) ;
195140} ) ;
196141
142+ function expectCustomerSafe ( value : unknown ) : void {
143+ if ( Array . isArray ( value ) ) {
144+ value . forEach ( expectCustomerSafe ) ;
145+ return ;
146+ }
147+ if ( typeof value !== 'object' || value === null ) {
148+ return ;
149+ }
150+ for ( const [ key , child ] of Object . entries ( value ) ) {
151+ expect ( key . toLowerCase ( ) ) . not . toContain ( 'credit' ) ;
152+ expect ( key . toLowerCase ( ) ) . not . toBe ( 'providers' ) ;
153+ if ( key . toLowerCase ( ) === 'provider' ) {
154+ expect ( child ) . toBe ( 'AnyAPI' ) ;
155+ }
156+ expectCustomerSafe ( child ) ;
157+ }
158+ }
159+
197160function clientFor (
198161 body : unknown ,
199162 onUrl ?: ( url : string ) => void ,
0 commit comments