Files
autoclip/backend/tests/test_settings_web_mode.py
T
81922206a1 fix: settings page crash under browser translation + make settings saveable in Docker / script mode (#100) (#102)
* fix(frontend): survive browser page translation instead of crashing the whole tree (#100)

Chrome / Edge 'Translate this page' replaces text nodes with <font> wrappers;
React's next removeChild / insertBefore on those nodes throws NotFoundError and
the ErrorBoundary unmounts everything. Guard both DOM methods before mount
(per facebook/react#11538) so displaced nodes are skipped rather than fatal.

ErrorBoundary now detects the translation case and tells the user (zh/en) to
turn translation off; its fallback page is redone per DESIGN.md (no purple
gradient / AntD Result, single-tone card + Btn primitives) and 'back home'
actually navigates under HashRouter.

Co-authored-by: Kris K <zhouxiaoka@users.noreply.github.com>

* feat(settings): make the settings page work in Docker / script mode (#100)

GET/PUT /settings, /test-api, /current-provider, /compatible-models,
/available-models, /validation, /reset and DELETE / no longer require desktop
mode. settings.json lands in get_data_directory() (./data in Docker) and the
API process + Celery worker already hot-reload on mtime, so the same code path
works for every deployment shape. Desktop-only endpoints (data-dir migration,
backup/restore, import/export, config sync) keep the guard.

When no settings.json exists yet, GET /settings reflects the provider / model /
base_url the LLM manager actually resolved from env (LLM_PROVIDER etc.) instead
of always showing dashscope.

Frontend SettingsPage drops the web-mode read-only short-circuit; also removes
the unused eslint-disable directive that has kept CI red on main (#99).

Adds backend/tests/test_settings_web_mode.py (6 cases).

Co-authored-by: Kris K <zhouxiaoka@users.noreply.github.com>

* docs: record #100 fix, 2026-09-20 issue/PR triage and schedule feature requests into v1.3 / v1.4

HANDOFF: root-cause note for #100, per-issue disposition table, external PR
decisions (#97 #98 #99 merge, #101 hold for i18n scaffold), and new v1.3 items
(fail-like-failure, i18n scaffold, dashscope intl, DESIGN.md debt list) plus
v1.4 items (pluggable ASR / scoring backends, Sentry). DOCKER.md: settings can
now be saved from the web UI; env vars are defaults only.

Co-authored-by: Kris K <zhouxiaoka@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Kris K <zhouxiaoka@users.noreply.github.com>
2026-09-21 02:12:58 +08:00

127 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
设置端点在非桌面模式(Docker / 本地脚本)下必须可用(issue #100)。
以前 GET/PUT /settings、/test-api、/current-provider 等都被 check_desktop_mode() 拦成 400,
Docker 用户只能改 .env。这里直接调用端点函数验证:读、写、热重载、环境变量回显都能走通。
"""
import asyncio
import json
import pytest
from fastapi import HTTPException
from backend.core.desktop_config import DesktopPaths
@pytest.fixture
def web_mode(monkeypatch, tmp_path):
for name in (
"AUTOCLIP_DESKTOP_MODE", "AUTOCLIP_MODE", "TAURI_PLATFORM",
"LLM_PROVIDER", "API_MODEL_NAME", "LLM_MODEL", "OPENAI_BASE_URL",
"API_DASHSCOPE_API_KEY", "DASHSCOPE_API_KEY", "API_OPENAI_API_KEY", "OPENAI_API_KEY",
"API_GEMINI_API_KEY", "GEMINI_API_KEY", "API_SILICONFLOW_API_KEY", "SILICONFLOW_API_KEY",
):
monkeypatch.delenv(name, raising=False)
from backend.api.v1 import settings as settings_api
from backend.core import llm_manager as manager_module
config = settings_api.get_desktop_config()
monkeypatch.setattr(config, "paths", DesktopPaths(
data_dir=tmp_path,
cache_dir=tmp_path / "cache",
temp_dir=tmp_path / "temp",
database_url=f"sqlite:///{tmp_path / 'autoclip.db'}",
))
# LLM 管理器读同一份 settings.json,且不碰真实用户目录
monkeypatch.setattr(manager_module.config_sync_service, "is_sync_needed", lambda: False)
manager = manager_module.LLMManager(settings_file=tmp_path / "settings.json")
monkeypatch.setattr(manager_module, "get_llm_manager", lambda: manager)
assert settings_api.is_desktop_mode() is False
return settings_api, tmp_path, manager
def _payload(settings_api, **api_overrides):
api = {
"api_keys": {"dashscope": "", "openai": "", "gemini": "AIza-test-key-1234567890", "siliconflow": ""},
"api_provider": "gemini",
"api_model": "gemini-2.5-flash",
}
api.update(api_overrides)
return settings_api.DesktopSettings(api=settings_api.ApiSettings(**api))
def test_desktop_only_endpoints_still_guarded(web_mode):
settings_api, _, _ = web_mode
with pytest.raises(HTTPException) as exc:
settings_api.check_desktop_mode()
assert exc.value.status_code == 400
def test_get_settings_works_without_desktop_mode(web_mode):
settings_api, tmp_path, _ = web_mode
settings = asyncio.run(settings_api.get_settings())
assert settings.api.api_provider == "dashscope"
assert settings.paths is not None
assert settings.paths.data_directory == str(tmp_path)
def test_get_settings_reflects_env_config_before_first_save(web_mode, monkeypatch):
"""Docker 用户在 .env 里写了 LLM_PROVIDER=gemini,设置页首屏就该显示 Gemini,而不是通义千问"""
settings_api, _, manager = web_mode
monkeypatch.setenv("LLM_PROVIDER", "gemini")
monkeypatch.setenv("API_MODEL_NAME", "gemini-2.5-flash")
manager.settings = manager._load_settings()
settings = asyncio.run(settings_api.get_settings())
assert settings.api.api_provider == "gemini"
assert settings.api.api_model == "gemini-2.5-flash"
def test_update_then_get_roundtrip_and_manager_reload(web_mode):
settings_api, tmp_path, manager = web_mode
assert manager.get_current_provider_info()["provider"] == "dashscope"
result = asyncio.run(settings_api.update_settings(_payload(settings_api)))
assert result["settings_file"] == str(tmp_path / "settings.json")
saved = json.loads((tmp_path / "settings.json").read_text(encoding="utf-8"))
assert saved["api"]["api_provider"] == "gemini"
assert saved["api"]["api_keys"]["gemini"] == "AIza-test-key-1234567890"
reloaded = asyncio.run(settings_api.get_settings())
assert reloaded.api.api_provider == "gemini"
assert reloaded.api.api_model == "gemini-2.5-flash"
# API 进程内的 LLM 管理器立刻切换;worker 进程靠 mtime 走同一条重载逻辑
info = manager.get_current_provider_info()
assert info["provider"] == "gemini"
assert info["model"] == "gemini-2.5-flash"
def test_current_provider_endpoint_works_without_desktop_mode(web_mode):
settings_api, _, _ = web_mode
asyncio.run(settings_api.update_settings(_payload(settings_api)))
info = asyncio.run(settings_api.get_current_provider())
assert info["provider"] == "gemini"
assert info["model"] == "gemini-2.5-flash"
def test_test_api_endpoint_no_longer_requires_desktop_mode(web_mode):
settings_api, _, _ = web_mode
request = settings_api.TestApiRequest(provider="gemini", api_key="short")
# 走到了业务校验(key 过短),而不是被 400 拦在门外
result = asyncio.run(settings_api.test_api_connection(request))
assert result["success"] is False
assert "过短" in result["error"]