test(local-dev): normalize the upgrade script's MSYS path rendering on Windows (#6660)

* 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 <xiaodu55@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
xiaodu55 2026-10-11 12:53:30 +08:00 committed by GitHub
parent 782fed9135
commit ea68e5b3d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 114 additions and 4 deletions

View File

@ -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

View File

@ -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