| """Desktop shell lifecycle and singleton contracts.""" |
|
|
| import threading |
| from pathlib import Path |
| from unittest.mock import MagicMock, patch |
|
|
| from free_claude_code.cli.commands import ServerStatus, ServerSupervisor |
| from free_claude_code.cli.desktop import DesktopController |
| from free_claude_code.config.settings import Settings |
| from free_claude_code.core.interprocess_lock import InterprocessFileLock |
|
|
|
|
| def _settings() -> Settings: |
| return Settings.model_construct(host="0.0.0.0", port=8082) |
|
|
|
|
| def test_desktop_instance_lock_is_exclusive_and_reusable(tmp_path: Path) -> None: |
| lock_path = tmp_path / "desktop.lock" |
| first = InterprocessFileLock(lock_path) |
| second = InterprocessFileLock(lock_path) |
|
|
| assert first.acquire() is True |
| assert first.acquire() is True |
| assert second.acquire() is False |
|
|
| first.release() |
| first.release() |
| assert second.acquire() is True |
| second.release() |
|
|
|
|
| def test_supervisor_accepts_restart_during_scheduled_startup() -> None: |
| supervisor = ServerSupervisor(console_logging=False) |
| settings = _settings() |
|
|
| with ( |
| patch( |
| "free_claude_code.cli.commands.load_server_settings", |
| return_value=settings, |
| ), |
| patch.object(supervisor, "_run_once", return_value=False) as run_once, |
| patch("free_claude_code.cli.commands.kill_all_best_effort"), |
| ): |
| assert supervisor.schedule_run() is True |
| assert supervisor.status is ServerStatus.STARTING |
| assert supervisor.request_restart() is True |
| supervisor.run(open_admin_browser=False) |
|
|
| run_once.assert_called_once_with( |
| settings, |
| open_admin_browser=False, |
| restart_generation=1, |
| ) |
| assert supervisor.status is ServerStatus.STOPPED |
|
|
|
|
| def test_desktop_controller_owns_server_thread_and_graceful_quit() -> None: |
| opened = threading.Event() |
|
|
| class FakeSupervisor: |
| def __init__(self) -> None: |
| self.status = ServerStatus.STARTING |
| self.started = threading.Event() |
| self.stopped = threading.Event() |
| self.run_arguments: list[bool | None] = [] |
| self.schedule_count = 0 |
| self.restart_count = 0 |
| self.stop_count = 0 |
|
|
| def schedule_run(self) -> bool: |
| self.schedule_count += 1 |
| return True |
|
|
| def run(self, *, open_admin_browser: bool | None = None) -> None: |
| self.run_arguments.append(open_admin_browser) |
| self.status = ServerStatus.RUNNING |
| self.started.set() |
| assert self.stopped.wait(2) |
| self.status = ServerStatus.STOPPED |
|
|
| def request_restart(self) -> bool: |
| self.restart_count += 1 |
| return True |
|
|
| def request_stop(self) -> None: |
| self.stop_count += 1 |
| self.status = ServerStatus.STOPPING |
| self.stopped.set() |
|
|
| class FakeTray: |
| def __init__(self, controller: DesktopController) -> None: |
| self.controller = controller |
| self.run_thread_id: int | None = None |
| self.stop_count = 0 |
|
|
| def run(self) -> None: |
| self.run_thread_id = threading.get_ident() |
| assert supervisor.started.wait(2) |
| self.controller.open_admin() |
| self.controller.restart_server() |
| self.controller.quit() |
|
|
| def stop(self) -> None: |
| self.stop_count += 1 |
|
|
| supervisor = FakeSupervisor() |
| tray: FakeTray | None = None |
|
|
| def make_tray(controller: DesktopController) -> FakeTray: |
| nonlocal tray |
| tray = FakeTray(controller) |
| return tray |
|
|
| main_thread_id = threading.get_ident() |
| controller = DesktopController(supervisor, make_tray, opened.set) |
| controller.run() |
|
|
| assert tray is not None |
| assert tray.run_thread_id == main_thread_id |
| assert supervisor.run_arguments == [False] |
| assert supervisor.schedule_count == 1 |
| assert supervisor.restart_count == 1 |
| assert supervisor.stop_count >= 1 |
| assert tray.stop_count >= 1 |
| assert opened.is_set() |
|
|
|
|
| def test_restart_during_server_startup_is_accepted_without_waiting() -> None: |
| class StartupSupervisor: |
| def __init__(self) -> None: |
| self.status = ServerStatus.STARTING |
| self.run_called = threading.Event() |
| self.allow_run = threading.Event() |
| self.worker_started = threading.Event() |
| self.release_worker = threading.Event() |
| self.run_scheduled = False |
| self.restart_count = 0 |
| self.accepted_restart_count = 0 |
|
|
| def schedule_run(self) -> bool: |
| self.run_scheduled = True |
| return True |
|
|
| def run(self, *, open_admin_browser: bool | None = None) -> None: |
| assert open_admin_browser is False |
| self.run_called.set() |
| assert self.allow_run.wait(2) |
| self.run_scheduled = False |
| self.worker_started.set() |
| assert self.release_worker.wait(2) |
| self.status = ServerStatus.STOPPED |
|
|
| def request_restart(self) -> bool: |
| self.restart_count += 1 |
| if self.run_scheduled: |
| self.accepted_restart_count += 1 |
| return True |
| return False |
|
|
| def request_stop(self) -> None: |
| self.release_worker.set() |
|
|
| class WaitingTray: |
| def __init__(self, _controller: DesktopController) -> None: |
| self.started = threading.Event() |
| self.stopped = threading.Event() |
|
|
| def run(self) -> None: |
| self.started.set() |
| assert self.stopped.wait(2) |
|
|
| def stop(self) -> None: |
| self.stopped.set() |
|
|
| supervisor = StartupSupervisor() |
| tray: WaitingTray | None = None |
|
|
| def make_tray(controller: DesktopController) -> WaitingTray: |
| nonlocal tray |
| tray = WaitingTray(controller) |
| return tray |
|
|
| controller = DesktopController(supervisor, make_tray, MagicMock()) |
| controller_thread = threading.Thread(target=controller.run) |
| controller_thread.start() |
| assert tray is not None |
| assert tray.started.wait(2) |
| assert supervisor.run_called.wait(2) |
|
|
| restart_thread = threading.Thread(target=controller.restart_server) |
| restart_thread.start() |
| restart_thread.join(0.5) |
| restart_blocked = restart_thread.is_alive() |
|
|
| supervisor.allow_run.set() |
| assert supervisor.worker_started.wait(2) |
| controller.quit() |
| supervisor.release_worker.set() |
| restart_thread.join(2) |
| controller_thread.join(2) |
|
|
| assert restart_blocked is False |
| assert supervisor.restart_count == 1 |
| assert supervisor.accepted_restart_count == 1 |
| assert not restart_thread.is_alive() |
| assert not controller_thread.is_alive() |
|
|
|
|
| def test_second_desktop_launch_opens_existing_admin_without_new_server() -> None: |
| from free_claude_code.cli import desktop |
|
|
| settings = _settings() |
| instance_lock = MagicMock() |
| instance_lock.acquire.return_value = False |
|
|
| with ( |
| patch.object(desktop, "load_server_settings", return_value=settings), |
| patch.object(desktop, "InterprocessFileLock", return_value=instance_lock), |
| patch.object(desktop, "open_admin_when_ready", return_value=True) as open_admin, |
| patch.object(desktop, "ServerSupervisor") as supervisor, |
| ): |
| desktop.launch_desktop(MagicMock()) |
|
|
| open_admin.assert_called_once_with(settings) |
| supervisor.assert_not_called() |
| instance_lock.release.assert_not_called() |
|
|
|
|
| def test_desktop_attaches_to_terminal_server_instead_of_binding_twice() -> None: |
| from free_claude_code.cli import desktop |
|
|
| settings = _settings() |
| instance_lock = MagicMock() |
| instance_lock.acquire.return_value = True |
|
|
| with ( |
| patch.object(desktop, "load_server_settings", return_value=settings), |
| patch.object(desktop, "InterprocessFileLock", return_value=instance_lock), |
| patch.object(desktop, "preflight_proxy", return_value=None), |
| patch.object(desktop, "open_admin_when_ready", return_value=True) as open_admin, |
| patch.object(desktop, "ServerSupervisor") as supervisor, |
| ): |
| desktop.launch_desktop(MagicMock()) |
|
|
| open_admin.assert_called_once_with(settings) |
| supervisor.assert_not_called() |
| instance_lock.release.assert_called_once_with() |
|
|
|
|
| def test_fresh_desktop_launch_disables_console_and_automatic_browser() -> None: |
| from free_claude_code.cli import desktop |
|
|
| settings = _settings() |
| instance_lock = MagicMock() |
| instance_lock.acquire.return_value = True |
| supervisor = MagicMock() |
| controller = MagicMock() |
|
|
| with ( |
| patch.object(desktop, "load_server_settings", return_value=settings), |
| patch.object(desktop, "InterprocessFileLock", return_value=instance_lock), |
| patch.object(desktop, "preflight_proxy", return_value="connection refused"), |
| patch.object(desktop, "ServerSupervisor", return_value=supervisor) as owner, |
| patch.object(desktop, "DesktopController", return_value=controller) as shell, |
| ): |
| tray_factory = MagicMock() |
| desktop.launch_desktop(tray_factory) |
|
|
| owner.assert_called_once_with(console_logging=False) |
| assert shell.call_args.args[:2] == (supervisor, tray_factory) |
| controller.run.assert_called_once_with() |
| instance_lock.release.assert_called_once_with() |
|
|