@@ -97,3 +97,123 @@ describe("buildNativeMediaParts — regressão da visão nativa", () => {
9797 expect ( parts ) . toEqual ( [ ] ) ;
9898 } ) ;
9999} ) ;
100+
101+ /**
102+ * ═══ O ROTEADOR: o call site REAL consulta o catálogo, e o direto NÃO ═══════
103+ *
104+ * A regra vive em `lib/ai/pontos/capacidade-em-vigor.ts` e tem teste próprio.
105+ * O que faltava é o que este bloco guarda: que ESTE arquivo a chama, e com o
106+ * catálogo ligado. Sem isso, trocar `visaoEmVigor` de volta por
107+ * `modelCapabilities(...).image` deixaria a suíte verde e o motor voltaria a
108+ * anexar a imagem a um modelo que a recusa — teste guarda a função, não o
109+ * call site.
110+ *
111+ * ⚠️ O dublê deste arquivo NÃO tinha `.from`, então nenhum caso passava pelo
112+ * ramo do roteador. A ausência de `.from` também é o caso de "banco fora", e
113+ * ele tem um teste aqui de propósito: falha de catálogo não pode derrubar o
114+ * turno — cai no palpite, que é o comportamento de antes da regra existir.
115+ */
116+ function fakeAdminComCatalogo ( bytes : Buffer , supports_vision : boolean | null ) {
117+ const consultas : string [ ] = [ ] ;
118+ const chain : Record < string , unknown > = { } ;
119+ chain . select = ( ) => chain ;
120+ chain . eq = ( _c : string , v : string ) => {
121+ consultas . push ( v ) ;
122+ return chain ;
123+ } ;
124+ chain . is = ( ) => chain ;
125+ chain . maybeSingle = async ( ) => ( { data : supports_vision === null ? null : { supports_vision } } ) ;
126+ return {
127+ consultas,
128+ admin : {
129+ from : ( ) => chain ,
130+ storage : {
131+ from : ( ) => ( {
132+ download : vi . fn ( async ( ) => ( {
133+ data : { arrayBuffer : async ( ) => bytes . buffer . slice ( bytes . byteOffset , bytes . byteOffset + bytes . byteLength ) } ,
134+ error : null ,
135+ } ) ) ,
136+ } ) ,
137+ } ,
138+ } as never ,
139+ } ;
140+ }
141+
142+ describe ( "buildNativeMediaParts — no roteador quem decide é o catálogo" , ( ) => {
143+ const bytes = Buffer . from ( [ 0xff , 0xd8 , 0xff , 0xe0 ] ) ;
144+
145+ it ( "openrouter + catálogo diz que NÃO enxerga → nenhuma parte nativa" , async ( ) => {
146+ // O defeito que este caso existe para impedir: o registro casa o prefixo
147+ // `openai/` e afirma que enxerga, o motor anexa os bytes, e o provedor
148+ // recusa — derrubando a resposta daquela mensagem para o cliente.
149+ const db = fakeAdminComCatalogo ( bytes , false ) ;
150+ const parts = await buildNativeMediaParts ( {
151+ messages : [ imageInbound ] ,
152+ provider : "openrouter" ,
153+ model : "openai/gpt-3.5-turbo" ,
154+ multimodalInput : true ,
155+ admin : db . admin ,
156+ } ) ;
157+ expect ( parts ) . toEqual ( [ ] ) ;
158+ expect ( db . consultas , "o catálogo nem foi consultado" ) . toContain ( "openai/gpt-3.5-turbo" ) ;
159+ } ) ;
160+
161+ it ( "openrouter + catálogo diz que enxerga → a parte nativa vai" , async ( ) => {
162+ // Controle: sem ele, "no roteador nunca anexe" satisfaria o caso acima e a
163+ // visão morreria para quem usa OpenRouter com um modelo que enxerga.
164+ const db = fakeAdminComCatalogo ( bytes , true ) ;
165+ const parts = await buildNativeMediaParts ( {
166+ messages : [ imageInbound ] ,
167+ provider : "openrouter" ,
168+ model : "openai/gpt-4o" ,
169+ multimodalInput : true ,
170+ admin : db . admin ,
171+ } ) ;
172+ expect ( parts ) . toHaveLength ( 1 ) ;
173+ } ) ;
174+
175+ it ( "openrouter SEM linha no catálogo → cai no prefixo, que é melhor que nada" , async ( ) => {
176+ const db = fakeAdminComCatalogo ( bytes , null ) ;
177+ const parts = await buildNativeMediaParts ( {
178+ messages : [ imageInbound ] ,
179+ provider : "openrouter" ,
180+ model : "openai/gpt-4o" ,
181+ multimodalInput : true ,
182+ admin : db . admin ,
183+ } ) ;
184+ expect ( parts ) . toHaveLength ( 1 ) ;
185+ } ) ;
186+
187+ it ( "⚠️ provedor DIRETO não paga ida ao banco — o dublê explode se for consultado" , async ( ) => {
188+ // Guarda de custo: um roundtrip por turno com mídia, para confirmar o que o
189+ // registro já sabe. Se alguém tirar o atalho de `visaoEmVigor`, este caso
190+ // vira vermelho em vez de a conta do banco subir em silêncio.
191+ const db = fakeAdminComCatalogo ( bytes , false ) ;
192+ ( db . admin as unknown as { from : ( ) => never } ) . from = ( ) => {
193+ throw new Error ( "provedor direto NÃO pode consultar o catálogo" ) ;
194+ } ;
195+ const parts = await buildNativeMediaParts ( {
196+ messages : [ imageInbound ] ,
197+ provider : "openai" ,
198+ model : "gpt-4o" ,
199+ multimodalInput : true ,
200+ admin : db . admin ,
201+ } ) ;
202+ expect ( parts ) . toHaveLength ( 1 ) ;
203+ } ) ;
204+
205+ it ( "catálogo fora do ar não derruba o turno — cai no palpite" , async ( ) => {
206+ const db = fakeAdminComCatalogo ( bytes , false ) ;
207+ ( db . admin as unknown as { from : ( ) => never } ) . from = ( ) => {
208+ throw new Error ( "banco fora" ) ;
209+ } ;
210+ const parts = await buildNativeMediaParts ( {
211+ messages : [ imageInbound ] ,
212+ provider : "openrouter" ,
213+ model : "openai/gpt-4o" ,
214+ multimodalInput : true ,
215+ admin : db . admin ,
216+ } ) ;
217+ expect ( parts ) . toHaveLength ( 1 ) ;
218+ } ) ;
219+ } ) ;
0 commit comments