Files
shelfmark/tests/slskd/test_handler_isolated.py
T
CaliBrain a00f3d28bf fix(slskd): refuse unsafe peer paths, keep queued downloads alive, report search outages (#1512)
Fixes four defects in the Soulseek (slskd)

A peer names its own files, and Shelfmark works out where slskd stored
one from the last folder and the file name. `remote_directory_name`
returned the whole directory when its last segment was empty. So
`../../../etc//passwd.epub` or `/abs/path//book.epub` resolved outside
the downloads folder, and Shelfmark imported whatever was there. For a
multi-file release it moved those files into staging (`STAGE_MOVE`).

- New `safe_remote_segments` refuses a name with an empty, `.` or `..`
segment (which covers absolute and UNC paths), a NUL, or a drive
anywhere but a Windows share root (`C:\Books\…`, which never reaches a
local path).
- Search (`iter_peer_files`, so `build_releases`) drops such files.
- `download` refuses them before anything is queued, in both modes. The
spec arrives with the queued release payload, so search filtering alone
is not enough.
- `local_path_for` also refuses a path that resolves outside the root
(`resolve()` + `is_relative_to`), for example through a symlinked
folder.

`_poll`, used when isolation is off and as the fallback for slskd <
0.26, never asked for an activity grace. A file "Queued, Remotely" sent
the same status and progress on every poll, so the orchestrator
cancelled it after 300s and `SLSKD_QUEUE_TIMEOUT_MINUTES` never applied.
It now requests, renews and releases the grace the same way isolated
mode does.

`search` caught every exception per query, so slskd being down or
rejecting the API key showed as "no releases". When no query gets an
answer it now raises `SourceUnavailableError`, as Prowlarr does, and the
error reaches the user. One failed query among answered ones is still
not an outage.

A retry reuses the task's folder, and slskd renames a new file when one
is already there. Files from a failed attempt therefore meant
`_folder_complete`'s exact count could never match again. Now:
- a failed or cancelled attempt removes its folder;
- a new attempt removes any finished files an earlier one left (for
example after a restart) before it queues the batch.

`tests/slskd/conftest.py` stubbed `flask_socketio` whenever it had not
been imported yet, even though it is installed. Running `pytest
tests/slskd tests/` therefore broke the Flask app tests in the same
worker (285 errors, also on 10710ab). It now stubs only when the module
is missing.

- 25 new regression tests. All of them fail on 10710ab.
- `make python-checks` passes.
- `uv run pytest tests/slskd tests/ -m "not integration and not e2e"`
passes: 4047 tests on e83bab9.
2026-10-21 01:58:13 -04:00

607 lines
23 KiB
Python

"""The handler's isolated mode: one folder per download, polled by id, confirmed on disk.
Several apps can share one slskd, and any of them can clear its list of finished transfers.
A download that watched the list would then "lose" a file that had actually arrived. These
tests pin the behaviour that does not: each download gets its own folder, each transfer is
read by its id, the files on disk decide whether it finished, and the finished files stay in
slskd's folder (still shared) while the orchestrator copies them.
"""
from __future__ import annotations
import json
from threading import Event
import pytest
import shelfmark.release_sources.slskd.handler as mod
from shelfmark.core.models import DownloadTask
from shelfmark.download.activity import ACTIVITY_GRACE_STATUS
from shelfmark.release_sources.slskd.api import SlskdBatchUnsupportedError
from shelfmark.release_sources.slskd.handler import SlskdHandler
REMOTE_DIR = "@@share\\Books\\Author"
FILE_A = f"{REMOTE_DIR}\\Book One.epub"
FILE_B = f"{REMOTE_DIR}\\Book Two.epub"
FILES = [{"filename": FILE_A, "size": 5}, {"filename": FILE_B, "size": 7}]
OK = "Completed, Succeeded"
class Recorder:
def __init__(self) -> None:
self.progress: list[float] = []
self.status: list[tuple[str, str | None]] = []
def progress_callback(self, value: float) -> None:
self.progress.append(value)
def status_callback(self, status: str, message: str | None) -> None:
self.status.append((status, message))
@property
def real(self) -> list[tuple[str, str | None]]:
"""Statuses a user would see, without the stall-grace signals."""
return [(s, m) for s, m in self.status if s != ACTIVITY_GRACE_STATUS]
@property
def statuses(self) -> list[str]:
return [s for s, _ in self.real]
@property
def graces(self) -> list[float]:
return [float(m) for s, m in self.status if s == ACTIVITY_GRACE_STATUS]
class FakeSlskd:
"""slskd as the handler sees it: batches, transfers by id, a list, events."""
def __init__(self, root, *, script=None, write_on_success=True):
self.root = root
# script[filename] = list of states, one per get_transfer call; the last repeats.
# A state of None means slskd no longer has the transfer (cleared from its list).
self.script = script or {}
self.write_on_success = write_on_success
self.batches: list[dict] = []
self.cancelled: list[tuple[str, str, bool]] = []
self.list_calls = 0
self.events: list[dict] = []
self.refuse: list[dict] = []
self.batch_error: Exception | None = None
self._calls: dict[str, int] = {}
self._written: set[str] = set()
self._ids: dict[str, str] = {}
self.on_get = None
def enqueue_batch(self, username, files, destination):
if self.batch_error:
raise self.batch_error
self.batches.append({"username": username, "files": files, "destination": destination})
self._destination = destination
transfers = []
for i, f in enumerate(files):
if any(r["filename"] == f["filename"] for r in self.refuse):
continue
tid = f"t{i}"
self._ids[tid] = f["filename"]
transfers.append({"id": tid, "filename": f["filename"], "state": "Requested"})
return {"batch_id": "b-1", "transfers": transfers, "failures": list(self.refuse)}
def get_transfer(self, username, transfer_id):
filename = self._ids[transfer_id]
count = self._calls.get(filename, 0)
self._calls[filename] = count + 1
states = self.script.get(filename, [OK])
state = states[min(count, len(states) - 1)]
if self.on_get:
self.on_get(self, filename, state)
if state is None:
return None
size = next(f["size"] for f in self.batches[-1]["files"] if f["filename"] == filename)
if state == OK and self.write_on_success and filename not in self._written:
folder = self.root / self._destination
folder.mkdir(parents=True, exist_ok=True)
target = folder / filename.rsplit("\\", 1)[-1]
if target.exists(): # slskd keeps the file already there and renames the new one
target = target.with_stem(f"{target.stem}_639000000000000000")
target.write_bytes(b"x" * size)
self._written.add(filename)
done = size if state == OK else 0
return {
"id": transfer_id,
"filename": filename,
"state": state,
"size": size,
"bytesTransferred": done,
"averageSpeed": 1000.0,
"placeInQueue": 3 if "Remotely" in state else None,
}
def list_downloads(self, username):
self.list_calls += 1
return [
{"id": tid, "filename": fn, "state": "InProgress", "removed": False}
for tid, fn in self._ids.items()
]
def cancel_transfer(self, username, transfer_id, *, remove):
self.cancelled.append((username, transfer_id, remove))
return True
def get_events(self, *, limit=200, offset=0):
return list(self.events)
def get_download_directory(self):
return str(self.root)
@pytest.fixture
def cfg(monkeypatch, tmp_path):
values: dict = {"SLSKD_DOWNLOAD_PATH": str(tmp_path)}
monkeypatch.setattr(mod.config, "get", lambda k, d=None: values.get(k, d))
return values
@pytest.fixture
def handler(monkeypatch):
monkeypatch.setattr(SlskdHandler, "_poll_interval", lambda self: 0)
monkeypatch.setattr(SlskdHandler, "_completed_path_retry_interval", lambda self: 0)
monkeypatch.setattr(SlskdHandler, "_completed_path_timeout_seconds", lambda self: 0)
monkeypatch.setattr(SlskdHandler, "_queue_timeout_seconds", lambda self: 0)
monkeypatch.setattr(SlskdHandler, "_cancel_remove_wait", lambda self: None)
monkeypatch.setattr(mod, "MAX_MISSING_POLLS", 3)
return SlskdHandler()
def _task(files=None, task_id="slskd:abc"):
files = files if files is not None else FILES
return DownloadTask(
task_id=task_id,
source="slskd",
title="Book",
retry_source_context={"username": "peer", "directory": REMOTE_DIR, "files": files},
)
def _run(handler, fake, task=None, cancel=None):
handler._get_client = lambda: fake # type: ignore[method-assign]
rec = Recorder()
task = task or _task()
result = handler.download(task, cancel or Event(), rec.progress_callback, rec.status_callback)
return result, task, rec
class TestHappyPath:
def test_the_task_gets_its_own_folder_and_it_is_what_the_orchestrator_receives(
self, handler, cfg, tmp_path
):
fake = FakeSlskd(
tmp_path, script={FILE_A: ["Queued, Remotely", "InProgress", OK], FILE_B: [OK]}
)
result, _task_obj, rec = _run(handler, fake)
folder = tmp_path / "shelfmark" / "slskd_abc"
assert result == str(folder)
assert sorted(p.name for p in folder.iterdir()) == ["Book One.epub", "Book Two.epub"]
assert fake.batches[0]["destination"] == "shelfmark/slskd_abc"
assert rec.progress[-1] == 100.0
assert "downloading" in rec.statuses
def test_the_orchestrator_is_told_the_folder_is_the_clients_so_it_copies_not_moves(
self, handler, cfg, tmp_path
):
fake = FakeSlskd(tmp_path)
result, task, _ = _run(handler, fake)
assert task.original_download_path == result
def test_transfers_are_read_by_id_never_by_the_list(self, handler, cfg, tmp_path):
fake = FakeSlskd(tmp_path, script={FILE_A: ["InProgress", OK], FILE_B: ["InProgress", OK]})
_run(handler, fake)
assert fake.list_calls == 0
def test_a_failure_to_name_a_safe_folder_never_escapes_the_downloads_root(
self, handler, cfg, tmp_path
):
fake = FakeSlskd(tmp_path)
_run(handler, fake, task=_task(task_id="../../etc/passwd"))
destination = fake.batches[0]["destination"]
assert ".." not in destination and not destination.startswith("/")
assert destination.startswith("shelfmark/")
def test_the_destination_prefix_is_a_setting_and_a_bad_one_is_ignored(
self, handler, cfg, tmp_path
):
cfg["SLSKD_DESTINATION_PREFIX"] = "books/incoming"
fake = FakeSlskd(tmp_path)
_run(handler, fake)
assert fake.batches[0]["destination"] == "books/incoming/slskd_abc"
cfg["SLSKD_DESTINATION_PREFIX"] = "../../elsewhere"
fake2 = FakeSlskd(tmp_path)
_run(handler, fake2, task=_task(task_id="slskd:def"))
assert fake2.batches[0]["destination"] == "shelfmark/slskd_def"
class TestAnotherAppClearsSlskdsList:
def test_a_transfer_slskd_no_longer_lists_is_confirmed_by_the_files_on_disk(
self, handler, cfg, tmp_path
):
# Both files arrive, then SoulSync (say) clears the finished transfers.
fake = FakeSlskd(tmp_path, script={FILE_A: [OK, None], FILE_B: [OK, None]})
result, _, rec = _run(handler, fake)
assert result == str(tmp_path / "shelfmark" / "slskd_abc")
assert "error" not in rec.statuses
def test_a_file_that_arrived_before_the_clear_is_not_lost(self, handler, cfg, tmp_path):
folder = tmp_path / "shelfmark" / "slskd_abc"
def write_a_then_clear(fake, filename, state):
if filename == FILE_A:
folder.mkdir(parents=True, exist_ok=True)
(folder / "Book One.epub").write_bytes(b"x" * 5)
fake = FakeSlskd(tmp_path, script={FILE_A: [None], FILE_B: ["InProgress", OK]})
fake.on_get = write_a_then_clear
result, _, rec = _run(handler, fake)
assert result == str(folder)
assert "error" not in rec.statuses
def test_a_transfer_that_vanishes_with_no_file_is_an_error_not_a_hang(
self, handler, cfg, tmp_path
):
fake = FakeSlskd(tmp_path, script={FILE_A: [None], FILE_B: [None]}, write_on_success=False)
result, task, rec = _run(handler, fake)
assert result is None
assert rec.real[-1][0] == "error"
assert "slskd" in (rec.real[-1][1] or "").lower()
assert task.original_download_path is None
def test_slskds_events_confirm_a_vanished_transfer_whose_file_is_still_arriving(
self, handler, cfg, tmp_path
):
folder = tmp_path / "shelfmark" / "slskd_abc"
fake = FakeSlskd(tmp_path, script={FILE_A: [None], FILE_B: [None]}, write_on_success=False)
fake.events = [
{"type": "DownloadFileComplete", "data": json.dumps({"remoteFilename": FILE_A})},
{"type": "DownloadFileComplete", "data": json.dumps({"remoteFilename": FILE_B})},
]
# The files land a moment after slskd reported the events.
calls = {"n": 0}
real_get_events = fake.get_events
def events_then_files(**kw):
folder.mkdir(parents=True, exist_ok=True)
(folder / "Book One.epub").write_bytes(b"x" * 5)
(folder / "Book Two.epub").write_bytes(b"x" * 7)
calls["n"] += 1
return real_get_events(**kw)
fake.get_events = events_then_files # type: ignore[method-assign]
result, _, rec = _run(handler, fake)
assert calls["n"] >= 1
assert result == str(folder)
assert "error" not in rec.statuses
class TestFailuresAndCancel:
def test_a_failed_transfer_fails_the_task_and_stops_the_rest(self, handler, cfg, tmp_path):
fake = FakeSlskd(tmp_path, script={FILE_A: ["Completed, Errored"], FILE_B: ["InProgress"]})
result, task, rec = _run(handler, fake)
assert result is None
assert rec.real[-1][0] == "error"
assert "Errored" in (rec.real[-1][1] or "")
assert {tid for _, tid, _ in fake.cancelled} == {"t0", "t1"}
assert task.original_download_path is None
def test_cancelling_cancels_and_removes_every_transfer(self, handler, cfg, tmp_path):
cancel = Event()
fake = FakeSlskd(tmp_path, script={FILE_A: ["InProgress"], FILE_B: ["InProgress"]})
fake.on_get = lambda f, name, state: cancel.set()
result, _, rec = _run(handler, fake, cancel=cancel)
assert result is None
assert rec.real[-1][0] == "cancelled"
assert {(tid, remove) for _, tid, remove in fake.cancelled} == {("t0", True), ("t1", True)}
def test_a_peer_that_never_starts_gives_up_after_the_queue_timeout(
self, handler, cfg, tmp_path, monkeypatch
):
monkeypatch.setattr(SlskdHandler, "_queue_timeout_seconds", lambda self: 600.0)
clock = iter([0.0, 1.0, 2.0, 700.0, 701.0, 702.0, 703.0, 704.0, 705.0])
monkeypatch.setattr(SlskdHandler, "_clock", lambda self: next(clock), raising=False)
fake = FakeSlskd(
tmp_path,
script={FILE_A: ["Queued, Remotely"], FILE_B: ["Queued, Remotely"]},
write_on_success=False,
)
result, _, rec = _run(handler, fake)
assert result is None
assert "minutes" in (rec.real[-1][1] or "")
assert fake.cancelled
class TestPeerQueue:
def test_a_queued_transfer_asks_the_orchestrator_not_to_stall_it(self, handler, cfg, tmp_path):
fake = FakeSlskd(
tmp_path,
script={
FILE_A: ["Queued, Remotely", "Queued, Remotely", OK],
FILE_B: ["Queued, Remotely", "Queued, Remotely", OK],
},
)
_, _, rec = _run(handler, fake)
assert rec.graces, "no activity grace was requested while the peer queued the files"
assert rec.graces[0] == mod.QUEUE_GRACE_SECONDS
# and it is released once the transfers start, so the normal stall timer applies
assert rec.graces[-1] == 0.0
class TestRejoinAfterARestart:
def test_files_already_on_disk_are_not_downloaded_again(self, handler, cfg, tmp_path):
folder = tmp_path / "shelfmark" / "slskd_abc"
folder.mkdir(parents=True)
(folder / "Book One.epub").write_bytes(b"x" * 5)
(folder / "Book Two.epub").write_bytes(b"x" * 7)
fake = FakeSlskd(tmp_path)
result, task, rec = _run(handler, fake)
assert result == str(folder)
assert fake.batches == []
assert task.original_download_path == str(folder)
assert rec.progress[-1] == 100.0
def test_files_slskd_says_are_already_queued_are_found_by_name_and_rejoined(
self, handler, cfg, tmp_path
):
fake = FakeSlskd(tmp_path, script={FILE_A: [OK], FILE_B: [OK]})
fake.refuse = [{"filename": FILE_A, "message": "already queued"}]
fake._ids["t0"] = FILE_A # slskd still lists the earlier transfer for it
result, _, rec = _run(handler, fake)
assert result is not None
assert "error" not in rec.statuses
def test_a_refusal_with_nothing_to_rejoin_is_reported(self, handler, cfg, tmp_path):
fake = FakeSlskd(tmp_path)
fake.refuse = [
{"filename": FILE_A, "message": "denied"},
{"filename": FILE_B, "message": "denied"},
]
result, _, rec = _run(handler, fake)
assert result is None
assert rec.real[-1][0] == "error"
assert "denied" in (rec.real[-1][1] or "")
class TestARetryAfterAPartialDownload:
"""A retry reuses the task's folder, and slskd renames a file that is already there, so
files from an attempt that did not finish must not stay behind."""
def test_a_failed_attempt_leaves_no_files_in_the_folder(self, handler, cfg, tmp_path):
fake = FakeSlskd(tmp_path, script={FILE_A: [OK], FILE_B: ["Completed, Errored"]})
result, _, rec = _run(handler, fake)
assert result is None
assert rec.real[-1][0] == "error"
assert not (tmp_path / "shelfmark" / "slskd_abc").exists()
def test_a_cancelled_attempt_leaves_no_files_in_the_folder(self, handler, cfg, tmp_path):
cancel = Event()
fake = FakeSlskd(tmp_path, script={FILE_A: [OK], FILE_B: ["InProgress"]})
fake.on_get = lambda f, name, state: cancel.set() if name == FILE_B else None
result, _, rec = _run(handler, fake, cancel=cancel)
assert result is None
assert rec.real[-1][0] == "cancelled"
assert not (tmp_path / "shelfmark" / "slskd_abc").exists()
def test_the_retry_of_a_failed_attempt_completes(self, handler, cfg, tmp_path):
first = FakeSlskd(tmp_path, script={FILE_A: [OK], FILE_B: ["Completed, Errored"]})
assert _run(handler, first)[0] is None
result, _, rec = _run(handler, FakeSlskd(tmp_path))
folder = tmp_path / "shelfmark" / "slskd_abc"
assert result == str(folder)
assert "error" not in rec.statuses
assert sorted(p.name for p in folder.iterdir()) == ["Book One.epub", "Book Two.epub"]
def test_files_left_by_an_attempt_that_never_cleaned_up_are_replaced(
self, handler, cfg, tmp_path
):
# Shelfmark restarted after one of the two files had arrived.
folder = tmp_path / "shelfmark" / "slskd_abc"
folder.mkdir(parents=True)
(folder / "Book One.epub").write_bytes(b"x" * 5)
result, _, rec = _run(handler, FakeSlskd(tmp_path))
assert result == str(folder)
assert "error" not in rec.statuses
assert sorted(p.name for p in folder.iterdir()) == ["Book One.epub", "Book Two.epub"]
class TestUnsafeFileNames:
def test_an_unsafe_peer_file_name_is_refused_before_anything_is_queued(
self, handler, cfg, tmp_path
):
fake = FakeSlskd(tmp_path)
task = _task(files=[{"filename": "../../../etc//passwd.epub", "size": 5}])
result, _, rec = _run(handler, fake, task=task)
assert result is None
assert rec.real[-1][0] == "error"
assert "unsafe" in (rec.real[-1][1] or "")
assert fake.batches == []
class TestFallbackAndSettings:
def test_an_old_slskd_without_batches_uses_the_original_enqueue(self, handler, cfg, tmp_path):
class Old(FakeSlskd):
def __init__(self, root):
super().__init__(root)
self.batch_error = SlskdBatchUnsupportedError("no batches")
self.enqueued: list = []
def enqueue_downloads(self, username, files):
self.enqueued.append(files)
return [
{"id": f"o{i}", "filename": f["filename"], "state": "Queued, Locally"}
for i, f in enumerate(files)
]
fake = Old(tmp_path)
# the original path polls the list, so give it finished transfers and the files
fake.list_downloads = lambda username: [ # type: ignore[method-assign]
{
"id": f"o{i}",
"filename": f["filename"],
"state": OK,
"bytesTransferred": f["size"],
"size": f["size"],
"removed": False,
}
for i, f in enumerate(FILES)
]
for name in ("Book One.epub", "Book Two.epub"):
target = tmp_path / "Author" / name
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(b"x" * 5)
result, _, rec = _run(handler, fake)
assert fake.enqueued, "the original enqueue was not used"
assert result is not None
assert "error" not in rec.statuses
def test_isolation_can_be_switched_off(self, handler, cfg, tmp_path):
cfg["SLSKD_ISOLATE_DOWNLOADS"] = False
class NoBatch(FakeSlskd):
def enqueue_batch(self, *a, **kw):
raise AssertionError("the batch endpoint was used with isolation off")
def enqueue_downloads(self, username, files):
return []
fake = NoBatch(tmp_path)
fake.list_downloads = lambda username: [] # type: ignore[method-assign]
result, _, rec = _run(handler, fake)
assert result is None # the original path found nothing to wait for
assert rec.real[-1][0] == "error"
class TestCleanup:
def _done(self, handler, cfg, tmp_path):
fake = FakeSlskd(tmp_path)
result, task, _ = _run(handler, fake)
return fake, result, task
def test_by_default_finished_files_and_records_are_left_in_slskd_so_they_stay_shared(
self, handler, cfg, tmp_path
):
fake, _result, task = self._done(handler, cfg, tmp_path)
handler.post_process_cleanup(task, success=True)
assert fake.cancelled == []
assert (tmp_path / "shelfmark" / "slskd_abc").is_dir()
def test_with_keep_off_the_transfers_and_the_folder_are_removed_after_import(
self, handler, cfg, tmp_path
):
cfg["SLSKD_KEEP_COMPLETED"] = False
fake, _result, task = self._done(handler, cfg, tmp_path)
handler.post_process_cleanup(task, success=True)
assert {(tid, remove) for _, tid, remove in fake.cancelled} == {("t0", True), ("t1", True)}
assert not (tmp_path / "shelfmark" / "slskd_abc").exists()
def test_a_failed_import_leaves_everything_for_a_retry(self, handler, cfg, tmp_path):
cfg["SLSKD_KEEP_COMPLETED"] = False
fake, _result, task = self._done(handler, cfg, tmp_path)
handler.post_process_cleanup(task, success=False)
assert fake.cancelled == []
assert (tmp_path / "shelfmark" / "slskd_abc").is_dir()
class TestFolderComplete:
def _folder(self, tmp_path, files):
folder = tmp_path / "f"
folder.mkdir()
for name, size in files.items():
(folder / name).write_bytes(b"x" * size)
return folder
def test_the_files_asked_for_are_complete(self, tmp_path):
folder = self._folder(tmp_path, {"a.epub": 5, "b.epub": 7})
assert mod._folder_complete(folder, [5, 7]) is True
def test_a_file_of_the_wrong_size_is_not_complete(self, tmp_path):
# the right number of files, but one is a partial copy
folder = self._folder(tmp_path, {"a.epub": 5, "b.epub": 3})
assert mod._folder_complete(folder, [5, 7]) is False
def test_names_do_not_matter_because_slskd_may_rename_a_duplicate(self, tmp_path):
folder = self._folder(tmp_path, {"a_1.epub": 5, "totally different.epub": 7})
assert mod._folder_complete(folder, [7, 5]) is True
def test_too_few_or_too_many_files_is_not_complete(self, tmp_path):
assert mod._folder_complete(self._folder(tmp_path, {"a.epub": 5}), [5, 7]) is False
other = tmp_path / "g"
other.mkdir()
for name, size in {"a": 5, "b": 7, "c": 1}.items():
(other / name).write_bytes(b"x" * size)
assert mod._folder_complete(other, [5, 7]) is False
def test_hidden_and_missing_folders_hold_nothing(self, tmp_path):
folder = self._folder(tmp_path, {".part": 5})
assert mod._folder_complete(folder, [5]) is False
assert mod._folder_complete(tmp_path / "nope", [5]) is False
def test_an_unknown_size_is_checked_by_count_only(self, tmp_path):
folder = self._folder(tmp_path, {"a.epub": 5, "b.epub": 9})
assert mod._folder_complete(folder, [0, 0]) is True
assert mod._folder_complete(folder, [0]) is False
def test_nothing_asked_for_is_never_complete(self, tmp_path):
assert mod._folder_complete(self._folder(tmp_path, {"a": 1}), []) is False