feat(03-04): retire flat-file settings; wire per-user AI config via DB lookup
- config.py: Remove SETTINGS_FILE, DEFAULT_SYSTEM_PROMPT, DEFAULT_SETTINGS constants; add system_prompt, default_ai_provider, default_ai_model to Settings - services/classifier.py: Add _DEFAULT_SYSTEM_PROMPT module constant; classify_document and suggest_topics_for_document accept ai_provider/ai_model kwargs; no longer calls storage.load_settings() — uses app_settings defaults with DB-supplied overrides (D-14, D-15) - services/storage.py: Delete load_settings, save_settings, mask_api_key, settings_masked; remove from __all__; remove import copy, json, DEFAULT_SETTINGS, SETTINGS_FILE (D-12) - tasks/document_tasks.py: _run resolves user.ai_provider/ai_model via session.get(User, doc.user_id) and passes through to classifier; task signature unchanged (T-03-19) - api/settings.py: Deleted — /api/settings endpoint removed (D-12) - main.py: Remove settings_router import and include_router call - tests/test_settings.py: Replace all tests with test_settings_endpoint_removed (404, green) - tests/test_classifier.py: Implement test_per_user_provider, test_celery_task_uses_user_provider, test_default_provider_fallback; remove xfail markers (DOC-03, DOC-05)
This commit is contained in:
@@ -1,123 +1,13 @@
|
||||
"""
|
||||
Settings API tests — async only (Plan 05 cutover).
|
||||
Settings API tests — Phase 3 D-12 retirement.
|
||||
|
||||
Settings remain flat-file backed in Phase 1 (D-03 deferred), so these tests
|
||||
use async_client but do not require a real database session.
|
||||
The /api/settings endpoint was removed in Plan 03-04. This file now contains
|
||||
only the 404 assertion test (no longer marked xfail — it should pass green).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
async def test_get_settings_defaults(async_client, tmp_path, monkeypatch):
|
||||
# Point SETTINGS_FILE at a temp dir so tests don't clobber each other
|
||||
import config as cfg
|
||||
monkeypatch.setattr(cfg, "SETTINGS_FILE", tmp_path / "settings.json")
|
||||
import services.storage as st
|
||||
monkeypatch.setattr(st, "SETTINGS_FILE", tmp_path / "settings.json")
|
||||
|
||||
resp = await async_client.get("/api/settings")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["active_provider"] == "lmstudio"
|
||||
assert "system_prompt" in data
|
||||
assert "providers" in data
|
||||
|
||||
|
||||
async def test_patch_system_prompt(async_client, tmp_path, monkeypatch):
|
||||
import config as cfg
|
||||
monkeypatch.setattr(cfg, "SETTINGS_FILE", tmp_path / "settings.json")
|
||||
import services.storage as st
|
||||
monkeypatch.setattr(st, "SETTINGS_FILE", tmp_path / "settings.json")
|
||||
|
||||
new_prompt = "Custom system prompt for testing."
|
||||
resp = await async_client.patch("/api/settings", json={"system_prompt": new_prompt})
|
||||
assert resp.status_code == 200
|
||||
|
||||
resp2 = await async_client.get("/api/settings")
|
||||
assert resp2.json()["system_prompt"] == new_prompt
|
||||
|
||||
|
||||
async def test_patch_active_provider(async_client, tmp_path, monkeypatch):
|
||||
import config as cfg
|
||||
monkeypatch.setattr(cfg, "SETTINGS_FILE", tmp_path / "settings.json")
|
||||
import services.storage as st
|
||||
monkeypatch.setattr(st, "SETTINGS_FILE", tmp_path / "settings.json")
|
||||
|
||||
resp = await async_client.patch("/api/settings", json={"active_provider": "ollama"})
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["active_provider"] == "ollama"
|
||||
|
||||
|
||||
async def test_patch_invalid_provider(async_client, tmp_path, monkeypatch):
|
||||
import config as cfg
|
||||
monkeypatch.setattr(cfg, "SETTINGS_FILE", tmp_path / "settings.json")
|
||||
import services.storage as st
|
||||
monkeypatch.setattr(st, "SETTINGS_FILE", tmp_path / "settings.json")
|
||||
|
||||
resp = await async_client.patch("/api/settings", json={"active_provider": "unknown"})
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
async def test_patch_provider_config(async_client, tmp_path, monkeypatch):
|
||||
import config as cfg
|
||||
monkeypatch.setattr(cfg, "SETTINGS_FILE", tmp_path / "settings.json")
|
||||
import services.storage as st
|
||||
monkeypatch.setattr(st, "SETTINGS_FILE", tmp_path / "settings.json")
|
||||
|
||||
resp = await async_client.patch(
|
||||
"/api/settings",
|
||||
json={
|
||||
"providers": {
|
||||
"ollama": {"model": "mistral", "base_url": "http://host.docker.internal:11434"}
|
||||
}
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["providers"]["ollama"]["model"] == "mistral"
|
||||
|
||||
|
||||
async def test_masked_api_key_not_overwritten(async_client, tmp_path, monkeypatch):
|
||||
"""Patching with a masked key should not overwrite the real stored key."""
|
||||
import config as cfg
|
||||
monkeypatch.setattr(cfg, "SETTINGS_FILE", tmp_path / "settings.json")
|
||||
import services.storage as st
|
||||
monkeypatch.setattr(st, "SETTINGS_FILE", tmp_path / "settings.json")
|
||||
|
||||
# First set a real key
|
||||
await async_client.patch(
|
||||
"/api/settings",
|
||||
json={"providers": {"anthropic": {"api_key": "sk-ant-realkey"}}},
|
||||
)
|
||||
# Then patch with masked key (simulating frontend re-submitting)
|
||||
await async_client.patch(
|
||||
"/api/settings",
|
||||
json={"providers": {"anthropic": {"api_key": "****key"}}},
|
||||
)
|
||||
# The stored key should still be the real one
|
||||
stored = st.load_settings()
|
||||
assert stored["providers"]["anthropic"]["api_key"] == "sk-ant-realkey"
|
||||
|
||||
|
||||
async def test_get_default_prompt(async_client):
|
||||
resp = await async_client.get("/api/settings/default-prompt")
|
||||
assert resp.status_code == 200
|
||||
assert "system_prompt" in resp.json()
|
||||
assert len(resp.json()["system_prompt"]) > 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Wave 0 xfail stub — D-12: /api/settings endpoint removed in Plan 03-04
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.xfail(strict=False, reason="implemented in plan 03-04")
|
||||
async def test_settings_endpoint_removed(async_client):
|
||||
"""GET /api/settings returns 404 after the flat-file settings system is retired.
|
||||
|
||||
D-12: the /api/settings endpoint is removed entirely in Phase 3. All AI config
|
||||
comes from the database (users.ai_provider / users.ai_model set by admin).
|
||||
The flat-file services/storage.py load_settings()/save_settings() functions
|
||||
are also deleted (CONTEXT.md D-12).
|
||||
"""
|
||||
assert True # scaffold
|
||||
"""D-12: /api/settings endpoint is removed in Phase 3."""
|
||||
resp = await async_client.get("/api/settings")
|
||||
assert resp.status_code == 404
|
||||
|
||||
Reference in New Issue
Block a user