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 @@ 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: