From 0f8ba6f24468c2c319427e9a003055c428b60e88 Mon Sep 17 00:00:00 2001 From: lukyturnflow <3421906768@qq.com> Date: Sun, 25 Oct 2026 10:26:23 +0800 Subject: [PATCH] test(channels): assert the blocked-rename error portably on Windows (#7276) * test(channels): assert the blocked-rename error portably on Windows Rebased onto the channels-store refactor. The suite pins that a failed atomic-rename save must surface the blocked-rename error, never the cleanup error. Replacing a file over a non-empty directory is EISDIR (errno 21) on POSIX but WinError 5 (errno 13, PermissionError) on Windows, so the assertions now accept the platform's blocked-rename error via a shared helper instead of hard-coding errno 21, which broke on Windows dev hosts. Verified by the maintainer on macOS (15/15 green on the POSIX arm) and run here on Windows (15/15 green on the nt arm); POSIX regression signal is unchanged. * style: ruff format 0.15.12 --------- Co-authored-by: Jay0130-a <309071492+Jay0130-a@users.noreply.github.com> --- backend/tests/test_channel_staging_cleanup.py | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/backend/tests/test_channel_staging_cleanup.py b/backend/tests/test_channel_staging_cleanup.py index da29d2d160..dac7f48b1c 100644 --- a/backend/tests/test_channel_staging_cleanup.py +++ b/backend/tests/test_channel_staging_cleanup.py @@ -11,6 +11,7 @@ made the save fail — and the staged file, which for the two credential stores from __future__ import annotations import logging +import os from pathlib import Path import pytest @@ -22,6 +23,22 @@ from app.channels.wechat import WechatChannel CLEANUP_ERRNO = 28 +# Replacing a file over a non-empty directory is EISDIR (errno 21) on POSIX and +# WinError 5 (errno 13, PermissionError) on Windows. The regression this file +# pins is "cleanup must not replace the save error", not the platform's errno, +# so accept the platform's blocked-rename error and assert it is not the +# cleanup error instead of hard-coding errno 21 (which failed on Windows dev +# hosts). +BLOCKED_RENAME_ERRORS: tuple[type[BaseException], ...] = (IsADirectoryError, PermissionError) if os.name == "nt" else (IsADirectoryError,) + + +def _assert_blocked_rename_error(exc: BaseException) -> None: + """The save error must be the blocked rename, never the cleanup error.""" + if os.name == "nt": + assert getattr(exc, "errno", None) != CLEANUP_ERRNO + else: + assert getattr(exc, "errno", None) == 21 + def _block_destination(destination: Path) -> None: """Make the atomic rename fail with a deterministic errno-21 error. @@ -67,10 +84,10 @@ def test_json_channel_store_keeps_the_save_error(tmp_path, cleanup_fails, caplog store = JsonChannelStore(path) store._data = {"feishu:chat": {"thread_id": "t"}} - with caplog.at_level(logging.WARNING), pytest.raises(IsADirectoryError) as raised: + with caplog.at_level(logging.WARNING), pytest.raises(BLOCKED_RENAME_ERRORS) as raised: store._save() - assert raised.value.errno == 21 + _assert_blocked_rename_error(raised.value) assert "Failed to remove temporary channel store file" in caplog.text @@ -78,10 +95,10 @@ def test_runtime_config_store_keeps_the_save_error(tmp_path, cleanup_fails, capl store = ChannelRuntimeConfigStore(tmp_path / "channels.json") _block_destination(store._path) - with caplog.at_level(logging.WARNING), pytest.raises(IsADirectoryError) as raised: + with caplog.at_level(logging.WARNING), pytest.raises(BLOCKED_RENAME_ERRORS) as raised: store._save({"feishu": {"app_id": "id"}}) - assert raised.value.errno == 21 + _assert_blocked_rename_error(raised.value) assert "Failed to remove temporary channel runtime config file" in caplog.text @@ -98,7 +115,7 @@ def test_wechat_auth_state_persist_failure_stays_attributable(tmp_path, cleanup_ records = [r for r in caplog.records if "failed to persist auth state" in r.getMessage()] assert len(records) == 1 - assert isinstance(records[0].exc_info[1], IsADirectoryError) + assert isinstance(records[0].exc_info[1], BLOCKED_RENAME_ERRORS) assert "Failed to remove temporary WeChat auth state file" in caplog.text @@ -121,7 +138,7 @@ def test_runtime_config_store_still_removes_the_staged_file(tmp_path): store = ChannelRuntimeConfigStore(tmp_path / "channels.json") _block_destination(store._path) - with pytest.raises(IsADirectoryError): + with pytest.raises(BLOCKED_RENAME_ERRORS): store._save({"feishu": {"app_id": "id"}}) assert _staged_files(tmp_path) == [] @@ -149,10 +166,10 @@ def test_a_raising_log_handler_does_not_replace_the_save_error(tmp_path, cleanup store = JsonChannelStore(path) store._data = {"feishu:chat": {"thread_id": "t"}} - with caplog.at_level(logging.WARNING), pytest.raises(IsADirectoryError) as raised: + with caplog.at_level(logging.WARNING), pytest.raises(BLOCKED_RENAME_ERRORS) as raised: store._save() - assert raised.value.errno == 21 + _assert_blocked_rename_error(raised.value) finally: store_module.logger.removeHandler(handler)