[eric] settings: leading-_ -> p_/public in store+settings+credentials+redaction; promote cross-file API public (save_settings/atomic_write_settings/migrate_legacy_fields), drop redundant _save_settings alias, fix re-export + cache test refs

This commit is contained in:
ciregenz
2026-06-23 20:24:06 -07:00
parent f703b8aa02
commit deeab52ed4
16 changed files with 110 additions and 115 deletions
@@ -96,7 +96,7 @@ def resolve_attachments(context_paths: Optional[List], api_type: str, model: str
"""
if not context_paths:
return "", [], []
from backend.apps.settings.settings import _sniff_file_kind
from backend.apps.settings.settings import sniff_file_kind
import base64 as _b64
sections: List[str] = []
native: List[dict] = []
@@ -182,7 +182,7 @@ def resolve_attachments(context_paths: Optional[List], api_type: str, model: str
size = os.path.getsize(path)
with open(path, "rb") as fh:
head = fh.read(4096)
kind, media_type = _sniff_file_kind(head, os.path.basename(path))
kind, media_type = sniff_file_kind(head, os.path.basename(path))
if kind == "text":
with open(path, "r", errors="replace") as f:
+2 -2
View File
@@ -72,13 +72,13 @@ def _get_install_id() -> str:
if _install_id:
return _install_id
try:
from backend.apps.settings.store import load_settings, _save_settings
from backend.apps.settings.store import load_settings, save_settings
s = load_settings()
iid = getattr(s, "installation_id", None)
if not iid:
iid = uuid4().hex
s.installation_id = iid
_save_settings(s)
save_settings(s)
_install_id = iid
except Exception:
_install_id = uuid4().hex
+2 -2
View File
@@ -125,13 +125,13 @@ async def service_lifespan():
global _pulse_task, _drain_task, _9r_start_task
try:
from backend.apps.settings.settings import load_settings, _save_settings
from backend.apps.settings.settings import load_settings, save_settings
settings = load_settings()
is_first_open = settings.first_opened_at is None
if is_first_open:
settings.first_opened_at = datetime.now().isoformat()
_save_settings(settings)
save_settings(settings)
days_since_install = 0
if settings.first_opened_at:
+3 -3
View File
@@ -31,7 +31,7 @@ def proxy_auth(settings: AppSettings) -> tuple[str | None, str | None]:
return (None, None)
def _check_9router() -> bool:
def p_check_9router() -> bool:
"""Check if 9Router is running locally."""
try:
import httpx
@@ -50,7 +50,7 @@ def validate_credentials(settings: AppSettings, provider: str = "anthropic") ->
return
# 9Router proxies every provider, so if it's up we don't need keys here.
if _check_9router():
if p_check_9router():
return
if p == "anthropic":
@@ -135,7 +135,7 @@ def get_anthropic_client(settings: AppSettings) -> anthropic.AsyncAnthropic:
return anthropic.AsyncAnthropic(api_key=settings.anthropic_api_key)
# Fall back to 9Router (free for users with Claude/ChatGPT/Gemini subscriptions).
if _check_9router():
if p_check_9router():
return anthropic.AsyncAnthropic(
api_key="9router",
base_url="http://localhost:20128",
+10 -10
View File
@@ -19,17 +19,17 @@ from typing import Any
from backend.common.secret_scan import looks_secret
_SECRET_NAME_SUFFIXES = ("_key", "_token", "_secret")
P_SECRET_NAME_SUFFIXES = ("_key", "_token", "_secret")
# Not a credential and doesn't match the suffix rule, but a stable hardware-ish
# fingerprint used for cohorting/abuse; keep it out of the agent's eyes too.
_SECRET_EXTRA_FIELDS = frozenset({"installation_id"})
P_SECRET_EXTRA_FIELDS = frozenset({"installation_id"})
def is_secret_field(name: str) -> bool:
return name.endswith(_SECRET_NAME_SUFFIXES) or name in _SECRET_EXTRA_FIELDS
return name.endswith(P_SECRET_NAME_SUFFIXES) or name in P_SECRET_EXTRA_FIELDS
def _value_is_secret_shaped(value: Any) -> bool:
def p_value_is_secret_shaped(value: Any) -> bool:
"""Fail-safe behind the name rule: a field the name rule misses (a future
secret with an off-convention name) is still caught if its VALUE looks like
a credential (sk-..., ghp_..., Bearer ...). So a leak needs BOTH a bad name
@@ -37,7 +37,7 @@ def _value_is_secret_shaped(value: Any) -> bool:
return isinstance(value, str) and looks_secret(value)
def _redact_value(value: Any) -> dict[str, Any]:
def p_redact_value(value: Any) -> dict[str, Any]:
"""A secret rendered as state, never content: configured + last 4 only."""
if value is None or (isinstance(value, str) and value.strip() == ""):
return {"configured": False}
@@ -50,19 +50,19 @@ def redact_settings(raw: dict[str, Any]) -> dict[str, Any]:
{configured, last4}. Nested custom-provider api_keys are redacted too."""
out: dict[str, Any] = {}
for key, value in raw.items():
if is_secret_field(key) or _value_is_secret_shaped(value):
out[key] = _redact_value(value)
if is_secret_field(key) or p_value_is_secret_shaped(value):
out[key] = p_redact_value(value)
elif key == "custom_providers" and isinstance(value, list):
out[key] = [_redact_custom_provider(cp) for cp in value]
out[key] = [p_redact_custom_provider(cp) for cp in value]
else:
out[key] = value
return out
def _redact_custom_provider(cp: Any) -> Any:
def p_redact_custom_provider(cp: Any) -> Any:
if not isinstance(cp, dict):
return cp
out = dict(cp)
if "api_key" in out:
out["api_key"] = _redact_value(out.get("api_key"))
out["api_key"] = p_redact_value(out.get("api_key"))
return out
+16 -17
View File
@@ -16,9 +16,8 @@ from backend.apps.settings.store import (
SETTINGS_FILE,
load_settings,
save_settings,
_save_settings,
_atomic_write_settings,
_migrate_legacy_fields,
atomic_write_settings,
migrate_legacy_fields,
)
logger = logging.getLogger(__name__)
@@ -77,13 +76,13 @@ async def settings_lifespan():
await sync_custom_providers(getattr(s, "custom_providers", None) or [])
_asyncio.create_task(_boot_router_then_sync())
_asyncio.create_task(_upload_dir_gc_loop())
_asyncio.create_task(p_upload_dir_gc_loop())
except Exception as e:
logger.warning(f"9Router sync startup failed: {e}")
yield
async def _upload_dir_gc_loop():
async def p_upload_dir_gc_loop():
"""Daily GC of UPLOAD_DIR. Without this, every PDF/image the user
drops sits in the OS temp dir forever, growing unbounded across
sessions. We keep files for 7 days to make resume-after-restart
@@ -116,7 +115,7 @@ async def save_settings_async(settings_obj: AppSettings) -> None:
"""Async atomic save via thread pool; shares the lock with the sync variant."""
payload = settings_obj.model_dump()
loop = asyncio.get_running_loop()
await loop.run_in_executor(None, _atomic_write_settings, payload)
await loop.run_in_executor(None, atomic_write_settings, payload)
@settings.router.get("")
@@ -157,15 +156,15 @@ import weakref as _weakref
# the first loop that uses it and then errors on reuse from another loop (every
# async test spins a fresh one). WeakKeyDictionary auto-drops a loop's lock once
# the loop is gone.
_settings_write_locks: "_weakref.WeakKeyDictionary" = _weakref.WeakKeyDictionary()
p_settings_write_locks: "_weakref.WeakKeyDictionary" = _weakref.WeakKeyDictionary()
def settings_write_lock() -> asyncio.Lock:
loop = asyncio.get_running_loop()
lock = _settings_write_locks.get(loop)
lock = p_settings_write_locks.get(loop)
if lock is None:
lock = asyncio.Lock()
_settings_write_locks[loop] = lock
p_settings_write_locks[loop] = lock
return lock
@@ -382,7 +381,7 @@ async def reset_system_prompt():
# defaults EXCEPT the things a "reset my preferences" click must never silently
# sever, your connections (server-owned subscription fields AND your pasted
# provider credentials) and your identity. Hard-erase is the separate flow.
_RESET_PRESERVE_FIELDS = SERVER_OWNED_FIELDS + (
P_RESET_PRESERVE_FIELDS = SERVER_OWNED_FIELDS + (
"anthropic_api_key",
"openai_api_key",
"google_api_key",
@@ -399,7 +398,7 @@ _RESET_PRESERVE_FIELDS = SERVER_OWNED_FIELDS + (
async def reset_to_defaults():
old = load_settings()
fresh = AppSettings()
for k in _RESET_PRESERVE_FIELDS:
for k in P_RESET_PRESERVE_FIELDS:
setattr(fresh, k, getattr(old, k, None))
await save_settings_async(fresh)
return {"ok": True, "settings": fresh.model_dump()}
@@ -416,7 +415,7 @@ UPLOAD_DIR = os.path.join(tempfile.gettempdir(), "self-swarm-uploads")
os.makedirs(UPLOAD_DIR, exist_ok=True)
def _sniff_file_kind(contents: bytes, name: str) -> tuple[str, str | None]:
def sniff_file_kind(contents: bytes, name: str) -> tuple[str, str | None]:
"""Classify an uploaded file as text/pdf/image/binary so the agent
layer can route it (inline as text, send as native document/image
block, or refuse). Returns (kind, media_type)."""
@@ -457,7 +456,7 @@ def _sniff_file_kind(contents: bytes, name: str) -> tuple[str, str | None]:
return ("binary", None)
def _estimate_pdf_tokens(contents: bytes) -> int:
def estimate_pdf_tokens(contents: bytes) -> int:
"""Conservative PDF token estimate without a parser dep.
We use two signals and take the MAX so the chip + dry-run never
@@ -546,7 +545,7 @@ async def upload_files(files: list[UploadFile] = File(...)):
pass
raise
kind, media_type = _sniff_file_kind(contents, safe_name)
kind, media_type = sniff_file_kind(contents, safe_name)
if kind == "text":
try:
@@ -556,7 +555,7 @@ async def upload_files(files: list[UploadFile] = File(...)):
except Exception:
tokens_est = min(len(contents), 512_000) // 4
elif kind == "pdf":
tokens_est = _estimate_pdf_tokens(contents)
tokens_est = estimate_pdf_tokens(contents)
elif kind == "image":
tokens_est = 1_500
else:
@@ -574,14 +573,14 @@ async def upload_files(files: list[UploadFile] = File(...)):
return JSONResponse({"files": results})
class _SummarizeRequest(BaseModel):
class p_SummarizeRequest(BaseModel):
path: str
target_tokens: int = 4_000
primary_model: Optional[str] = None
@settings.router.post("/summarize-file")
async def summarize_file(req: _SummarizeRequest):
async def summarize_file(req: p_SummarizeRequest):
"""Compress an attached file down to a fact-dense summary the agent can
still reason over without paying the full token cost.
+24 -28
View File
@@ -22,7 +22,7 @@ logger = logging.getLogger(__name__)
SETTINGS_FILE = os.path.join(DATA_DIR, "settings.json")
def _migrate_legacy_fields(raw: dict) -> dict:
def migrate_legacy_fields(raw: dict) -> dict:
"""Translate deprecated pre-launch field names ('managed', 'openswarm_auth_token') into production schema."""
if raw.get("connection_mode") == "managed":
raw["connection_mode"] = "openswarm-pro"
@@ -31,7 +31,7 @@ def _migrate_legacy_fields(raw: dict) -> dict:
return raw
def _coerce_settings(raw: dict) -> AppSettings:
def p_coerce_settings(raw: dict) -> AppSettings:
"""Build AppSettings, surviving a settings.json written by a different app
version. Unknown fields are already ignored by pydantic; the case this guards
is a field whose TYPE drifted across versions (e.g. a list that is now a
@@ -55,7 +55,7 @@ def _coerce_settings(raw: dict) -> AppSettings:
return AppSettings()
def _preserve_corrupt_settings() -> None:
def p_preserve_corrupt_settings() -> None:
"""Move an unparseable settings.json aside so boot proceeds on defaults while
the original stays recoverable (the next save would otherwise overwrite it)."""
try:
@@ -70,11 +70,11 @@ def _preserve_corrupt_settings() -> None:
# so even a hand-edited file or an unexpected writer is picked up immediately. A stat
# skips the open+parse+validate that Defender turns into 5-50ms on Windows. Copies on
# both sides keep handler isolation: callers mutate their copy, never the cache.
_cached_settings: AppSettings | None = None
_cached_sig: tuple[int, int] | None = None
p_cached_settings: AppSettings | None = None
p_cached_sig: tuple[int, int] | None = None
def _settings_sig() -> tuple[int, int] | None:
def p_settings_sig() -> tuple[int, int] | None:
try:
st = os.stat(SETTINGS_FILE)
return (st.st_mtime_ns, st.st_size)
@@ -86,38 +86,38 @@ def load_settings() -> AppSettings:
"""Load settings from JSON file, returning defaults if not found. Never raises
on a corrupt or version-mismatched file: a single bad settings.json must not
brick boot (it is read at startup, by the settings endpoint, and per dispatch)."""
global _cached_settings, _cached_sig
sig = _settings_sig()
if sig is not None and _cached_settings is not None and sig == _cached_sig:
return _cached_settings.model_copy(deep=True)
global p_cached_settings, p_cached_sig
sig = p_settings_sig()
if sig is not None and p_cached_settings is not None and sig == p_cached_sig:
return p_cached_settings.model_copy(deep=True)
if os.path.exists(SETTINGS_FILE):
try:
with open(SETTINGS_FILE) as f:
raw = json.load(f)
except (json.JSONDecodeError, OSError, ValueError):
_preserve_corrupt_settings()
p_preserve_corrupt_settings()
return AppSettings()
if not isinstance(raw, dict):
# Valid JSON but not an object (e.g. a bare list/number); unusable.
_preserve_corrupt_settings()
p_preserve_corrupt_settings()
return AppSettings()
settings = _coerce_settings(_migrate_legacy_fields(raw))
settings = p_coerce_settings(migrate_legacy_fields(raw))
if settings.default_system_prompt is None:
settings.default_system_prompt = DEFAULT_SYSTEM_PROMPT
_cached_settings = settings.model_copy(deep=True)
_cached_sig = sig
p_cached_settings = settings.model_copy(deep=True)
p_cached_sig = sig
return settings
return AppSettings()
# threading.Lock guards every SETTINGS_FILE write; works for sync paths and async run_in_executor paths.
_settings_write_lock = threading.Lock()
p_settings_write_lock = threading.Lock()
def _atomic_write_settings(payload: dict) -> None:
def atomic_write_settings(payload: dict) -> None:
"""Atomic SETTINGS_FILE write; call via save_settings*, not directly."""
global _cached_settings, _cached_sig
with _settings_write_lock:
global p_cached_settings, p_cached_sig
with p_settings_write_lock:
os.makedirs(DATA_DIR, exist_ok=True)
fd, tmp = tempfile.mkstemp(prefix=".settings.", suffix=".tmp", dir=DATA_DIR)
try:
@@ -128,10 +128,10 @@ def _atomic_write_settings(payload: dict) -> None:
try:
os.replace(tmp, SETTINGS_FILE)
# Refresh the cache inside the lock so cache order matches disk order.
_cached_settings = _coerce_settings(_migrate_legacy_fields(dict(payload)))
if _cached_settings.default_system_prompt is None:
_cached_settings.default_system_prompt = DEFAULT_SYSTEM_PROMPT
_cached_sig = _settings_sig()
p_cached_settings = p_coerce_settings(migrate_legacy_fields(dict(payload)))
if p_cached_settings.default_system_prompt is None:
p_cached_settings.default_system_prompt = DEFAULT_SYSTEM_PROMPT
p_cached_sig = p_settings_sig()
return
except PermissionError:
if attempt == 1:
@@ -147,8 +147,4 @@ def _atomic_write_settings(payload: dict) -> None:
def save_settings(settings_obj: AppSettings) -> None:
"""Sync atomic persist; thread-safe. Async callers should prefer save_settings_async (Defender can stretch writes to 50-200ms)."""
_atomic_write_settings(settings_obj.model_dump())
def _save_settings(settings_obj: AppSettings) -> None:
save_settings(settings_obj)
atomic_write_settings(settings_obj.model_dump())
+6 -6
View File
@@ -31,11 +31,11 @@ def client():
@pytest.fixture
def reset_settings():
"""Snapshot + restore settings around each test so writes don't leak."""
from backend.apps.settings.settings import load_settings, _save_settings
from backend.apps.settings.settings import load_settings, save_settings
original = load_settings().model_copy(deep=True)
yield
_save_settings(original)
save_settings(original)
# ---------------------------------------------------------------------------
@@ -140,14 +140,14 @@ def test_signin_activate_short_token_rejected_locally(client, reset_settings):
# ---------------------------------------------------------------------------
def test_signout_clears_local_identity(client, reset_settings):
from backend.apps.settings.settings import load_settings, _save_settings
from backend.apps.settings.settings import load_settings, save_settings
s = load_settings()
s.user_id = "u-bye"
s.user_email = "bye@example.com"
s.signin_method = "google"
s.openswarm_bearer_token = "bearer-to-revoke-xxxxxxxx"
s.connection_mode = "openswarm-pro"
_save_settings(s)
save_settings(s)
fake_response = AsyncMock()
fake_response.status_code = 200
@@ -168,11 +168,11 @@ def test_signout_clears_local_identity(client, reset_settings):
def test_signout_succeeds_even_when_cloud_unreachable(client, reset_settings):
"""A flaky network shouldn't strand the user signed-in locally."""
from backend.apps.settings.settings import load_settings, _save_settings
from backend.apps.settings.settings import load_settings, save_settings
s = load_settings()
s.user_id = "u-flaky"
s.openswarm_bearer_token = "bearer-flaky-network-xxxx"
_save_settings(s)
save_settings(s)
with patch("httpx.AsyncClient") as MockClient:
instance = MockClient.return_value.__aenter__.return_value
+2 -2
View File
@@ -15,8 +15,8 @@ def settings_tmp(tmp_path, monkeypatch):
f = tmp_path / "settings.json"
monkeypatch.setattr(store, "DATA_DIR", str(tmp_path))
monkeypatch.setattr(store, "SETTINGS_FILE", str(f))
monkeypatch.setattr(store, "_cached_settings", None)
monkeypatch.setattr(store, "_cached_sig", None)
monkeypatch.setattr(store, "p_cached_settings", None)
monkeypatch.setattr(store, "p_cached_sig", None)
return f
@@ -29,20 +29,20 @@ def _auth_headers():
@pytest.fixture
def reset_settings():
from backend.apps.settings.settings import load_settings, _save_settings
from backend.apps.settings.settings import load_settings, save_settings
original = load_settings().model_copy(deep=True)
yield
_save_settings(original)
save_settings(original)
@pytest.mark.asyncio
async def test_concurrent_writes_to_different_fields_both_survive(reset_settings):
from backend.apps.settings.settings import load_settings, _save_settings
from backend.apps.settings.settings import load_settings, save_settings
base = load_settings()
base.theme = "dark"
base.default_mode = "agent"
_save_settings(base)
save_settings(base)
headers = _auth_headers()
transport = httpx.ASGITransport(app=app)
+7 -7
View File
@@ -20,13 +20,13 @@ async def test_second_wall_restores_protected_credential_even_if_body_blanks_it(
live credential blanked (a guard slip upstream), the second-wall restore puts
it back. Proves the api-key guard isn't a single point of failure."""
from backend.apps.settings.settings import (
apply_settings_update, settings_write_lock, load_settings, _save_settings,
apply_settings_update, settings_write_lock, load_settings, save_settings,
)
original = load_settings().model_copy(deep=True)
try:
s = load_settings()
s.anthropic_api_key = "sk-live-KEEP-ME"
_save_settings(s)
save_settings(s)
# A body that (as if a guard bug let it through) clears the live key.
body = load_settings()
body.anthropic_api_key = ""
@@ -41,7 +41,7 @@ async def test_second_wall_restores_protected_credential_even_if_body_blanks_it(
await apply_settings_update(body2, protect_fields={"anthropic_api_key"})
assert not load_settings().openai_api_key
finally:
_save_settings(original)
save_settings(original)
@pytest.fixture
@@ -55,10 +55,10 @@ def client():
@pytest.fixture
def reset_settings():
from backend.apps.settings.settings import load_settings, _save_settings
from backend.apps.settings.settings import load_settings, save_settings
original = load_settings().model_copy(deep=True)
yield
_save_settings(original)
save_settings(original)
@pytest.fixture
@@ -67,13 +67,13 @@ def session_on_anthropic_key():
Anthropic key powers it. Registered in agent_manager so the guard sees it."""
from backend.apps.agents.agent_manager import agent_manager
from backend.apps.agents.core.models import AgentSession
from backend.apps.settings.settings import load_settings, _save_settings
from backend.apps.settings.settings import load_settings, save_settings
s = load_settings()
s.connection_mode = "own_key"
s.anthropic_api_key = "sk-ant-test-LIVE"
s.openai_api_key = "sk-openai-test-OTHER"
_save_settings(s)
save_settings(s)
sess = AgentSession(id="settings-meta-test", name="t", model="opus-4-8")
agent_manager.sessions["settings-meta-test"] = sess
@@ -58,10 +58,10 @@ def live_backend():
@pytest.fixture
def reset_settings():
from backend.apps.settings.settings import load_settings, _save_settings
from backend.apps.settings.settings import load_settings, save_settings
original = load_settings().model_copy(deep=True)
yield
_save_settings(original)
save_settings(original)
def _run_stdio(port: int, token: str, session_id: str, changes: dict) -> str:
@@ -84,7 +84,7 @@ def _run_stdio(port: int, token: str, session_id: str, changes: dict) -> str:
def test_live_stdio_settingswrite_refuses_live_key_clears_other(live_backend, reset_settings):
port, token = live_backend
from backend.apps.settings.settings import load_settings, _save_settings
from backend.apps.settings.settings import load_settings, save_settings
from backend.apps.agents.agent_manager import agent_manager
from backend.apps.agents.core.models import AgentSession
@@ -94,7 +94,7 @@ def test_live_stdio_settingswrite_refuses_live_key_clears_other(live_backend, re
s.connection_mode = "own_key"
s.anthropic_api_key = "sk-ant-LIVE-do-not-clear"
s.openai_api_key = "sk-oai-OTHER-ok-to-clear"
_save_settings(s)
save_settings(s)
agent_manager.sessions["live-stdio-test"] = AgentSession(id="live-stdio-test", name="t", model="opus-4-8")
try:
+5 -5
View File
@@ -27,25 +27,25 @@ def _write(path, obj):
json.dump(obj, fh)
# ---------------- _migrate_legacy_fields ----------------
# ---------------- migrate_legacy_fields ----------------
def test_migrate_managed_to_openswarm_pro():
assert store._migrate_legacy_fields({"connection_mode": "managed"})["connection_mode"] == "openswarm-pro"
assert store.migrate_legacy_fields({"connection_mode": "managed"})["connection_mode"] == "openswarm-pro"
def test_migrate_auth_token_renamed_and_popped():
out = store._migrate_legacy_fields({"openswarm_auth_token": "tok"})
out = store.migrate_legacy_fields({"openswarm_auth_token": "tok"})
assert out["openswarm_bearer_token"] == "tok"
assert "openswarm_auth_token" not in out
def test_migrate_does_not_clobber_existing_bearer():
out = store._migrate_legacy_fields({"openswarm_auth_token": "old", "openswarm_bearer_token": "new"})
out = store.migrate_legacy_fields({"openswarm_auth_token": "old", "openswarm_bearer_token": "new"})
assert out["openswarm_bearer_token"] == "new"
def test_migrate_leaves_modern_values_untouched():
out = store._migrate_legacy_fields({"connection_mode": "own_key"})
out = store.migrate_legacy_fields({"connection_mode": "own_key"})
assert out["connection_mode"] == "own_key"
+6 -6
View File
@@ -30,18 +30,18 @@ def client():
@pytest.fixture
def reset_settings():
from backend.apps.settings.settings import load_settings, _save_settings
from backend.apps.settings.settings import load_settings, save_settings
original = load_settings().model_copy(deep=True)
yield
_save_settings(original)
save_settings(original)
def test_patch_changes_only_sent_fields(client, reset_settings):
from backend.apps.settings.settings import load_settings, _save_settings
from backend.apps.settings.settings import load_settings, save_settings
s = load_settings()
s.theme = "dark"
s.default_mode = "chat" # as if something else had set this
_save_settings(s)
save_settings(s)
r = client.patch("/api/settings", json={"theme": "light"})
assert r.status_code == 200, r.text
@@ -64,11 +64,11 @@ async def test_concurrent_renderer_patch_and_agent_write_both_survive(reset_sett
"""The renderer PATCHes one field while an autonomous agent writes another,
at the same time. Both must land: the renderer never sends the agent's field,
so it can't clobber it, and both reads happen fresh under the shared lock."""
from backend.apps.settings.settings import load_settings, _save_settings
from backend.apps.settings.settings import load_settings, save_settings
base = load_settings()
base.theme = "dark"
base.default_mode = "agent"
_save_settings(base)
save_settings(base)
transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(transport=transport, base_url="http://test", headers=_auth_headers()) as client:
+2 -2
View File
@@ -28,11 +28,11 @@ def client():
@pytest.fixture
def reset_settings():
from backend.apps.settings.settings import load_settings, _save_settings
from backend.apps.settings.settings import load_settings, save_settings
original = load_settings().model_copy(deep=True)
yield
_save_settings(original)
save_settings(original)
def _activate_pro(client, token="repro-bearer-0123456789abcdef"):
+15 -15
View File
@@ -1315,18 +1315,18 @@ def test_apply_context_window_silent_on_unknown_model():
def test_estimate_pdf_tokens_floors_empty_pdf_at_byte_heuristic():
"""A truly empty / minimal PDF still returns a non-zero estimate so
the dry-run guard doesn't allow many tiny PDFs through silently."""
from backend.apps.settings.settings import _estimate_pdf_tokens
assert _estimate_pdf_tokens(b"") >= 1_000
assert _estimate_pdf_tokens(b"%PDF-1.4\n") >= 1_000
from backend.apps.settings.settings import estimate_pdf_tokens
assert estimate_pdf_tokens(b"") >= 1_000
assert estimate_pdf_tokens(b"%PDF-1.4\n") >= 1_000
def test_estimate_pdf_tokens_takes_max_of_pages_and_bytes():
"""An image-heavy PDF with low page count should still report high
tokens via the byte-size signal; we never under-report."""
from backend.apps.settings.settings import _estimate_pdf_tokens
from backend.apps.settings.settings import estimate_pdf_tokens
# 8MB PDF with 1 page (image-heavy), byte heuristic should dominate.
fake = b"%PDF-1.4\n/Type /Pages /Count 1\n" + b"X" * (8 * 1024 * 1024)
tokens = _estimate_pdf_tokens(fake)
tokens = estimate_pdf_tokens(fake)
# byte heuristic: 8MB / 80 = 100k tokens > pages * 750 = 750
assert tokens >= 100_000
@@ -1334,9 +1334,9 @@ def test_estimate_pdf_tokens_takes_max_of_pages_and_bytes():
def test_estimate_pdf_tokens_caps_malformed_count():
"""A PDF with /Count 999999 (malformed or hostile) does NOT bypass
the 10k pages sanity cap; falls through to byte heuristic instead."""
from backend.apps.settings.settings import _estimate_pdf_tokens
from backend.apps.settings.settings import estimate_pdf_tokens
fake = b"%PDF-1.4\n/Type /Pages /Count 999999\n"
t = _estimate_pdf_tokens(fake)
t = estimate_pdf_tokens(fake)
# Should NOT be 999999 * 750 = 750 million.
assert t < 50_000_000
@@ -1526,20 +1526,20 @@ def test_resolve_attachments_uses_os_path_basename_for_windows_paths():
def test_sniff_file_kind_consistent_across_platforms():
"""The sniffer reads bytes, never paths. So platform doesn't matter
for the classification logic, same bytes → same kind on Windows/Mac/Linux."""
from backend.apps.settings.settings import _sniff_file_kind
assert _sniff_file_kind(b"%PDF-1.4\n", "x.pdf") == ("pdf", "application/pdf")
assert _sniff_file_kind(b"\x89PNG\r\n\x1a\n", "x.png") == ("image", "image/png")
assert _sniff_file_kind(b"PK\x03\x04", "x.zip") == ("binary", None)
assert _sniff_file_kind(b"MZ\x90\x00", "x.exe") == ("binary", None)
assert _sniff_file_kind(b"hello world", "x.txt") == ("text", "text/plain")
from backend.apps.settings.settings import sniff_file_kind
assert sniff_file_kind(b"%PDF-1.4\n", "x.pdf") == ("pdf", "application/pdf")
assert sniff_file_kind(b"\x89PNG\r\n\x1a\n", "x.png") == ("image", "image/png")
assert sniff_file_kind(b"PK\x03\x04", "x.zip") == ("binary", None)
assert sniff_file_kind(b"MZ\x90\x00", "x.exe") == ("binary", None)
assert sniff_file_kind(b"hello world", "x.txt") == ("text", "text/plain")
def test_estimate_pdf_tokens_consistent_across_platforms():
"""Same byte-level math regardless of OS."""
from backend.apps.settings.settings import _estimate_pdf_tokens
from backend.apps.settings.settings import estimate_pdf_tokens
# 5MB PDF should always estimate ≥ 5MB/80 = 65536 tokens.
fake = b"%PDF-1.4\n" + b"X" * (5 * 1024 * 1024)
assert _estimate_pdf_tokens(fake) >= 65000
assert estimate_pdf_tokens(fake) >= 65000
def test_sniff_handles_windows_style_backslash_path_string():