<!-- CURSOR_SUMMARY --> > [!NOTE] > **Medium Risk** > Touches authentication/login flow, credentials-file permissions, and subprocess lifecycle behavior across platforms; while mostly defensive, regressions could impact login or process management on Windows/macOS runners. > > **Overview** > Improves Windows/cross-platform reliability across the CLI and MCP server: OAuth login now binds the callback server to `127.0.0.1`, avoids slow loopback reverse-DNS, adds a configurable callback timeout (`--timeout` + env default), and opens URLs via a Windows-friendly `_open_browser` to avoid flashing console windows. > > Centralizes CLI output via a shared `console` that forces UTF-8 on Windows, standardizes UTF-8 file reads/writes throughout, tightens credentials-file permissions on Windows using `icacls`, and adds shared Windows subprocess helpers for **no-window** process creation and graceful termination (used by `deploy`, MCP reload, and usage-tracking worker). > > Updates client configuration UX/robustness (Windows AppData resolution via `platformdirs`, Cursor config path fallbacks + compatibility writes, overwrite warnings, absolute `uv` path for GUI clients, safer path display) and improves `deploy` child-process handling to avoid pipe-buffer deadlocks while giving better debug-aware error messages. > > Expands CI to run tests on Linux/Windows/macOS, adds a no-auth CLI integration workflow, disables usage tracking in toolkits CI, and adds extensive regression tests for Windows signals, subprocess cleanup, UTF-8, and config-path edge cases; bumps `arcade-core` to `4.4.2` and `arcade-mcp-server` to `1.17.2` (with updated dependency pin). > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 0fabd8ca1cd647039ba6ddbdf3f7809c330bab9e. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
161 lines
5.9 KiB
Python
161 lines
5.9 KiB
Python
"""Tests for the console.py encoding safety layer.
|
|
|
|
These tests verify that _needs_utf8() and _configure_windows_utf8()
|
|
behave correctly and do not crash, even when the console encoding
|
|
would be cp1252 (the default on many Western-European Windows installs).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import io
|
|
import os
|
|
import sys
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
from arcade_cli.console import _configure_windows_utf8, _needs_utf8
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _needs_utf8()
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestNeedsUtf8:
|
|
"""Unit tests for _needs_utf8()."""
|
|
|
|
@pytest.mark.parametrize(
|
|
"encoding, expected",
|
|
[
|
|
("utf-8", False),
|
|
("UTF-8", False),
|
|
("utf8", False),
|
|
("UTF8", False),
|
|
("cp1252", True),
|
|
("ascii", True),
|
|
("latin-1", True),
|
|
("", True),
|
|
(None, True),
|
|
],
|
|
)
|
|
def test_known_encodings(self, encoding: str | None, expected: bool) -> None:
|
|
assert _needs_utf8(encoding) is expected
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _configure_windows_utf8()
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestConfigureWindowsUtf8:
|
|
"""Tests for _configure_windows_utf8()."""
|
|
|
|
def test_noop_on_non_windows(self) -> None:
|
|
"""On non-Windows platforms the function should be a no-op."""
|
|
with patch.object(sys, "platform", "linux"):
|
|
# Should not raise and not change anything.
|
|
_configure_windows_utf8()
|
|
|
|
def test_reconfigures_when_cp1252(self) -> None:
|
|
"""Simulate a cp1252 stdout on 'win32' and verify reconfigure is called."""
|
|
fake_stdout = io.TextIOWrapper(io.BytesIO(), encoding="cp1252")
|
|
fake_stderr = io.TextIOWrapper(io.BytesIO(), encoding="cp1252")
|
|
|
|
with (
|
|
patch.object(sys, "platform", "win32"),
|
|
patch.object(sys, "stdout", fake_stdout),
|
|
patch.object(sys, "stderr", fake_stderr),
|
|
):
|
|
_configure_windows_utf8()
|
|
|
|
# After reconfiguration the streams should be utf-8.
|
|
assert fake_stdout.encoding.lower().replace("-", "") == "utf8"
|
|
assert fake_stderr.encoding.lower().replace("-", "") == "utf8"
|
|
|
|
def test_sets_pythonioencoding_env(self) -> None:
|
|
"""When reconfiguring, PYTHONIOENCODING should be set as a fallback."""
|
|
fake_stdout = io.TextIOWrapper(io.BytesIO(), encoding="cp1252")
|
|
fake_stderr = io.TextIOWrapper(io.BytesIO(), encoding="cp1252")
|
|
|
|
env_copy = os.environ.copy()
|
|
env_copy.pop("PYTHONIOENCODING", None)
|
|
|
|
with (
|
|
patch.object(sys, "platform", "win32"),
|
|
patch.object(sys, "stdout", fake_stdout),
|
|
patch.object(sys, "stderr", fake_stderr),
|
|
patch.dict(os.environ, env_copy, clear=True),
|
|
):
|
|
_configure_windows_utf8()
|
|
assert os.environ.get("PYTHONIOENCODING") == "utf-8"
|
|
|
|
def test_does_not_override_existing_pythonioencoding(self) -> None:
|
|
"""If PYTHONIOENCODING is already set, don't overwrite it."""
|
|
fake_stdout = io.TextIOWrapper(io.BytesIO(), encoding="cp1252")
|
|
fake_stderr = io.TextIOWrapper(io.BytesIO(), encoding="cp1252")
|
|
|
|
with (
|
|
patch.object(sys, "platform", "win32"),
|
|
patch.object(sys, "stdout", fake_stdout),
|
|
patch.object(sys, "stderr", fake_stderr),
|
|
patch.dict(os.environ, {"PYTHONIOENCODING": "ascii"}, clear=False),
|
|
):
|
|
_configure_windows_utf8()
|
|
# Should keep the existing value.
|
|
assert os.environ["PYTHONIOENCODING"] == "ascii"
|
|
|
|
def test_no_crash_when_reconfigure_missing(self) -> None:
|
|
"""Streams without a reconfigure method should not crash."""
|
|
|
|
class FakeStream:
|
|
encoding = "cp1252"
|
|
# No reconfigure attribute.
|
|
|
|
with (
|
|
patch.object(sys, "platform", "win32"),
|
|
patch.object(sys, "stdout", FakeStream()),
|
|
patch.object(sys, "stderr", FakeStream()),
|
|
):
|
|
# Should not raise.
|
|
_configure_windows_utf8()
|
|
|
|
def test_noop_when_already_utf8(self) -> None:
|
|
"""If both streams are already utf-8, nothing should be reconfigured."""
|
|
fake_stdout = io.TextIOWrapper(io.BytesIO(), encoding="utf-8")
|
|
fake_stderr = io.TextIOWrapper(io.BytesIO(), encoding="utf-8")
|
|
|
|
reconfigure_called = False
|
|
original_reconfigure = fake_stdout.reconfigure
|
|
|
|
def tracking_reconfigure(**kwargs):
|
|
nonlocal reconfigure_called
|
|
reconfigure_called = True
|
|
return original_reconfigure(**kwargs)
|
|
|
|
fake_stdout.reconfigure = tracking_reconfigure # type: ignore[assignment]
|
|
|
|
with (
|
|
patch.object(sys, "platform", "win32"),
|
|
patch.object(sys, "stdout", fake_stdout),
|
|
patch.object(sys, "stderr", fake_stderr),
|
|
):
|
|
_configure_windows_utf8()
|
|
assert not reconfigure_called, "reconfigure should not be called when already utf-8"
|
|
|
|
def test_emoji_output_after_reconfigure(self) -> None:
|
|
"""After reconfiguring a cp1252 stream, writing emoji should not crash."""
|
|
buf = io.BytesIO()
|
|
fake_stdout = io.TextIOWrapper(buf, encoding="cp1252")
|
|
|
|
with (
|
|
patch.object(sys, "platform", "win32"),
|
|
patch.object(sys, "stdout", fake_stdout),
|
|
patch.object(sys, "stderr", fake_stdout),
|
|
):
|
|
_configure_windows_utf8()
|
|
# Now write emoji — should not raise UnicodeEncodeError.
|
|
fake_stdout.write("Hello! \u2705 \U0001f680 Done.\n")
|
|
fake_stdout.flush()
|
|
|
|
output = buf.getvalue().decode("utf-8")
|
|
assert "\u2705" in output
|
|
assert "\U0001f680" in output
|