Skip to content

Commit 4207df0

Browse files
JuliaEdomclaude
andcommitted
test: DI seam + mocked API coverage 58→89% (Kimi P1.6)
Extract Singletons NamedTuple + get_singletons() Depends-factory so routes can be exercised through app.dependency_overrides[get_singletons]. Bootstrap path stays behind @lru_cache(maxsize=1) for production callers (no behavior change). New tests/api/test_api_routes_mocked.py (13 tests) covers: - /readyz: healthy / empty-chroma / empty-registry / factory-raises - /databases: list + table_count / auth-required / auth-accepted / schema collection raises - /ask: unknown db_id 404 / canned PipelineRunResult passthrough / error kind propagation / confidence label buckets (High/Medium/Low/Unknown) - rate-limit: 60 OK → 61st 429 with Retry-After src/nl_sql/api/main.py coverage 58% → 89% (only the live Mistral/Chroma bootstrap remains uncovered, which is correct — that path needs real infrastructure). 370 pytest pass (was 357 + 13 new), ruff + mypy strict clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a7c1d81 commit 4207df0

2 files changed

Lines changed: 360 additions & 16 deletions

File tree

‎src/nl_sql/api/main.py‎

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from collections import defaultdict, deque
2525
from functools import lru_cache
2626
from pathlib import Path
27-
from typing import Any
27+
from typing import Any, NamedTuple
2828

2929
from fastapi import Depends, FastAPI, Header, HTTPException, Request, status
3030
from pydantic import BaseModel, Field
@@ -221,8 +221,21 @@ def _build_pipeline_components(
221221
return registry, schema_index, sql_provider, explain_provider
222222

223223

224+
class Singletons(NamedTuple):
225+
"""The four runtime objects the API routes share.
226+
227+
Exposed as a public type so tests can construct mock instances and feed
228+
them through ``app.dependency_overrides[get_singletons]``.
229+
"""
230+
231+
pipeline: Any
232+
registry: DatabaseRegistry
233+
schema_index: SchemaIndex
234+
sql_provider: LLMProvider
235+
236+
224237
@lru_cache(maxsize=1)
225-
def _make_singletons() -> tuple[Any, DatabaseRegistry, SchemaIndex, LLMProvider]:
238+
def _make_singletons() -> Singletons:
226239
"""Lazy: build the pipeline only when the first /ask hits — keeps /healthz
227240
fast and avoids touching Chroma when the API is used for status probes."""
228241
import os
@@ -244,7 +257,12 @@ def _make_singletons() -> tuple[Any, DatabaseRegistry, SchemaIndex, LLMProvider]
244257
use_dac_prompt=os.environ.get("NLSQL_DAC") == "1",
245258
)
246259
pipeline = build_pipeline(config)
247-
return pipeline, registry, schema_index, sql_provider
260+
return Singletons(pipeline, registry, schema_index, sql_provider)
261+
262+
263+
def get_singletons() -> Singletons:
264+
"""FastAPI Depends-able factory; tests override via ``app.dependency_overrides``."""
265+
return _make_singletons()
248266

249267

250268
def create_app() -> FastAPI:
@@ -311,10 +329,11 @@ def readyz() -> ReadyResponse:
311329
registered = 0
312330
schema_chunks = 0
313331
try:
314-
_pipeline, registry, schema_index, _sql = _make_singletons()
315-
registered = len(registry.ids())
332+
factory: Any = app.dependency_overrides.get(get_singletons, get_singletons)
333+
singletons: Singletons = factory()
334+
registered = len(singletons.registry.ids())
316335
registry_ok = registered > 0
317-
schema_chunks = schema_index.schema_collection.count()
336+
schema_chunks = singletons.schema_index.schema_collection.count()
318337
chroma_ok = schema_chunks > 0
319338
except Exception:
320339
pass
@@ -330,13 +349,15 @@ def readyz() -> ReadyResponse:
330349
# --------------------------------------------------------- product API
331350

332351
@app.get("/databases", response_model=DatabasesResponse, tags=["catalog"])
333-
def databases(_auth: str = Depends(require_api_key)) -> DatabasesResponse:
334-
_pipeline, registry, schema_index, _sql = _make_singletons()
352+
def databases(
353+
_auth: str = Depends(require_api_key),
354+
singletons: Singletons = Depends(get_singletons), # noqa: B008
355+
) -> DatabasesResponse:
335356
infos: list[DatabaseInfo] = []
336-
for db_id in registry.ids():
337-
spec = registry.get(db_id)
357+
for db_id in singletons.registry.ids():
358+
spec = singletons.registry.get(db_id)
338359
try:
339-
records = schema_index.schema_collection.get(
360+
records = singletons.schema_index.schema_collection.get(
340361
where={"db_id": db_id}, include=["metadatas"]
341362
)
342363
table_count = len(records.get("metadatas") or [])
@@ -353,18 +374,21 @@ def databases(_auth: str = Depends(require_api_key)) -> DatabasesResponse:
353374
return DatabasesResponse(databases=infos)
354375

355376
@app.post("/ask", response_model=AskResponse, tags=["nl-sql"])
356-
def ask(req: AskRequest, _auth: str = Depends(require_api_key)) -> AskResponse:
357-
pipeline, registry, _schema, _sql = _make_singletons()
358-
if req.db_id not in registry.ids():
377+
def ask(
378+
req: AskRequest,
379+
_auth: str = Depends(require_api_key),
380+
singletons: Singletons = Depends(get_singletons), # noqa: B008
381+
) -> AskResponse:
382+
if req.db_id not in singletons.registry.ids():
359383
raise HTTPException(
360384
status_code=status.HTTP_404_NOT_FOUND,
361385
detail=f"unknown db_id: {req.db_id!r}; see /databases for the list",
362386
)
363-
spec = registry.get(req.db_id)
387+
spec = singletons.registry.get(req.db_id)
364388
t0 = time.perf_counter()
365389
try:
366390
result = run_pipeline(
367-
pipeline,
391+
singletons.pipeline,
368392
question=req.question,
369393
db_id=req.db_id,
370394
dialect=spec.dialect,

0 commit comments

Comments
 (0)