From ea68e5b3d77c7330bc36c1927424937a08ee82ed Mon Sep 17 00:00:00 2001 From: xiaodu55 <1704788159@qq.com> Date: Sun, 11 Oct 2026 12:53:30 +0800 Subject: [PATCH] test(local-dev): normalize the upgrade script's MSYS path rendering on Windows (#6660) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test(local-dev): normalize the upgrade script's MSYS path rendering on Windows nginx-local-conf.sh computes its repo root with pwd -P, which inside Git Bash yields the MSYS mount form (/tmp/... for the Windows temp dir) — correct for its bash-native consumers (serve.sh, nginx.sh) but never equal to the test's Windows-form Path.resolve(). Map the rendering back through cygpath -w before comparing, so the tests read and compare the rendered config on Git Bash hosts too. Fixes #6657 * test(local-dev): harden Git Bash path normalization --------- Co-authored-by: xiaodu55 Co-authored-by: Willem Jiang --- backend/tests/AGENTS.md | 5 + backend/tests/test_local_dev_loopback_bind.py | 113 +++++++++++++++++- 2 files changed, 114 insertions(+), 4 deletions(-) diff --git a/backend/tests/AGENTS.md b/backend/tests/AGENTS.md index f25ddbfa6b..46cc210e2d 100644 --- a/backend/tests/AGENTS.md +++ b/backend/tests/AGENTS.md @@ -2,6 +2,11 @@ Backend tests must preserve the runtime invariants they exercise without changing production execution topology. +Local nginx path fixtures convert MSYS output through the selected Git Bash's +PATH and decode both renderer and converter output as UTF-8. POSIX Git-layout +stubs exercise Windows-style locale decoding and missing/failed/empty converters; +native Windows keeps the real Git Bash renderer checks, including Unicode paths. + `test_skills_custom_multiprocess.py` starts independent spawn workers sharing a temporary custom-skill root. Park edit/rollback before the write and before the history append, observe the peer reaching the mutation lock, then check both diff --git a/backend/tests/test_local_dev_loopback_bind.py b/backend/tests/test_local_dev_loopback_bind.py index 398f63b714..84eec16ea7 100644 --- a/backend/tests/test_local_dev_loopback_bind.py +++ b/backend/tests/test_local_dev_loopback_bind.py @@ -17,9 +17,11 @@ from __future__ import annotations import os import re +import shlex import shutil import subprocess from pathlib import Path +from types import SimpleNamespace import pytest from support.shell import require_script_bash @@ -100,9 +102,11 @@ def test_stop_signals_nginx_through_the_tracked_config(): # ── nginx-local-conf.sh ───────────────────────────────────────────────────── -@pytest.fixture -def checkout(tmp_path: Path) -> Path: +@pytest.fixture(params=["checkout", "checkout-测试 with spaces"], ids=["ascii", "unicode-spaces"]) +def checkout(tmp_path: Path, request: pytest.FixtureRequest) -> Path: """A minimal checkout: the script resolves the repo root from its own location.""" + tmp_path = tmp_path / request.param + tmp_path.mkdir() (tmp_path / "scripts").mkdir() (tmp_path / "docker" / "nginx").mkdir(parents=True) shutil.copy2(NGINX_LOCAL_CONF_SH, tmp_path / "scripts" / NGINX_LOCAL_CONF_SH.name) @@ -119,16 +123,117 @@ def _run(checkout: Path, bind_host: str | None) -> subprocess.CompletedProcess[s env=env, capture_output=True, text=True, + encoding="utf-8", check=False, ) +def _normalize_rendered_path(rendered: str) -> str: + """Map the script's POSIX rendering back to Windows form on Git Bash hosts. + + ``pwd -P`` inside Git Bash yields the MSYS mount form (``/tmp/...`` for the + Windows temp dir), which the test's Windows-side ``Path`` can neither + compare nor read. Resolve cygpath through the selected Git Bash's PATH, + including when bash.exe lives in Git's wrapper directory. + """ + if os.name != "nt" or not rendered.startswith("/"): + return rendered + result = subprocess.run( + [require_script_bash(), "-c", 'cygpath -w "$DEERFLOW_TEST_RENDERED_PATH"'], + env={**os.environ, "DEERFLOW_TEST_RENDERED_PATH": rendered}, + capture_output=True, + text=True, + encoding="utf-8", + check=False, + ) + if result.returncode != 0: + pytest.fail(f"Git Bash cygpath failed to normalize {rendered!r} (exit {result.returncode}): {result.stderr.strip()}") + normalized = result.stdout.strip() + if not normalized: + pytest.fail(f"Git Bash cygpath returned an empty path for {rendered!r}") + return normalized + + +def _simulate_windows_locale(monkeypatch) -> None: + native_run = subprocess.run + + def run_with_windows_locale(*args, **kwargs): + # Exercise real byte decoding even on UTF-8 POSIX hosts. + if kwargs.get("text"): + kwargs.setdefault("encoding", "cp1252") + return native_run(*args, **kwargs) + + monkeypatch.setitem(globals(), "subprocess", SimpleNamespace(run=run_with_windows_locale)) + + +@pytest.fixture(params=["bin", "usr/bin"], ids=["wrapper", "usr-bin"]) +def git_bash_layout(tmp_path: Path, monkeypatch, request: pytest.FixtureRequest) -> Path: + """Run a POSIX shim with Git for Windows' wrapper and usr/bin layouts.""" + if os.name == "nt": + pytest.skip("POSIX executable shims; native Windows uses the real renderer tests") + native_bash = require_script_bash() + git_root = tmp_path / "Git installation" + usr_bin = git_root / "usr" / "bin" + usr_bin.mkdir(parents=True) + bash = git_root / request.param / "bash.exe" + bash.parent.mkdir(parents=True, exist_ok=True) + bash.write_text(f'#!/bin/sh\nexport PATH={shlex.quote(str(usr_bin))}\nexec {shlex.quote(native_bash)} "$@"\n', encoding="utf-8") + bash.chmod(0o755) + monkeypatch.setitem(globals(), "require_script_bash", lambda: str(bash)) + monkeypatch.setitem(globals(), "os", SimpleNamespace(name="nt", environ=os.environ)) + return usr_bin + + +def _write_cygpath_stubs(directory: Path, body: str) -> None: + # POSIX Bash resolves the bare name; the .exe also models the old lookup. + for name in ("cygpath", "cygpath.exe"): + path = directory / name + path.write_text("#!/bin/sh\n" + body + "\n", encoding="utf-8") + path.chmod(0o755) + + +def test_rendered_path_uses_selected_git_bash(git_bash_layout: Path, monkeypatch): + rendered = "/tmp/测试 user/$(printf changed)/nginx.local.conf" + native = r"C:\Users\测试 User\AppData\Local\Temp\nginx.local.conf" + monkeypatch.setenv("EXPECTED_MSYS_PATH", rendered) + monkeypatch.setenv("EXPECTED_NATIVE_PATH", native) + _write_cygpath_stubs( + git_bash_layout, + '[ "$1" = "-w" ] || exit 2\n[ "$2" = "$EXPECTED_MSYS_PATH" ] || exit 3\nprintf "%s\\n" "$EXPECTED_NATIVE_PATH"', + ) + _simulate_windows_locale(monkeypatch) + + assert _normalize_rendered_path(rendered) == native + + +@pytest.mark.parametrize("behavior", ["missing", "failed", "empty"]) +def test_rendered_path_conversion_fails_loudly(git_bash_layout: Path, behavior: str): + if behavior == "failed": + _write_cygpath_stubs(git_bash_layout, 'echo "converter failed" >&2\nexit 5') + elif behavior == "empty": + _write_cygpath_stubs(git_bash_layout, "exit 0") + + with pytest.raises(pytest.fail.Exception, match="cygpath") as failure: + _normalize_rendered_path("/tmp/nginx.local.conf") + if behavior == "failed": + assert "converter failed" in str(failure.value) + + +def test_renderer_decodes_paths_as_utf8(checkout: Path, monkeypatch): + _simulate_windows_locale(monkeypatch) + + result = _run(checkout, None) + + assert result.returncode == 0, result.stderr + assert Path(_normalize_rendered_path(result.stdout.strip())) == (checkout / "docker" / "nginx" / "nginx.local.conf").resolve() + + @pytest.mark.parametrize("bind_host", [None, "", "127.0.0.1"], ids=["unset", "blank", "loopback"]) def test_default_bind_host_uses_the_tracked_config(checkout: Path, bind_host: str | None): result = _run(checkout, bind_host) assert result.returncode == 0, result.stderr - assert Path(result.stdout.strip()) == (checkout / "docker" / "nginx" / "nginx.local.conf").resolve() + assert Path(_normalize_rendered_path(result.stdout.strip())) == (checkout / "docker" / "nginx" / "nginx.local.conf").resolve() assert not (checkout / "temp").exists() @@ -163,7 +268,7 @@ def test_bind_host_renders_a_config_listening_there(checkout: Path, bind_host: s result = _run(checkout, bind_host) assert result.returncode == 0, result.stderr - rendered = Path(result.stdout.strip()) + rendered = Path(_normalize_rendered_path(result.stdout.strip())) assert rendered == (checkout / "temp" / "nginx.local.conf").resolve() content = rendered.read_text(encoding="utf-8") assert _listen_directives(content) == expected