diff --git a/tests/tools/test_file_sync_back.py b/tests/tools/test_file_sync_back.py index 5b29010795..5075dd3aea 100644 --- a/tests/tools/test_file_sync_back.py +++ b/tests/tools/test_file_sync_back.py @@ -457,3 +457,44 @@ class TestSyncBackSizeCap: # Default cap (2 GiB) is far above our tiny tar; extraction should proceed mgr.sync_back(hermes_home=tmp_path / ".hermes") assert Path(host_file).read_bytes() == b"remote_version" + + +class TestSyncBackWindowsHost: + """#76267: sync_back on a Windows host. The staging tar must be reopenable for writing by + the backend (NamedTemporaryFile held an exclusive handle → PermissionError), and remote + keys/parents must stay POSIX (relpath/Path stringify with backslashes on Windows, so no + staged file ever matched its mapping and every edit was dropped as "no host mapping").""" + + def test_backend_can_reopen_the_tar_path_for_writing(self, tmp_path): + """Contract on every host: the download callback receives a path nothing else holds open.""" + seen = {} + + def download(dest: Path) -> None: + buf = io.BytesIO() + with tarfile.open(fileobj=buf, mode="w") as tar: + info = tarfile.TarInfo(name="root/.hermes/skill.py") + info.size = 2 + tar.addfile(info, io.BytesIO(b"v2")) + with open(dest, "wb") as fh: # the SSH/Modal backends write exactly like this + fh.write(buf.getvalue()) + seen["dest"] = dest + + host_file = tmp_path / "host" / "skill.py" + _write_file(host_file, b"v1") + mgr = _make_manager(tmp_path, [(str(host_file), "/root/.hermes/skill.py")], bulk_download_fn=download) + mgr._pushed_hashes["/root/.hermes/skill.py"] = _sha256_bytes(b"v1") + mgr.sync_back(hermes_home=tmp_path / ".hermes") + assert host_file.read_bytes() == b"v2" + assert not seen["dest"].exists() # staging tar removed after use + + @pytest.mark.windows_only + def test_posix_remote_keys_match_on_windows(self, tmp_path): + host_file = tmp_path / "host" / "skill.py" + _write_file(host_file, b"v1") + mapping = [(str(host_file), "/root/.hermes/skills/a/skill.py")] + mgr = _make_manager(tmp_path, mapping, bulk_download_fn=_make_download_fn({ + "root/.hermes/skills/a/skill.py": b"v2", "root/.hermes/skills/a/new.md": b"new"})) + mgr._pushed_hashes["/root/.hermes/skills/a/skill.py"] = _sha256_bytes(b"v1") + mgr.sync_back(hermes_home=tmp_path / ".hermes") + assert host_file.read_bytes() == b"v2" # relpath key was 'root\\.hermes\\...' → skipped + assert (tmp_path / "host" / "new.md").read_bytes() == b"new" # _infer_host_path parent match