Discord (Shdjfgatdif, standalone): some downloads complete on disk but get marked failed with
"File not found on disk after 5 search attempts. Expected: <basename>" — which tells the user
nothing about where we looked or what to check.
This is deliberately a DIAGNOSTIC fix, not a behavior change. The finder + path handling are sound
(verified: docker_resolve_path no-ops in standalone, the finder walks the configured
soulseek.download_path and resolves a present file). When it still misses after slskd reported the
transfer Succeeded, the cause is environmental — either the file is still landing (timing) or, the
classic standalone gotcha, SoulSync's download_path doesn't point at slskd's actual download dir.
Neither is something our code can "fix"; the user fixes the config, or the file arrives.
So: name the folder we actually searched and spell out the two real causes, turning an opaque
failure into self-diagnosis ("oh, my download folder's wrong"). Retry/wait behavior is left
untouched on purpose — widening the window does nothing for a path mismatch and I can't justify it
for this user. Also normalizes the slskd backslash path so the reported filename is the leaf, not
the whole "@@@user\folder\file" string.
Updated the existing not-found test to pin the new actionable message (searched path + config
hint + filename). 588 downloads tests green, ruff clean.
530 lines
20 KiB
Python
530 lines
20 KiB
Python
"""Tests for core/downloads/post_processing.py — verification worker for completed downloads.
|
|
|
|
The worker is large + side-effecty. Tests cover the major control-flow
|
|
branches: missing task, cancelled, already-completed, missing
|
|
filename/username, file-found-in-transfer with + without metadata, file-
|
|
found-in-downloads with + without context, file-not-found-after-retries,
|
|
youtube special path, and top-level exception swallow.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import pytest
|
|
|
|
from core.downloads import post_processing as pp
|
|
from core.runtime_state import (
|
|
download_tasks,
|
|
matched_context_lock,
|
|
matched_downloads_context,
|
|
tasks_lock,
|
|
)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def reset_state():
|
|
download_tasks.clear()
|
|
matched_downloads_context.clear()
|
|
yield
|
|
download_tasks.clear()
|
|
matched_downloads_context.clear()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fakes
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class _Recorder:
|
|
"""Captures every call into a list of (name, args, kwargs)."""
|
|
def __init__(self):
|
|
self.calls = []
|
|
|
|
def __call__(self, name):
|
|
def _inner(*args, **kwargs):
|
|
self.calls.append((name, args, kwargs))
|
|
return None
|
|
return _inner
|
|
|
|
|
|
def _build_deps(
|
|
*,
|
|
config=None,
|
|
download_orchestrator=None,
|
|
run_async=None,
|
|
docker_resolve_path=None,
|
|
extract_filename=None,
|
|
make_context_key=None,
|
|
find_completed_file=None,
|
|
enhance_file_metadata=None,
|
|
wipe_source_tags=None,
|
|
post_process_with_verification=None,
|
|
mark_task_completed=None,
|
|
on_download_completed=None,
|
|
):
|
|
rec = _Recorder()
|
|
return pp.PostProcessDeps(
|
|
config_manager=config or _FakeConfig(),
|
|
download_orchestrator=download_orchestrator,
|
|
run_async=run_async or (lambda c: None),
|
|
docker_resolve_path=docker_resolve_path or (lambda p: p),
|
|
extract_filename=extract_filename or (lambda f: os.path.basename(f) if f else ''),
|
|
make_context_key=make_context_key or (lambda u, f: f"{u}::{f}"),
|
|
find_completed_file=find_completed_file or (lambda *a, **kw: (None, None)),
|
|
enhance_file_metadata=enhance_file_metadata or rec('enhance'),
|
|
wipe_source_tags=wipe_source_tags or rec('wipe'),
|
|
post_process_with_verification=post_process_with_verification or rec('post_process'),
|
|
mark_task_completed=mark_task_completed or rec('mark_completed'),
|
|
on_download_completed=on_download_completed or rec('on_complete'),
|
|
), rec
|
|
|
|
|
|
class _FakeConfig:
|
|
def __init__(self, values=None):
|
|
self._v = values or {}
|
|
|
|
def get(self, key, default=None):
|
|
return self._v.get(key, default)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Branch coverage tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_missing_task_returns_early_no_callbacks():
|
|
deps, rec = _build_deps()
|
|
pp.run_post_processing_worker('absent', 'b1', deps)
|
|
assert rec.calls == []
|
|
|
|
|
|
def test_cancelled_task_returns_early_no_callbacks():
|
|
download_tasks['t1'] = {'status': 'cancelled'}
|
|
deps, rec = _build_deps()
|
|
pp.run_post_processing_worker('t1', 'b1', deps)
|
|
assert rec.calls == []
|
|
|
|
|
|
def test_already_completed_task_returns_early():
|
|
download_tasks['t1'] = {'status': 'completed'}
|
|
deps, rec = _build_deps()
|
|
pp.run_post_processing_worker('t1', 'b1', deps)
|
|
assert rec.calls == []
|
|
|
|
|
|
def test_stream_processed_task_returns_early():
|
|
download_tasks['t1'] = {'status': 'post_processing', 'stream_processed': True}
|
|
deps, rec = _build_deps()
|
|
pp.run_post_processing_worker('t1', 'b1', deps)
|
|
assert rec.calls == []
|
|
|
|
|
|
def test_requeued_task_bails_without_marking_failed():
|
|
"""RACE GUARD: the monitor sets status -> 'post_processing' and submits this
|
|
worker. If, before the worker runs, the browser-poll post-processor
|
|
quarantines the file and requeues the next-best candidate (status ->
|
|
'searching', username/filename cleared), this worker must bail WITHOUT
|
|
marking failed or notifying batch completion. Otherwise it clobbers the
|
|
in-flight retry with a false 'missing file or source information' failure
|
|
while a parallel attempt imports the song."""
|
|
download_tasks['t1'] = {'status': 'searching', 'track_info': {}}
|
|
deps, rec = _build_deps()
|
|
pp.run_post_processing_worker('t1', 'b1', deps)
|
|
assert download_tasks['t1']['status'] == 'searching' # untouched
|
|
assert 'error_message' not in download_tasks['t1']
|
|
assert not any(c[0] == 'on_complete' for c in rec.calls)
|
|
|
|
|
|
def test_queued_task_bails_without_marking_failed():
|
|
"""Same race guard for a task another path reset to 'queued'."""
|
|
download_tasks['t1'] = {'status': 'queued', 'track_info': {}}
|
|
deps, rec = _build_deps()
|
|
pp.run_post_processing_worker('t1', 'b1', deps)
|
|
assert download_tasks['t1']['status'] == 'queued'
|
|
assert not any(c[0] == 'on_complete' for c in rec.calls)
|
|
|
|
|
|
def test_missing_filename_marks_failed_and_calls_on_complete():
|
|
download_tasks['t1'] = {'status': 'post_processing', 'username': 'u1', 'track_info': {}}
|
|
deps, rec = _build_deps()
|
|
pp.run_post_processing_worker('t1', 'b1', deps)
|
|
assert download_tasks['t1']['status'] == 'failed'
|
|
assert 'Post-processing failed' in download_tasks['t1']['error_message']
|
|
assert ('on_complete', ('b1', 't1', False), {}) in rec.calls
|
|
|
|
|
|
def test_missing_username_marks_failed_and_calls_on_complete():
|
|
download_tasks['t1'] = {'status': 'post_processing', 'filename': 'song.flac', 'track_info': {}}
|
|
deps, rec = _build_deps()
|
|
pp.run_post_processing_worker('t1', 'b1', deps)
|
|
assert download_tasks['t1']['status'] == 'failed'
|
|
|
|
|
|
def test_file_not_found_after_retries_marks_failed(monkeypatch):
|
|
download_tasks['t1'] = {
|
|
'status': 'post_processing',
|
|
'filename': 'song.flac',
|
|
'username': 'u1',
|
|
'track_info': {},
|
|
}
|
|
# Skip sleeps to keep test fast
|
|
monkeypatch.setattr(pp.time, 'sleep', lambda s: None)
|
|
deps, rec = _build_deps()
|
|
pp.run_post_processing_worker('t1', 'b1', deps)
|
|
assert download_tasks['t1']['status'] == 'failed'
|
|
# Actionable failure: names the folder searched + the two real causes, so a
|
|
# standalone user with a path mismatch can self-diagnose (Discord: Shdjfgatdif).
|
|
msg = download_tasks['t1']['error_message']
|
|
assert './downloads' in msg # the folder we actually searched
|
|
assert "download path doesn't match slskd" in msg # the config-mismatch hint
|
|
assert 'song.flac' in msg # the file slskd reported
|
|
assert ('on_complete', ('b1', 't1', False), {}) in rec.calls
|
|
|
|
|
|
def test_stream_processor_completes_during_search_loop_returns_no_failure(monkeypatch):
|
|
"""If task gets marked completed by stream processor mid-retry, abort without failing."""
|
|
download_tasks['t1'] = {
|
|
'status': 'post_processing',
|
|
'filename': 'song.flac',
|
|
'username': 'u1',
|
|
'track_info': {},
|
|
}
|
|
monkeypatch.setattr(pp.time, 'sleep', lambda s: None)
|
|
call_count = [0]
|
|
|
|
def _stream_completes_after_first_search(*a, **kw):
|
|
call_count[0] += 1
|
|
if call_count[0] >= 1:
|
|
download_tasks['t1']['stream_processed'] = True
|
|
return (None, None)
|
|
|
|
deps, rec = _build_deps(find_completed_file=_stream_completes_after_first_search)
|
|
pp.run_post_processing_worker('t1', 'b1', deps)
|
|
# Worker should detect stream_processed, return early, not mark failed
|
|
assert download_tasks['t1']['status'] == 'post_processing' # original status preserved
|
|
assert ('on_complete', ('b1', 't1', False), {}) not in rec.calls
|
|
|
|
|
|
def test_file_found_in_transfer_with_metadata_enhanced_skips_enhancement_and_completes():
|
|
download_tasks['t1'] = {
|
|
'status': 'post_processing',
|
|
'filename': 'song.flac',
|
|
'username': 'u1',
|
|
'track_info': {'name': 'Money'},
|
|
'metadata_enhanced': True,
|
|
}
|
|
deps, rec = _build_deps(
|
|
find_completed_file=lambda *a, **kw: ('/transfer/song.flac', 'transfer'),
|
|
)
|
|
pp.run_post_processing_worker('t1', 'b1', deps)
|
|
# No enhance call because metadata_enhanced=True
|
|
assert not any(c[0] == 'enhance' for c in rec.calls)
|
|
# Mark + on-complete called
|
|
assert any(c[0] == 'mark_completed' for c in rec.calls)
|
|
assert ('on_complete', ('b1', 't1', True), {}) in rec.calls
|
|
|
|
|
|
def test_file_found_in_transfer_no_context_no_filename_wipes_tags(monkeypatch):
|
|
"""Transfer file but missing context AND expected filename -> wipe tags only."""
|
|
download_tasks['t1'] = {
|
|
'status': 'post_processing',
|
|
'filename': 'song.flac',
|
|
'username': 'u1',
|
|
'track_info': {},
|
|
'metadata_enhanced': False,
|
|
}
|
|
monkeypatch.setattr(pp.os.path, 'exists', lambda p: True)
|
|
deps, rec = _build_deps(
|
|
find_completed_file=lambda *a, **kw: ('/transfer/song.flac', 'transfer'),
|
|
)
|
|
pp.run_post_processing_worker('t1', 'b1', deps)
|
|
# wipe_source_tags called (no full enhancement possible)
|
|
assert any(c[0] == 'wipe' for c in rec.calls)
|
|
# Still completed
|
|
assert ('on_complete', ('b1', 't1', True), {}) in rec.calls
|
|
|
|
|
|
def test_file_found_in_downloads_with_context_runs_post_process_with_verification():
|
|
download_tasks['t1'] = {
|
|
'status': 'post_processing',
|
|
'filename': 'song.flac',
|
|
'username': 'u1',
|
|
'track_info': {'name': 'Money'},
|
|
}
|
|
matched_downloads_context['u1::song.flac'] = {
|
|
'original_search_result': {'title': 'Money', 'track_number': 1},
|
|
'context_artist': {'name': 'Pink Floyd', 'id': 'art1'},
|
|
'context_album': {'name': 'DSOTM'},
|
|
}
|
|
deps, rec = _build_deps(
|
|
find_completed_file=lambda *a, **kw: ('/downloads/song.flac', 'download'),
|
|
)
|
|
pp.run_post_processing_worker('t1', 'b1', deps)
|
|
# post_process_with_verification called with the context + file
|
|
assert any(c[0] == 'post_process' for c in rec.calls)
|
|
|
|
|
|
def test_file_search_ignores_non_audio_candidates(monkeypatch):
|
|
download_tasks['t1'] = {
|
|
'status': 'post_processing',
|
|
'filename': 'Artist - Album.cue',
|
|
'username': 'torrent',
|
|
'track_info': {'name': 'Money'},
|
|
}
|
|
matched_downloads_context['torrent::Artist - Album.cue'] = {
|
|
'original_search_result': {'title': 'Money', 'track_number': 1},
|
|
}
|
|
monkeypatch.setattr(pp.time, 'sleep', lambda s: None)
|
|
deps, rec = _build_deps(
|
|
find_completed_file=lambda *a, **kw: ('/downloads/Artist - Album.cue', 'download'),
|
|
)
|
|
|
|
pp.run_post_processing_worker('t1', 'b1', deps)
|
|
|
|
assert download_tasks['t1']['status'] == 'failed'
|
|
assert not any(c[0] == 'post_process' for c in rec.calls)
|
|
assert ('on_complete', ('b1', 't1', False), {}) in rec.calls
|
|
|
|
|
|
def test_file_found_in_downloads_no_context_marks_completed_directly():
|
|
"""No matched context for the file → just mark completed since file exists."""
|
|
download_tasks['t1'] = {
|
|
'status': 'post_processing',
|
|
'filename': 'song.flac',
|
|
'username': 'u1',
|
|
'track_info': {'name': 'Money'},
|
|
}
|
|
deps, rec = _build_deps(
|
|
find_completed_file=lambda *a, **kw: ('/downloads/song.flac', 'download'),
|
|
)
|
|
pp.run_post_processing_worker('t1', 'b1', deps)
|
|
# No post_process call (no context)
|
|
assert not any(c[0] == 'post_process' for c in rec.calls)
|
|
# Mark + on-complete called
|
|
assert any(c[0] == 'mark_completed' for c in rec.calls)
|
|
assert ('on_complete', ('b1', 't1', True), {}) in rec.calls
|
|
|
|
|
|
def test_processing_exception_marks_failed_and_calls_on_complete():
|
|
download_tasks['t1'] = {
|
|
'status': 'post_processing',
|
|
'filename': 'song.flac',
|
|
'username': 'u1',
|
|
'track_info': {'name': 'Money'},
|
|
}
|
|
matched_downloads_context['u1::song.flac'] = {'original_search_result': {}}
|
|
|
|
def _exploding_post_process(*a, **kw):
|
|
raise RuntimeError("post-process boom")
|
|
|
|
deps, rec = _build_deps(
|
|
find_completed_file=lambda *a, **kw: ('/downloads/song.flac', 'download'),
|
|
post_process_with_verification=_exploding_post_process,
|
|
)
|
|
pp.run_post_processing_worker('t1', 'b1', deps)
|
|
assert download_tasks['t1']['status'] == 'failed'
|
|
assert 'Post-processing failed' in download_tasks['t1']['error_message']
|
|
assert ('on_complete', ('b1', 't1', False), {}) in rec.calls
|
|
|
|
|
|
def test_critical_outer_exception_marks_failed():
|
|
"""Top-level exception (e.g. broken deps) still marks task failed."""
|
|
download_tasks['t1'] = {
|
|
'status': 'post_processing',
|
|
'filename': 'song.flac',
|
|
'username': 'u1',
|
|
'track_info': {},
|
|
}
|
|
|
|
def _broken_resolve(p):
|
|
raise RuntimeError("config dead")
|
|
|
|
deps, rec = _build_deps(docker_resolve_path=_broken_resolve)
|
|
# Must NOT raise
|
|
pp.run_post_processing_worker('t1', 'b1', deps)
|
|
assert download_tasks['t1']['status'] == 'failed'
|
|
assert 'Critical post-processing error' in download_tasks['t1']['error_message']
|
|
assert ('on_complete', ('b1', 't1', False), {}) in rec.calls
|
|
|
|
|
|
def test_youtube_task_uses_get_download_status_to_resolve_path(monkeypatch):
|
|
"""YouTube downloads use a different filename scheme — worker queries soulseek client for real path."""
|
|
download_tasks['t1'] = {
|
|
'status': 'post_processing',
|
|
'filename': 'vid_id||Money',
|
|
'username': 'youtube',
|
|
'download_id': 'dl-yt-1',
|
|
'track_info': {},
|
|
}
|
|
|
|
class _FakeStatus:
|
|
file_path = '/downloads/Money.mp3'
|
|
|
|
class _FakeYTClient:
|
|
def get_download_status(self, dl_id):
|
|
assert dl_id == 'dl-yt-1'
|
|
return _FakeStatus()
|
|
|
|
# File exists on disk (mock)
|
|
monkeypatch.setattr(pp.os.path, 'exists', lambda p: p == '/downloads/Money.mp3')
|
|
|
|
deps, rec = _build_deps(
|
|
download_orchestrator=_FakeYTClient(),
|
|
run_async=lambda coro: coro, # not async — direct call
|
|
)
|
|
pp.run_post_processing_worker('t1', 'b1', deps)
|
|
# mark_completed should fire (file resolved from YouTube status)
|
|
assert any(c[0] == 'mark_completed' for c in rec.calls)
|
|
|
|
|
|
def test_torrent_release_copies_best_matching_audio_to_transfer(tmp_path):
|
|
release_dir = tmp_path / 'release'
|
|
release_dir.mkdir()
|
|
wrong = release_dir / '01 - Intro.flac'
|
|
right = release_dir / '02 - Money.flac'
|
|
wrong.write_bytes(b'wrong')
|
|
right.write_bytes(b'right')
|
|
transfer_dir = tmp_path / 'transfer'
|
|
|
|
filename = 'magnet:?xt=abc||Artist - Album'
|
|
download_tasks['t1'] = {
|
|
'status': 'post_processing',
|
|
'filename': filename,
|
|
'username': 'torrent',
|
|
'download_id': 'dl-torrent-1',
|
|
'track_info': {'name': 'Money', 'artists': [{'name': 'Artist'}]},
|
|
}
|
|
matched_downloads_context[f'torrent::{filename}'] = {
|
|
'original_search_result': {'title': 'Money', 'track_number': 2},
|
|
}
|
|
|
|
class _FakeStatus:
|
|
file_path = str(wrong)
|
|
audio_files = [str(wrong), str(right)]
|
|
|
|
class _FakeTorrentClient:
|
|
def get_download_status(self, dl_id):
|
|
assert dl_id == 'dl-torrent-1'
|
|
return _FakeStatus()
|
|
|
|
deps, rec = _build_deps(
|
|
config=_FakeConfig({'soulseek.transfer_path': str(transfer_dir)}),
|
|
download_orchestrator=_FakeTorrentClient(),
|
|
run_async=lambda coro: coro,
|
|
)
|
|
|
|
pp.run_post_processing_worker('t1', 'b1', deps)
|
|
|
|
copied = transfer_dir / '02 - Money.flac'
|
|
assert copied.exists()
|
|
assert right.exists()
|
|
assert any(c[0] == 'post_process' and c[1][2] == str(copied) for c in rec.calls)
|
|
|
|
|
|
def test_torrent_release_prefers_task_title_over_release_context(tmp_path):
|
|
release_dir = tmp_path / 'release'
|
|
release_dir.mkdir()
|
|
wrong = release_dir / '09.Harry Styles - Pop.flac'
|
|
right = release_dir / '10.Harry Styles - American Girls.flac'
|
|
wrong.write_bytes(b'wrong')
|
|
right.write_bytes(b'right')
|
|
transfer_dir = tmp_path / 'transfer'
|
|
|
|
filename = 'http://prowlarr/download?id=1||Harry Styles - Kiss All The Time'
|
|
download_tasks['t1'] = {
|
|
'status': 'post_processing',
|
|
'filename': filename,
|
|
'username': 'torrent',
|
|
'download_id': 'dl-torrent-1',
|
|
'track_info': {'name': 'American Girls', 'artists': [{'name': 'Harry Styles'}]},
|
|
}
|
|
matched_downloads_context[f'torrent::{filename}'] = {
|
|
'original_search_result': {'title': 'Pop', 'clean_title': 'Pop', 'track_number': 9},
|
|
}
|
|
|
|
class _FakeStatus:
|
|
file_path = str(wrong)
|
|
audio_files = [str(wrong), str(right)]
|
|
|
|
class _FakeTorrentClient:
|
|
def get_download_status(self, dl_id):
|
|
assert dl_id == 'dl-torrent-1'
|
|
return _FakeStatus()
|
|
|
|
deps, rec = _build_deps(
|
|
config=_FakeConfig({'soulseek.transfer_path': str(transfer_dir)}),
|
|
download_orchestrator=_FakeTorrentClient(),
|
|
run_async=lambda coro: coro,
|
|
)
|
|
|
|
pp.run_post_processing_worker('t1', 'b1', deps)
|
|
|
|
copied = transfer_dir / '10.Harry Styles - American Girls.flac'
|
|
assert copied.exists()
|
|
assert any(c[0] == 'post_process' and c[1][2] == str(copied) for c in rec.calls)
|
|
|
|
|
|
def test_torrent_release_without_matching_file_does_not_fallback_to_generic_search(tmp_path):
|
|
release_dir = tmp_path / 'release'
|
|
release_dir.mkdir()
|
|
wrong = release_dir / '09.Harry Styles - Pop.flac'
|
|
wrong.write_bytes(b'wrong')
|
|
transfer_dir = tmp_path / 'transfer'
|
|
|
|
filename = 'http://prowlarr/download?id=1||Harry Styles - Kiss All The Time'
|
|
download_tasks['t1'] = {
|
|
'status': 'post_processing',
|
|
'filename': filename,
|
|
'username': 'torrent',
|
|
'download_id': 'dl-torrent-1',
|
|
'track_info': {'name': 'American Girls', 'artists': [{'name': 'Harry Styles'}]},
|
|
}
|
|
matched_downloads_context[f'torrent::{filename}'] = {
|
|
'original_search_result': {'title': 'Pop', 'clean_title': 'Pop', 'track_number': 9},
|
|
}
|
|
|
|
class _FakeStatus:
|
|
file_path = str(wrong)
|
|
audio_files = [str(wrong)]
|
|
|
|
class _FakeTorrentClient:
|
|
def get_download_status(self, dl_id):
|
|
assert dl_id == 'dl-torrent-1'
|
|
return _FakeStatus()
|
|
|
|
def _unexpected_search(*args, **kwargs):
|
|
raise AssertionError("torrent releases should not fall back to generic file search")
|
|
|
|
deps, rec = _build_deps(
|
|
config=_FakeConfig({'soulseek.transfer_path': str(transfer_dir)}),
|
|
download_orchestrator=_FakeTorrentClient(),
|
|
run_async=lambda coro: coro,
|
|
find_completed_file=_unexpected_search,
|
|
)
|
|
|
|
pp.run_post_processing_worker('t1', 'b1', deps)
|
|
|
|
assert download_tasks['t1']['status'] == 'failed'
|
|
assert 'No matching audio file' in download_tasks['t1']['error_message']
|
|
assert any(c[0] == 'on_complete' and c[1] == ('b1', 't1', False) for c in rec.calls)
|
|
assert not list(transfer_dir.glob('*'))
|
|
|
|
|
|
def test_fuzzy_context_matching_when_exact_key_missing(monkeypatch):
|
|
"""When exact key isn't in matched_downloads_context, worker tries fuzzy match
|
|
constrained to same Soulseek username."""
|
|
download_tasks['t1'] = {
|
|
'status': 'post_processing',
|
|
'filename': 'song.flac',
|
|
'username': 'u1',
|
|
'track_info': {},
|
|
}
|
|
# Different exact key but same user + filename substring
|
|
matched_downloads_context['u1::folder/song.flac'] = {
|
|
'original_search_result': {'title': 'Money', 'track_number': 1},
|
|
}
|
|
deps, rec = _build_deps(
|
|
find_completed_file=lambda *a, **kw: (None, None), # file not found
|
|
)
|
|
monkeypatch.setattr(pp.time, 'sleep', lambda s: None)
|
|
# Won't find file → marks failed. But the fuzzy match log path executes.
|
|
pp.run_post_processing_worker('t1', 'b1', deps)
|
|
assert download_tasks['t1']['status'] == 'failed'
|