diff --git a/cloudhop/templates/wizard.html b/cloudhop/templates/wizard.html
index 8ead47a..249aa4a 100644
--- a/cloudhop/templates/wizard.html
+++ b/cloudhop/templates/wizard.html
@@ -260,7 +260,7 @@
⇄
-
Two-Way Mirror ⓘ
+
Two-Way Sync ⓘ
Mirror both directions. Changes on either side are propagated to the other.
diff --git a/cloudhop/tests/test_schedule.py b/cloudhop/tests/test_schedule.py
index 87f150c..01c7a50 100644
--- a/cloudhop/tests/test_schedule.py
+++ b/cloudhop/tests/test_schedule.py
@@ -190,6 +190,7 @@ def test_ac_power_no_pause(self, tmp_path):
"""On AC power, transfer should not be paused."""
mgr = TransferManager(cm_dir=str(tmp_path))
mgr.state["pause_on_battery"] = True
+ mgr._has_battery = True # Force battery capability for CI (Linux/Windows)
with patch.object(mgr, "_is_on_battery", return_value=False):
with patch.object(mgr, "is_rclone_running", return_value=True):
with patch.object(mgr, "pause") as mock_pause:
@@ -200,6 +201,7 @@ def test_battery_below_threshold_pauses(self, tmp_path):
"""On battery power, running transfer should be paused."""
mgr = TransferManager(cm_dir=str(tmp_path))
mgr.state["pause_on_battery"] = True
+ mgr._has_battery = True # Force battery capability for CI (Linux/Windows)
with patch.object(mgr, "_is_on_battery", return_value=True):
with patch.object(mgr, "is_rclone_running", return_value=True):
with patch.object(mgr, "pause") as mock_pause:
@@ -212,6 +214,7 @@ def test_recovery_battery_to_ac_resumes(self, tmp_path):
mgr = TransferManager(cm_dir=str(tmp_path))
mgr.state["pause_on_battery"] = True
mgr.state["_battery_paused"] = True
+ mgr._has_battery = True # Force battery capability for CI (Linux/Windows)
mgr.rclone_cmd = ["rclone", "copy", "a:", "b:"]
with patch.object(mgr, "_is_on_battery", return_value=False):
with patch.object(mgr, "is_rclone_running", return_value=False):
diff --git a/cloudhop/tests/test_transfer.py b/cloudhop/tests/test_transfer.py
index 005338b..3e5be4c 100644
--- a/cloudhop/tests/test_transfer.py
+++ b/cloudhop/tests/test_transfer.py
@@ -317,7 +317,7 @@ def test_pause_kills_process_windows(self, mock_sleep, mock_run, mock_sys, manag
assert result["ok"] is True
assert "5555" in result["msg"]
mock_run.assert_called_once_with(
- ["taskkill", "/F", "/T", "/PID", "5555"], capture_output=True
+ ["taskkill", "/F", "/T", "/PID", "5555"], capture_output=True, timeout=10
)
assert manager.rclone_pid is None
diff --git a/cloudhop/transfer.py b/cloudhop/transfer.py
index fea6e6c..f224aad 100644
--- a/cloudhop/transfer.py
+++ b/cloudhop/transfer.py
@@ -2123,7 +2123,15 @@ def _start_transfer_locked(self, body: Dict[str, Any]) -> Dict[str, Any]:
transfers = 8
except (ValueError, TypeError):
transfers = 8
- excludes: List[str] = body.get("excludes", [])
+ # [V901] Accept both "excludes" (list) and "exclude" (string/list)
+ # so API callers using either key name get the correct behaviour.
+ excludes_raw = body.get("excludes", body.get("exclude", []))
+ if isinstance(excludes_raw, str):
+ excludes = [e.strip() for e in excludes_raw.split(",") if e.strip()]
+ elif isinstance(excludes_raw, list):
+ excludes = [str(e).strip() for e in excludes_raw if str(e).strip()]
+ else:
+ excludes = []
bw_limit: str = body.get("bw_limit", "")
source_type: str = body.get("source_type", "")
dest_type: str = body.get("dest_type", "")
@@ -2202,7 +2210,10 @@ def _start_transfer_locked(self, body: Dict[str, Any]) -> Dict[str, Any]:
if not _dest_path:
# Dest is cloud root — determine source folder name
_src_folder = ""
- if source_type == "local":
+ # [V906] Infer source_type when not provided (API callers
+ # may omit it). A path without ":" is assumed local.
+ _effective_src_type = source_type or ("local" if ":" not in source else "")
+ if _effective_src_type == "local":
if os.path.isdir(source):
_src_folder = os.path.basename(source.rstrip("/\\"))
else: