Skip to content

Commit 300cb49

Browse files
committed
refactor: move gui_launcher to deprecated and add comprehensive tests for server state and launcher configuration.
1 parent 45e6b67 commit 300cb49

17 files changed

Lines changed: 1224 additions & 197 deletions

browser_utils/initialization/core.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
logger = logging.getLogger("AIStudioProxyServer")
3838

3939

40-
async def initialize_page_logic(
40+
async def initialize_page_logic( # pragma: no cover
4141
browser: AsyncBrowser, storage_state_path: Optional[str] = None
4242
) -> Tuple[AsyncPage, bool]:
4343
"""
@@ -383,7 +383,7 @@ async def initialize_page_logic(
383383
raise RuntimeError(f"页面初始化意外错误: {e_init_page}") from e_init_page
384384

385385

386-
async def close_page_logic() -> Tuple[None, bool]:
386+
async def close_page_logic() -> Tuple[None, bool]: # pragma: no cover
387387
"""关闭页面逻辑"""
388388
# 需要访问全局变量
389389
from api_utils.server_state import state
@@ -410,7 +410,7 @@ async def close_page_logic() -> Tuple[None, bool]:
410410
return None, False
411411

412412

413-
async def signal_camoufox_shutdown() -> None:
413+
async def signal_camoufox_shutdown() -> None: # pragma: no cover
414414
"""发送关闭信号到Camoufox服务器"""
415415
logger.info(" 尝试发送关闭信号到 Camoufox 服务器 (此功能可能已由父进程处理)...")
416416
ws_endpoint = os.environ.get("CAMOUFOX_WS_ENDPOINT")
@@ -433,7 +433,7 @@ async def signal_camoufox_shutdown() -> None:
433433
logger.error(f" 发送关闭信号过程中捕获异常: {e}", exc_info=True)
434434

435435

436-
async def enable_temporary_chat_mode(page: AsyncPage) -> None:
436+
async def enable_temporary_chat_mode(page: AsyncPage) -> None: # pragma: no cover
437437
"""
438438
检查并启用 AI Studio 界面的“临时聊天”模式。
439439
这是一个独立的UI操作,应该在页面完全稳定后调用。

launcher/process.py

Lines changed: 49 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import sys
77
import threading
88
import time
9+
from typing import Optional
910

1011
from launcher.config import ENDPOINT_CAPTURE_TIMEOUT, PYTHON_EXECUTABLE, ws_regex
1112

@@ -42,43 +43,68 @@ def _enqueue_output(stream, stream_name, output_queue, process_pid_for_log="<未
4243
logger.debug(f"{log_prefix} 线程退出。")
4344

4445

46+
def build_launch_command(
47+
final_launch_mode: str,
48+
effective_active_auth_json_path: Optional[str],
49+
simulated_os_for_camoufox: str,
50+
camoufox_debug_port: int,
51+
internal_camoufox_proxy: Optional[str],
52+
) -> list[str]:
53+
"""
54+
Build the command-line arguments for launching the internal Camoufox process.
55+
56+
This is a pure function (no I/O) that can be easily unit tested.
57+
58+
Args:
59+
final_launch_mode: The launch mode (headless, virtual_headless, debug)
60+
effective_active_auth_json_path: Path to auth file, or None
61+
simulated_os_for_camoufox: OS to simulate (linux, windows, macos)
62+
camoufox_debug_port: Debug port for Camoufox
63+
internal_camoufox_proxy: Proxy configuration, or None
64+
65+
Returns:
66+
List of command-line arguments for subprocess.Popen
67+
"""
68+
cmd = [
69+
PYTHON_EXECUTABLE,
70+
"-u",
71+
sys.argv[0],
72+
"--internal-launch-mode",
73+
final_launch_mode,
74+
]
75+
76+
if effective_active_auth_json_path:
77+
cmd.extend(["--internal-auth-file", effective_active_auth_json_path])
78+
79+
cmd.extend(["--internal-camoufox-os", simulated_os_for_camoufox])
80+
cmd.extend(["--internal-camoufox-port", str(camoufox_debug_port)])
81+
82+
if internal_camoufox_proxy is not None:
83+
cmd.extend(["--internal-camoufox-proxy", internal_camoufox_proxy])
84+
85+
return cmd
86+
87+
4588
class CamoufoxProcessManager:
4689
def __init__(self):
4790
self.camoufox_proc = None
4891
self.captured_ws_endpoint = None
4992

50-
def start(
93+
def start( # pragma: no cover
5194
self,
5295
final_launch_mode,
5396
effective_active_auth_json_path,
5497
simulated_os_for_camoufox,
5598
args,
5699
):
57100
# 构建 Camoufox 内部启动命令 (from dev)
58-
camoufox_internal_cmd_args = [
59-
PYTHON_EXECUTABLE,
60-
"-u",
61-
sys.argv[0], # Use sys.argv[0] to refer to the main script
62-
"--internal-launch-mode",
101+
camoufox_internal_cmd_args = build_launch_command(
63102
final_launch_mode,
64-
]
65-
if effective_active_auth_json_path:
66-
camoufox_internal_cmd_args.extend(
67-
["--internal-auth-file", effective_active_auth_json_path]
68-
)
69-
70-
camoufox_internal_cmd_args.extend(
71-
["--internal-camoufox-os", simulated_os_for_camoufox]
103+
effective_active_auth_json_path,
104+
simulated_os_for_camoufox,
105+
args.camoufox_debug_port,
106+
args.internal_camoufox_proxy,
72107
)
73-
camoufox_internal_cmd_args.extend(
74-
["--internal-camoufox-port", str(args.camoufox_debug_port)]
75-
)
76-
77-
# 修复:传递代理参数到内部Camoufox进程
78-
if args.internal_camoufox_proxy is not None:
79-
camoufox_internal_cmd_args.extend(
80-
["--internal-camoufox-proxy", args.internal_camoufox_proxy]
81-
)
82108

83109
camoufox_popen_kwargs = {
84110
"stdout": subprocess.PIPE,

launcher/runner.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
logger = logging.getLogger("CamoufoxLauncher")
4949

5050

51-
class Launcher:
51+
class Launcher: # pragma: no cover
5252
def __init__(self):
5353
self.args = parse_args()
5454
self.camoufox_manager = CamoufoxProcessManager()
@@ -736,7 +736,7 @@ def watch_for_saved_auth_and_shutdown():
736736
watcher_thread.join()
737737

738738

739-
def signal_handler(sig, frame):
739+
def signal_handler(sig, frame): # pragma: no cover
740740
logger.info(f"接收到信号 {signal.Signals(sig).name} ({sig})。正在启动退出程序...")
741741
# Note: sys.exit(0) will trigger atexit handlers which can hang on multiprocessing cleanup.
742742
# The cleanup is handled by CamoufoxProcessManager registered via atexit in Launcher.__init__.
@@ -749,7 +749,7 @@ def signal_handler(sig, frame):
749749
signal.signal(signal.SIGTERM, signal_handler)
750750

751751

752-
def cleanup():
752+
def cleanup(): # pragma: no cover
753753
# This cleanup is now handled by CamoufoxProcessManager's cleanup method
754754
# But we need to ensure it's called.
755755
# Since we don't have a global instance easily accessible here for atexit,

pyproject.toml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ build-backend = "poetry.core.masonry.api"
4949
asyncio_mode = "auto"
5050
testpaths = ["tests"]
5151
python_files = "test_*.py"
52-
addopts = "-v --cov=api_utils --cov=browser_utils --cov=stream --cov=config --cov=models --cov-report=term-missing --ignore=tests/browser_utils/test_operations_old.py --tb=short"
52+
addopts = "-v --cov=api_utils --cov=browser_utils --cov=stream --cov=config --cov=models --cov=launcher --cov=logging_utils --cov=server.py --cov-report=term-missing --ignore=tests/browser_utils/test_operations_old.py --tb=short"
5353
timeout = 120
5454
timeout_method = "thread"
5555
markers = [
@@ -64,6 +64,15 @@ env = [
6464
omit = [
6565
"tests/*",
6666
"**/__init__.py",
67+
"launch_camoufox.py",
68+
"monkeytype_config.py",
69+
"deprecated/*",
70+
]
71+
72+
[tool.coverage.report]
73+
exclude_lines = [
74+
"if __name__ == .__main__.:",
75+
"pragma: no cover",
6776
]
6877

6978
[tool.ruff]

tests/browser_utils/conftest.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,34 @@
11
"""Browser utils test fixtures."""
22

33
import asyncio
4-
from unittest.mock import MagicMock, patch
4+
from unittest.mock import AsyncMock, MagicMock, patch
55

66
import pytest
77

88

9+
@pytest.fixture
10+
def mock_expect():
11+
"""Create a mock for playwright's expect function.
12+
13+
This fixture patches both:
14+
1. browser_utils.initialization.core.expect_async (used directly in core.py)
15+
2. playwright.async_api.expect (used by find_first_visible_locator in selector_utils.py)
16+
17+
This is necessary because find_first_visible_locator imports expect directly
18+
from playwright.async_api, while core.py imports it with an alias.
19+
"""
20+
mock = MagicMock()
21+
assertion_wrapper = MagicMock()
22+
assertion_wrapper.to_be_visible = AsyncMock()
23+
mock.return_value = assertion_wrapper
24+
25+
with (
26+
patch("browser_utils.initialization.core.expect_async", mock),
27+
patch("playwright.async_api.expect", mock),
28+
):
29+
yield mock
30+
31+
932
@pytest.fixture(autouse=True)
1033
def mock_server_state(request):
1134
"""Automatically mock server_state.state for all browser_utils tests.

0 commit comments

Comments
 (0)