Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion reme/steps/file_io/move.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
original is still removed in that case (move semantics, not copy).
"""

import asyncio
import shutil
from pathlib import Path

Expand Down Expand Up @@ -82,7 +83,7 @@ async def _move(self, src_path: str, dst_path: str, overwrite: bool, retarget: b

# Step 1 — copy. Wikilinks use workspace-relative paths, so outgoing
# targets do not change when their containing document moves.
shutil.copyfile(str(src_abs), str(dst_abs))
await asyncio.to_thread(shutil.copyfile, str(src_abs), str(dst_abs))
payload: dict = {"src_path": src_path, "dst_path": dst_path, "size": dst_abs.stat().st_size}

# Step 2 — retarget. workspace_dir stays consistent throughout: refs still
Expand Down
39 changes: 39 additions & 0 deletions tests/unit/test_crud_steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import asyncio
import os
import tempfile
import threading
import warnings
from pathlib import Path, PureWindowsPath

Expand Down Expand Up @@ -252,6 +253,44 @@ async def run():
asyncio.run(run())


def test_move_copy_does_not_block_event_loop(monkeypatch):
"""A slow file copy must not prevent unrelated async work from running."""

async def run():
with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp):
store = await _make_store({"daily/source.md": "draft"})
step = crud_move.MoveStep(file_store=store)
copy_started = threading.Event()
release_copy = threading.Event()
original_copyfile = crud_move.shutil.copyfile

def blocking_copyfile(src, dst):
copy_started.set()
assert release_copy.wait(timeout=1)
return original_copyfile(src, dst)

monkeypatch.setattr(crud_move.shutil, "copyfile", blocking_copyfile)
release_timer = threading.Timer(0.2, release_copy.set)
release_timer.start()
try:
move_task = asyncio.create_task(
step(src_path="daily/source.md", dst_path="knowledge/source.md", retarget=False),
)
await asyncio.sleep(0.02)

assert copy_started.is_set()
assert not release_copy.is_set(), "synchronous copyfile blocked the event loop"

release_copy.set()
await move_task
finally:
release_copy.set()
release_timer.join()
await store.close()

asyncio.run(run())


def test_move_refuses_overwrite_without_flag():
"""move refuses to clobber an existing dst_path unless overwrite=True."""

Expand Down
Loading