@danhachuel/pandanews 0.4.9 → 0.4.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  Todas as versões publicadas são registradas aqui antes do publish (regra em AGENTS.md). A etiqueta visual usa PATCH de dois dígitos (ex.: 0.1.00); o registry npm normaliza para a versão canônica (ex.: 0.1.0).
4
4
 
5
+ ## 0.4.10 (2026-11-08)
6
+
7
+ - **Cobertura E2E pelo caminho HTTP real**: o teste de fallback ganhou um cenário que substitui apenas o `httpx.Client` — `chamar_provedor`, a construção da URL, os headers e o `raise_for_status` rodam de verdade. A NVIDIA devolve 410, o Gemini devolve 200 e a notícia nasce do Gemini com o log `Erro na NVIDIA (...). Acionando Google Gemini...` emitido. Fecha a lacuna de testar só a função mockada
8
+ - Teste sobe para 16 verificações, todas verdes local e no CI
9
+
5
10
  ## 0.4.09 (2026-11-08)
6
11
 
7
12
  - **Frontend — Gemini tratado como a NVIDIA no teste de conexão**: o badge agora distingue `degraded` (âmbar, modelo em fim de vida 404/410) de `falhou` (vermelho) e a mensagem de detalhe usa a cor correspondente. Antes um 404/410 do Gemini aparecia como "falhou" vermelho — agora o usuário entende que é o modelo, não a chave, e que a geração segue pela NVIDIA
@@ -31,7 +31,7 @@ async def lifespan(_: FastAPI):
31
31
  scheduler.parar()
32
32
 
33
33
 
34
- app = FastAPI(title="PandaNews API", version="0.4.9", lifespan=lifespan)
34
+ app = FastAPI(title="PandaNews API", version="0.4.10", lifespan=lifespan)
35
35
 
36
36
  app.add_middleware(
37
37
  CORSMiddleware,
@@ -13,8 +13,10 @@ Cobre exatamente o que a correção promete:
13
13
  """
14
14
  from __future__ import annotations
15
15
 
16
+ import json
16
17
  import logging
17
18
  import sys
19
+ import types
18
20
  from datetime import timedelta as _timedelta
19
21
  from pathlib import Path
20
22
 
@@ -165,7 +167,7 @@ resultado_410 = _testar_com_resposta(
165
167
  check(resultado_410["ok"] is False, "test_provider: HTTP 410 -> ok=False")
166
168
  check(resultado_410.get("degraded") is True, "test_provider: HTTP 410 -> degraded=True (fallback avisa)")
167
169
  check(
168
- "fim da vida" in resultado_410["detail"] or "410" in resultado_410["detail"],
170
+ "fim de vida" in resultado_410["detail"] or "410" in resultado_410["detail"],
169
171
  "test_provider: a string de erro do provedor volta no JSON para a tela",
170
172
  )
171
173
 
@@ -181,6 +183,67 @@ check(
181
183
  "test_provider: HTTP 500 -> código e corpo devolvidos ao frontend",
182
184
  )
183
185
 
186
+ # ------------------------------------ 3b) E2E pelo caminho HTTP real (Chamar)
187
+
188
+ # Diferente dos cenários acima (que trocam `chamar_provedor` por um dublê),
189
+ # aqui só o `httpx.Client` é substituído: `chamar_provedor`, a construção da
190
+ # URL, os headers e o `raise_for_status` rodam de verdade. A NVIDIA devolve
191
+ # 410 e o Gemini devolve 200 — a notícia tem de nascer do Gemini.
192
+ llm_router.chamar_provedor = original_chamar
193
+
194
+
195
+ class _RespostaHttp:
196
+ def __init__(self, status_code: int, corpo: dict) -> None:
197
+ self.status_code = status_code
198
+ self._corpo = corpo
199
+ self.text = json.dumps(corpo)
200
+
201
+ def raise_for_status(self) -> None:
202
+ if self.status_code >= 400:
203
+ raise RuntimeError(f"HTTP {self.status_code}: {self.text}")
204
+
205
+ def json(self) -> dict:
206
+ return self._corpo
207
+
208
+
209
+ def _post_e2e(self, endpoint, headers=None, json=None):
210
+ # distingue pelo modelo pedido: nvidia (410) vs google/gemini (200)
211
+ modelo = (json or {}).get("model", "")
212
+ if "gemini" in modelo:
213
+ return _RespostaHttp(
214
+ 200,
215
+ {"choices": [{"message": {"content": '{"titulo":"T","resumo":"R","corpo":"C"}'}}], "model": modelo},
216
+ )
217
+ return _RespostaHttp(410, {"detail": "The model has reached its end of life"})
218
+
219
+
220
+ class _ClientE2E:
221
+ def __init__(self, *a, **k) -> None:
222
+ pass
223
+
224
+ def __enter__(self):
225
+ return self
226
+
227
+ def __exit__(self, *a):
228
+ return False
229
+
230
+ def post(self, endpoint, headers=None, json=None):
231
+ return _post_e2e(self, endpoint, headers, json)
232
+
233
+
234
+ llm_router.httpx.Client = _ClientE2E
235
+
236
+ registros.clear()
237
+
238
+ texto_e2e, provedor_e2e = llm_router.gerar_texto(_db_com_chaves(), "escreva uma noticia")
239
+
240
+ check(provedor_e2e == "gemini", "E2E HTTP: NVIDIA 410 -> Gemini assume (provedor: gemini)")
241
+ check("titulo" in texto_e2e, "E2E HTTP: o conteúdo devolvido é o do Gemini (JSON utilizável)")
242
+ check(
243
+ any("Erro na NVIDIA (" in m and "Acionando Google Gemini" in m for m in registros),
244
+ "E2E HTTP: log 'Erro na NVIDIA (...). Acionando Google Gemini...' emitido",
245
+ )
246
+
184
247
  # ------------------------------------------- 4) endpoint replicado do Thunderbolt
185
248
 
186
249
  check(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/pandanews",
3
- "version": "0.4.9",
3
+ "version": "0.4.10",
4
4
  "description": "SaaS local de criação e automação de múltiplos blogs de notícias com IA (FastAPI + React + Tailwind).",
5
5
  "license": "MIT",
6
6
  "type": "module",