From d968f3ce12ed21e56fd816ba7528a6ccc066ee9f Mon Sep 17 00:00:00 2001 From: BigDataDZ <76271875+BigDataDZ@users.noreply.github.com> Date: Mon, 2 Nov 2026 17:56:31 +0800 Subject: [PATCH] fix(windows): preserve long paths when copying releases Signed-off-by: BigDataDZ <76271875+BigDataDZ@users.noreply.github.com> --- loopx/windows_install.py | 3 +++ tests/test_windows_install.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/loopx/windows_install.py b/loopx/windows_install.py index d68b162d27..b504ac4be8 100644 --- a/loopx/windows_install.py +++ b/loopx/windows_install.py @@ -12,6 +12,7 @@ from typing import Any, Sequence from .file_lock import exclusive_file_lock +from .control_plane.runtime.file_paths import windows_extended_path from .release_manifest import write_release_manifest from .skill_install_readback import ( PACKAGED_HOST_SKILL_IDS, @@ -87,6 +88,8 @@ def _ensure_chat_bundle( def _copy_release(source_root: Path, target: Path) -> None: + source_root = windows_extended_path(source_root) + target = windows_extended_path(target) ignored = shutil.ignore_patterns( "__pycache__", "*.pyc", "node_modules", ".next", "dist", "build", "coverage" ) diff --git a/tests/test_windows_install.py b/tests/test_windows_install.py index d90690c1b2..f8a88cec38 100644 --- a/tests/test_windows_install.py +++ b/tests/test_windows_install.py @@ -15,6 +15,7 @@ from loopx import windows_install from loopx.skill_install_readback import PACKAGED_HOST_SKILL_IDS +from loopx.control_plane.runtime.file_paths import windows_extended_path def _run_loopx( @@ -36,6 +37,35 @@ def _run_loopx( ) +@pytest.mark.skipif(os.name != "nt", reason="native Windows long-path copy") +@pytest.mark.parametrize("long_side", ["source", "target"]) +def test_release_copy_preserves_files_beyond_max_path(tmp_path: Path, long_side: str) -> None: + source, target = tmp_path / "source", tmp_path / "release" + suffix = Path("install-" + "a" * 70) / ("b" * 70) / ("c" * 70) + if long_side == "source": + source /= suffix + else: + target /= suffix + content = b"release bytes\r\n\x1a\x00\xff" + guide = windows_extended_path(source / "docs" / "guide.txt") + guide.parent.mkdir(parents=True) + guide.write_bytes(content) + readme = windows_extended_path(source / "README.md") + readme.write_bytes("# 安装记录 🚀\n".encode("utf-8")) + ignored = guide.parent / "node_modules" / "generated.txt" + ignored.parent.mkdir() + ignored.write_bytes(b"excluded dependency") + longest = (source if long_side == "source" else target) / "docs" / "guide.txt" + assert len(str(longest)) > 260 + + windows_install._copy_release(source, target) + + assert windows_extended_path(target / "docs" / "guide.txt").read_bytes() == content + assert windows_extended_path(target / "README.md").read_bytes() == readme.read_bytes() + assert not windows_extended_path(target / "docs" / "node_modules").exists() + assert guide.read_bytes() == content + + def _windows_user_path(pwsh: str) -> str: result = subprocess.run( [