Stage 0: Repository und Architekturgrundlage

- Pyproject.toml mit FastAPI, Pydantic v2, SQLAlchemy, httpx, asyncio,
  BeautifulSoup4, selectolax, trafilatura, uvicorn, pytest-asyncio
- Multi-stage Dockerfile (Python 3.12-slim, Non-Root-User nsct)
- docker-compose.yml (nsct-api + postgres + optional searxng)
- .env.example mit allen Config-Parametern
- Config-System: AppSettings mit LLMConfig, VisionConfig, AudioConfig,
  DatabaseConfig — komplett aus Environment, keine Hardcodes
- Strukturiertes Logging mit research_id/llm_request_id Tracking
- Pydantic v2 Schemas: SearchQuery, Source, Claim, EvidenceRelation,
  CitationEdge, ResearchReport
- SQLAlchemy 2.0 Declarative Models + async Engine Factory
- SSRF-Schutz: URL-Validation, IP-Blocklist (RFC1918, Cloud Metadata,
  file://, ftp://)
- Provider-Interfaces: LLMProvider, VisionProvider, AudioProvider,
  SearchProvider, ContentFetcher als ABCs
- Health-Endpoints: /health, /ready (LLM-Connect-Test), /providers
- FastAPI App mit CORS, lifespan (LLM Pre-Flight)
- CLI-Stub mit Entry-Points: nsct, nsct-core, nsct-api
- 6 Test-Cases: /health, /ready, /providers + No-Secrets-Test
- Vollständige Dokumentation: README, ARCHITECTURE, SECURITY,
  METHODOLOGY, API, DEPLOYMENT
- .gitignore (Python, Docker, IDE, .env)
This commit is contained in:
NSCT Agent
2026-08-23 11:33:45 +00:00
commit e9410be941
28 changed files with 4192 additions and 0 deletions

50
tests/conftest.py Normal file
View File

@@ -0,0 +1,50 @@
"""pytest fixtures for NSCT tests."""
from __future__ import annotations
import os
from typing import AsyncGenerator
import pytest
import pytest_asyncio
from fastapi.testclient import TestClient
from httpx import AsyncClient
from nsct.api.main import create_app
from nsct.config import AppSettings
@pytest.fixture(scope="session")
def app() -> AppSettings:
"""Minimal AppSettings for test environment."""
return AppSettings.from_env()
@pytest.fixture(scope="session")
def client(app: AppSettings):
"""Synchronous test client for the FastAPI app."""
fastapi_app = create_app()
with TestClient(fastapi_app) as c:
yield c
@pytest_asyncio.fixture(scope="function")
async def async_client(app: AppSettings) -> AsyncGenerator[AsyncClient, None]:
"""Async test client for the FastAPI app."""
fastapi_app = create_app()
async with AsyncClient(app=fastapi_app, base_url="http://test") as ac:
yield ac
@pytest.fixture(scope="function")
def clean_env(monkeypatch: pytest.MonkeyPatch) -> None:
"""Ensure environment has the minimum required vars for AppSettings."""
monkeypatch.setenv("NSCT_LLM_BASE_URL", "http://localhost:8030/openai/v1")
monkeypatch.setenv("NSCT_LLM_MODEL", "test-model")
monkeypatch.setenv("NSCT_LLM_MAX_CONCURRENCY", "1")
monkeypatch.setenv("NSCT_VISION_BASE_URL", "http://localhost:8030/openai/visual/v1")
monkeypatch.setenv("NSCT_VISION_MODEL", "test-model")
monkeypatch.setenv("NSCT_AUDIO_BASE_URL", "http://localhost:8030/hermes-audio")
monkeypatch.setenv("NSCT_AUDIO_MODEL", "default")
monkeypatch.setenv("NSCT_DB_URL", "sqlite+aiosqlite:///:memory:")
monkeypatch.setenv("NSCT_DEBUG", "false")

78
tests/test_health.py Normal file
View File

@@ -0,0 +1,78 @@
"""Tests for the health endpoints (/health, /ready, /providers)."""
from __future__ import annotations
from typing import Generator
import pytest
from fastapi.testclient import TestClient
@pytest.fixture
def client() -> Generator[TestClient, None, None]:
"""Synchronous test client."""
from nsct.api.main import create_app
app = create_app()
with TestClient(app) as c:
yield c
# ---------------------------------------------------------------------------
# /health
# ---------------------------------------------------------------------------
def test_health_returns_ok(client: TestClient) -> None:
"""GET /health should return status ok and the version."""
resp = client.get("/health")
assert resp.status_code == 200
body = resp.json()
assert body["status"] == "ok"
assert "version" in body
assert body["version"] == "0.1.0"
# ---------------------------------------------------------------------------
# /ready
# ---------------------------------------------------------------------------
def test_ready_returns_response(client: TestClient) -> None:
"""GET /ready should return a dict with status, llm key, etc."""
resp = client.get("/ready")
assert resp.status_code == 200
body = resp.json()
assert "status" in body
assert "llm" in body
def test_ready_status_field(client: TestClient) -> None:
"""GET /ready status should be 'ready' or 'not_ready'."""
resp = client.get("/ready")
body = resp.json()
assert body["status"] in ("ready", "not_ready")
# ---------------------------------------------------------------------------
# /providers
# ---------------------------------------------------------------------------
def test_providers_returns_provider_info(client: TestClient) -> None:
"""GET /providers should return keys for llm, vision, audio."""
resp = client.get("/providers")
assert resp.status_code == 200
body = resp.json()
assert "llm" in body
assert "vision" in body
assert "audio" in body
def test_providers_no_secrets(client: TestClient) -> None:
"""GET /providers must not contain secrets."""
resp = client.get("/providers")
body = resp.json()
flat = str(body)
assert "API_KEY" not in flat
assert "secret" not in flat.lower().replace("available", "")