You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
SoulSync/tests/downloads/test_download_engine.py

725 lines
27 KiB

"""Tests for the DownloadEngine skeleton (Phase B).
Pinning the engine's state-storage contract: add/update/remove,
per-source iteration, find-by-id, plugin registration, lock-held
mutations vs lock-released reads. Future phases (C/D/E/F) bolt
behavior on top of this surface — these tests stay green and act
as the regression net while behavior moves in.
"""
from __future__ import annotations
import threading
import pytest
from core.download_engine import DownloadEngine
# ---------------------------------------------------------------------------
# Plugin registration
# ---------------------------------------------------------------------------
def test_register_plugin_stores_under_source_name():
engine = DownloadEngine()
plugin = object()
engine.register_plugin('soulseek', plugin)
assert engine.get_plugin('soulseek') is plugin
assert 'soulseek' in engine.registered_sources()
def test_get_plugin_returns_none_for_unknown_source():
engine = DownloadEngine()
assert engine.get_plugin('made_up') is None
def test_register_plugin_swallows_set_engine_failure(caplog):
"""Per JohnBaumb: if a plugin's ``set_engine`` callback raises,
registration shouldn't take down the engine — the failure gets
logged + the plugin stays registered. The plugin's own download()
method is responsible for surfacing the missing-engine state to
the user (see ``test_download_raises_when_engine_not_wired`` per
source). Pinning this so a future refactor can't accidentally
propagate the set_engine exception and crash boot.
"""
class _BrokenSetEngine:
def set_engine(self, engine):
raise RuntimeError("plugin's set_engine blew up")
engine = DownloadEngine()
plugin = _BrokenSetEngine()
# Should not raise.
engine.register_plugin('flaky', plugin)
# Plugin still registered + lookupable.
assert engine.get_plugin('flaky') is plugin
assert 'flaky' in engine.registered_sources()
def test_register_plugin_overwrites_on_duplicate(caplog):
"""Re-registering under the same name overwrites and warns. Not a
common path but useful so test fixtures that build a fresh engine
can swap a mock plugin in without setup gymnastics."""
engine = DownloadEngine()
first = object()
second = object()
engine.register_plugin('soulseek', first)
engine.register_plugin('soulseek', second)
assert engine.get_plugin('soulseek') is second
# ---------------------------------------------------------------------------
# Active-download state — add / get / update / remove
# ---------------------------------------------------------------------------
def test_add_record_inserts_under_composite_key():
engine = DownloadEngine()
engine.add_record('youtube', 'dl-1', {'state': 'Initializing', 'progress': 0.0})
rec = engine.get_record('youtube', 'dl-1')
assert rec is not None
assert rec['state'] == 'Initializing'
assert rec['progress'] == 0.0
def test_get_record_returns_shallow_copy():
"""Mutating the returned dict must NOT affect engine state.
Engine reads should be safe to hold / iterate without locks."""
engine = DownloadEngine()
engine.add_record('youtube', 'dl-1', {'state': 'Initializing'})
rec = engine.get_record('youtube', 'dl-1')
rec['state'] = 'TamperedByCaller'
# Engine state still has the original.
fresh = engine.get_record('youtube', 'dl-1')
assert fresh['state'] == 'Initializing'
def test_update_record_applies_partial_patch():
engine = DownloadEngine()
engine.add_record('tidal', 'dl-2', {'state': 'Initializing', 'progress': 0.0,
'file_path': None})
engine.update_record('tidal', 'dl-2', {'state': 'Completed, Succeeded',
'progress': 100.0,
'file_path': '/tmp/song.flac'})
rec = engine.get_record('tidal', 'dl-2')
assert rec['state'] == 'Completed, Succeeded'
assert rec['progress'] == 100.0
assert rec['file_path'] == '/tmp/song.flac'
def test_update_record_is_noop_when_record_removed():
"""If a record was removed (e.g. user cancelled mid-download),
the worker thread's late update is silently dropped — never
raises. Mirrors the per-client `if download_id in active_downloads`
guard pattern that's all over the existing clients."""
engine = DownloadEngine()
engine.add_record('tidal', 'dl-2', {'state': 'Initializing'})
engine.remove_record('tidal', 'dl-2')
# Should not raise.
engine.update_record('tidal', 'dl-2', {'state': 'Completed, Succeeded'})
assert engine.get_record('tidal', 'dl-2') is None
def test_remove_record_returns_removed_record():
engine = DownloadEngine()
engine.add_record('qobuz', 'dl-3', {'state': 'InProgress'})
removed = engine.remove_record('qobuz', 'dl-3')
assert removed is not None
assert removed['state'] == 'InProgress'
assert engine.get_record('qobuz', 'dl-3') is None
def test_remove_record_returns_none_when_missing():
engine = DownloadEngine()
assert engine.remove_record('qobuz', 'never-existed') is None
def test_per_source_locks_dont_block_each_other():
"""Per JohnBaumb: each source must have its own lock so a long-held
write on one source doesn't block writes on another. Pre-refactor
each client owned its own download lock; the engine has to match
that semantic.
Hold source-A's lock from one thread, then mutate source-B from
another thread. Source-B's mutation must complete promptly even
while source-A is locked.
"""
engine = DownloadEngine()
engine.add_record('youtube', 'yt-1', {'state': 'InProgress'})
engine.add_record('tidal', 'td-1', {'state': 'InProgress'})
held = threading.Event()
release = threading.Event()
other_done = threading.Event()
def hold_youtube_lock():
with engine._source_lock('youtube'):
held.set()
release.wait(timeout=2.0)
def update_tidal_while_youtube_held():
held.wait(timeout=1.0)
engine.update_record('tidal', 'td-1', {'state': 'Completed, Succeeded'})
other_done.set()
holder = threading.Thread(target=hold_youtube_lock)
other = threading.Thread(target=update_tidal_while_youtube_held)
holder.start()
other.start()
# Tidal write must complete even though YouTube's lock is held.
assert other_done.wait(timeout=1.0), (
"Tidal mutation blocked by YouTube's lock — sources are not "
"independently shardable"
)
release.set()
holder.join()
other.join()
assert engine.get_record('tidal', 'td-1')['state'] == 'Completed, Succeeded'
def test_remove_record_drops_empty_source_bucket():
"""Per JohnBaumb: nested layout makes per-source iteration
O(source_records). Removing the last record for a source must
also drop the empty source bucket so future iter_records_for_source
calls don't see a stale source key."""
engine = DownloadEngine()
engine.add_record('youtube', 'yt-1', {'title': 'A'})
engine.remove_record('youtube', 'yt-1')
# Source bucket gone — internal state matches the contract.
assert 'youtube' not in engine._records
# Public surface still answers correctly.
assert list(engine.iter_records_for_source('youtube')) == []
assert engine.get_record('youtube', 'yt-1') is None
# ---------------------------------------------------------------------------
# Iteration
# ---------------------------------------------------------------------------
def test_iter_records_for_source_filters_correctly():
engine = DownloadEngine()
engine.add_record('youtube', 'yt-1', {'title': 'A'})
engine.add_record('youtube', 'yt-2', {'title': 'B'})
engine.add_record('tidal', 'td-1', {'title': 'C'})
yt_records = list(engine.iter_records_for_source('youtube'))
assert len(yt_records) == 2
assert {r['title'] for r in yt_records} == {'A', 'B'}
td_records = list(engine.iter_records_for_source('tidal'))
assert len(td_records) == 1
assert td_records[0]['title'] == 'C'
def test_iter_yields_shallow_copies():
"""Iteration returns COPIES — caller can hold the list and mutate
each record without affecting engine state. Important: future
Phase B3's `get_all_downloads` will iterate then build
DownloadStatus objects from the snapshots."""
engine = DownloadEngine()
engine.add_record('youtube', 'yt-1', {'title': 'A'})
snapshot = list(engine.iter_records_for_source('youtube'))
snapshot[0]['title'] = 'TAMPERED'
fresh = engine.get_record('youtube', 'yt-1')
assert fresh['title'] == 'A'
# ---------------------------------------------------------------------------
# Thread safety — basic concurrent-mutation smoke
# ---------------------------------------------------------------------------
def test_concurrent_adds_dont_lose_records():
"""Hammer the engine with concurrent add_record from multiple
threads. With proper locking, every record lands in state.
Future Phase C BackgroundDownloadWorker spawns N threads doing
exactly this kind of mutation."""
engine = DownloadEngine()
def add_records(source, base):
for i in range(50):
engine.add_record(source, f'{base}-{i}', {'i': i})
threads = [
threading.Thread(target=add_records, args=(f'src-{n}', f'dl-{n}'))
for n in range(4)
]
for t in threads:
t.start()
for t in threads:
t.join()
total = sum(
1
for n in range(4)
for _ in engine.iter_records_for_source(f'src-{n}')
)
assert total == 4 * 50 # 200 records, none lost
# ---------------------------------------------------------------------------
# Cross-source query dispatch (Phase B2)
# ---------------------------------------------------------------------------
def _run_async(coro):
import asyncio
loop = asyncio.new_event_loop()
try:
return loop.run_until_complete(coro)
finally:
loop.close()
class _FakePlugin:
"""Minimal plugin double for engine query tests. Exposes the
methods engine.get_all_downloads / get_download_status /
cancel_download / clear_all_completed_downloads call."""
def __init__(self, name, configured=True, downloads=None,
cancel_result=True, clear_result=True):
self.name = name
self._configured = configured
self._downloads = downloads or []
self._cancel_result = cancel_result
self._clear_result = clear_result
self.cancel_calls = []
self.clear_calls = 0
def is_configured(self):
return self._configured
async def get_all_downloads(self):
return list(self._downloads)
async def get_download_status(self, download_id):
for d in self._downloads:
if getattr(d, 'id', None) == download_id:
return d
return None
async def cancel_download(self, download_id, source_hint, remove):
self.cancel_calls.append((download_id, source_hint, remove))
return self._cancel_result
async def clear_all_completed_downloads(self):
self.clear_calls += 1
return self._clear_result
class _FakeStatus:
def __init__(self, id, source):
self.id = id
self.source = source
def test_engine_get_all_downloads_aggregates_across_plugins():
"""Engine concatenates every plugin's get_all_downloads output —
same behavior as the legacy orchestrator."""
engine = DownloadEngine()
yt_plugin = _FakePlugin('youtube', downloads=[_FakeStatus('yt-1', 'youtube')])
td_plugin = _FakePlugin('tidal', downloads=[_FakeStatus('td-1', 'tidal'),
_FakeStatus('td-2', 'tidal')])
engine.register_plugin('youtube', yt_plugin)
engine.register_plugin('tidal', td_plugin)
result = _run_async(engine.get_all_downloads())
assert len(result) == 3
assert {r.id for r in result} == {'yt-1', 'td-1', 'td-2'}
def test_engine_get_all_downloads_excludes_dont_invoke_plugin():
"""Per JohnBaumb: the monitor calls engine.get_all_downloads(
exclude=('soulseek',)) AFTER already pulling slskd transfers via
the transfers/downloads endpoint. The whole point of the exclude
is to NOT touch soulseek's plugin a second time — so the plugin's
get_all_downloads must literally not be called when its name is
in the exclude list. Pin that semantic explicitly (a test that
just checks IDs would pass even if soulseek was called and
returned [])."""
engine = DownloadEngine()
sl_plugin = _FakePlugin('soulseek', downloads=[_FakeStatus('sl-1', 'soulseek')])
yt_plugin = _FakePlugin('youtube', downloads=[_FakeStatus('yt-1', 'youtube')])
engine.register_plugin('soulseek', sl_plugin)
engine.register_plugin('youtube', yt_plugin)
# Sentinel — flips True if get_all_downloads is called on soulseek.
soulseek_called = []
original_get_all = sl_plugin.get_all_downloads
async def _tracking_get_all():
soulseek_called.append(True)
return await original_get_all()
sl_plugin.get_all_downloads = _tracking_get_all
_run_async(engine.get_all_downloads(exclude=('soulseek',)))
assert soulseek_called == [], (
"soulseek's get_all_downloads was invoked despite being in exclude — "
"monitor would still double-fetch slskd"
)
def test_engine_get_all_downloads_skips_excluded_sources():
"""Per JohnBaumb: monitor pulls slskd transfers via the
transfers/downloads endpoint earlier in its loop, so engine
aggregation must skip soulseek to avoid double-fetching."""
engine = DownloadEngine()
sl_plugin = _FakePlugin('soulseek', downloads=[_FakeStatus('sl-1', 'soulseek')])
yt_plugin = _FakePlugin('youtube', downloads=[_FakeStatus('yt-1', 'youtube')])
td_plugin = _FakePlugin('tidal', downloads=[_FakeStatus('td-1', 'tidal')])
engine.register_plugin('soulseek', sl_plugin)
engine.register_plugin('youtube', yt_plugin)
engine.register_plugin('tidal', td_plugin)
result = _run_async(engine.get_all_downloads(exclude=('soulseek',)))
ids = {r.id for r in result}
assert ids == {'yt-1', 'td-1'}
assert 'sl-1' not in ids
def test_engine_get_all_downloads_swallows_per_plugin_exceptions():
"""One plugin throwing must NOT take down the whole list — same
defensive behavior as the legacy orchestrator (matched by
`try ... except: pass` on every iteration)."""
engine = DownloadEngine()
class _BrokenPlugin:
async def get_all_downloads(self):
raise RuntimeError("boom")
yt_plugin = _FakePlugin('youtube', downloads=[_FakeStatus('yt-1', 'youtube')])
engine.register_plugin('broken', _BrokenPlugin())
engine.register_plugin('youtube', yt_plugin)
result = _run_async(engine.get_all_downloads())
assert [r.id for r in result] == ['yt-1']
def test_engine_get_download_status_returns_first_match():
engine = DownloadEngine()
yt_plugin = _FakePlugin('youtube', downloads=[_FakeStatus('shared', 'youtube')])
td_plugin = _FakePlugin('tidal', downloads=[])
engine.register_plugin('youtube', yt_plugin)
engine.register_plugin('tidal', td_plugin)
result = _run_async(engine.get_download_status('shared'))
assert result is not None
assert result.id == 'shared'
def test_engine_cancel_routes_streaming_source_directly():
"""When source_hint is a known streaming-source name (not
'soulseek'), engine routes the cancel to that specific plugin
only — doesn't ask every other plugin first."""
engine = DownloadEngine()
yt_plugin = _FakePlugin('youtube')
td_plugin = _FakePlugin('tidal')
engine.register_plugin('youtube', yt_plugin)
engine.register_plugin('tidal', td_plugin)
_run_async(engine.cancel_download('dl-1', 'tidal', remove=False))
assert yt_plugin.cancel_calls == []
assert td_plugin.cancel_calls == [('dl-1', 'tidal', False)]
def test_engine_cancel_routes_unknown_source_hint_to_soulseek():
"""A username that's NOT in the plugin registry is a real
Soulseek peer name — route to the soulseek plugin."""
engine = DownloadEngine()
sl_plugin = _FakePlugin('soulseek')
yt_plugin = _FakePlugin('youtube')
engine.register_plugin('soulseek', sl_plugin)
engine.register_plugin('youtube', yt_plugin)
_run_async(engine.cancel_download('dl-1', 'random_peer_username', remove=False))
assert sl_plugin.cancel_calls == [('dl-1', 'random_peer_username', False)]
assert yt_plugin.cancel_calls == []
def test_engine_cancel_falls_back_to_iterating_all_plugins_without_hint():
"""No source hint → ask every plugin until one accepts the
cancel (returns True). Mirrors legacy orchestrator behavior."""
engine = DownloadEngine()
yt_plugin = _FakePlugin('youtube', cancel_result=False)
td_plugin = _FakePlugin('tidal', cancel_result=True)
engine.register_plugin('youtube', yt_plugin)
engine.register_plugin('tidal', td_plugin)
result = _run_async(engine.cancel_download('dl-1', None, remove=False))
assert result is True
# Both plugins were asked; tidal accepted.
assert len(yt_plugin.cancel_calls) == 1
assert len(td_plugin.cancel_calls) == 1
def test_engine_clear_all_skips_unconfigured_plugins():
"""Unconfigured plugins are silently skipped (no API call, no
error) — matches legacy orchestrator's defensive handling."""
engine = DownloadEngine()
configured = _FakePlugin('youtube', configured=True, clear_result=True)
unconfigured = _FakePlugin('tidal', configured=False)
engine.register_plugin('youtube', configured)
engine.register_plugin('tidal', unconfigured)
result = _run_async(engine.clear_all_completed_downloads())
assert result is True
assert configured.clear_calls == 1
assert unconfigured.clear_calls == 0
def test_engine_clear_all_returns_false_when_any_configured_plugin_fails():
engine = DownloadEngine()
failing = _FakePlugin('youtube', configured=True, clear_result=False)
engine.register_plugin('youtube', failing)
result = _run_async(engine.clear_all_completed_downloads())
assert result is False
# ---------------------------------------------------------------------------
# Hybrid fallback (Phase F)
# ---------------------------------------------------------------------------
class _FakeSearchPlugin:
def __init__(self, name, configured=True, search_result=None, raises=None):
self.name = name
self._configured = configured
self._search_result = search_result if search_result is not None else ([], [])
self._raises = raises
self.search_calls = 0
def is_configured(self):
return self._configured
async def search(self, query, timeout=None, progress_callback=None):
self.search_calls += 1
if self._raises:
raise self._raises
return self._search_result
class _FakeDownloadPlugin:
def __init__(self, name, configured=True, download_result=None, raises=None):
self.name = name
self._configured = configured
self._download_result = download_result
self._raises = raises
self.download_calls = []
def is_configured(self):
return self._configured
async def download(self, username, filename, file_size):
self.download_calls.append((username, filename, file_size))
if self._raises:
raise self._raises
return self._download_result
def test_search_with_fallback_returns_first_non_empty_result():
engine = DownloadEngine()
yt = _FakeSearchPlugin('youtube', search_result=([], []))
td = _FakeSearchPlugin('tidal', search_result=(['track1'], []))
qz = _FakeSearchPlugin('qobuz', search_result=(['track2'], []))
engine.register_plugin('youtube', yt)
engine.register_plugin('tidal', td)
engine.register_plugin('qobuz', qz)
tracks, _ = _run_async(engine.search_with_fallback('q', ['youtube', 'tidal', 'qobuz']))
assert tracks == ['track1']
# Tidal short-circuits — qobuz never queried.
assert yt.search_calls == 1
assert td.search_calls == 1
assert qz.search_calls == 0
def test_search_with_fallback_skips_unconfigured_plugins():
engine = DownloadEngine()
yt = _FakeSearchPlugin('youtube', configured=False)
td = _FakeSearchPlugin('tidal', configured=True, search_result=(['hit'], []))
engine.register_plugin('youtube', yt)
engine.register_plugin('tidal', td)
tracks, _ = _run_async(engine.search_with_fallback('q', ['youtube', 'tidal']))
assert tracks == ['hit']
assert yt.search_calls == 0 # skipped
def test_search_with_fallback_continues_after_per_source_exception():
engine = DownloadEngine()
yt = _FakeSearchPlugin('youtube', raises=RuntimeError("yt down"))
td = _FakeSearchPlugin('tidal', search_result=(['fallback-hit'], []))
engine.register_plugin('youtube', yt)
engine.register_plugin('tidal', td)
tracks, _ = _run_async(engine.search_with_fallback('q', ['youtube', 'tidal']))
assert tracks == ['fallback-hit']
def test_search_with_fallback_returns_empty_when_chain_exhausted():
engine = DownloadEngine()
yt = _FakeSearchPlugin('youtube', search_result=([], []))
td = _FakeSearchPlugin('tidal', search_result=([], []))
engine.register_plugin('youtube', yt)
engine.register_plugin('tidal', td)
tracks, _ = _run_async(engine.search_with_fallback('q', ['youtube', 'tidal']))
assert tracks == []
assert yt.search_calls == 1
assert td.search_calls == 1
def test_download_with_fallback_returns_first_accepted_download_id():
"""Phase F bug fix: legacy hybrid download routed to one source
via username hint with no retry. Engine now falls through chain."""
engine = DownloadEngine()
yt = _FakeDownloadPlugin('youtube', download_result=None) # refuses
td = _FakeDownloadPlugin('tidal', download_result='td-id')
engine.register_plugin('youtube', yt)
engine.register_plugin('tidal', td)
result = _run_async(engine.download_with_fallback(
'youtube', 'v||t', 0, ['youtube', 'tidal'],
))
assert result == 'td-id'
assert len(yt.download_calls) == 1 # tried first
assert len(td.download_calls) == 1 # took over
def test_download_with_fallback_promotes_username_hint_to_head():
"""A username hint that matches a source-chain entry tries that
source FIRST regardless of declared chain order."""
engine = DownloadEngine()
yt = _FakeDownloadPlugin('youtube', download_result='yt-id')
td = _FakeDownloadPlugin('tidal', download_result='td-id')
engine.register_plugin('youtube', yt)
engine.register_plugin('tidal', td)
# Chain says tidal-first, but username hint promotes youtube.
result = _run_async(engine.download_with_fallback(
'youtube', 'v||t', 0, ['tidal', 'youtube'],
))
assert result == 'yt-id'
assert len(yt.download_calls) == 1
assert len(td.download_calls) == 0 # never reached
def test_download_with_fallback_returns_none_when_all_refuse():
engine = DownloadEngine()
yt = _FakeDownloadPlugin('youtube', download_result=None)
td = _FakeDownloadPlugin('tidal', download_result=None)
engine.register_plugin('youtube', yt)
engine.register_plugin('tidal', td)
result = _run_async(engine.download_with_fallback(
'youtube', 'v||t', 0, ['youtube', 'tidal'],
))
assert result is None
assert len(yt.download_calls) == 1
assert len(td.download_calls) == 1
def test_download_with_fallback_continues_past_exception():
engine = DownloadEngine()
yt = _FakeDownloadPlugin('youtube', raises=RuntimeError("yt died"))
td = _FakeDownloadPlugin('tidal', download_result='td-id')
engine.register_plugin('youtube', yt)
engine.register_plugin('tidal', td)
result = _run_async(engine.download_with_fallback(
'youtube', 'v||t', 0, ['youtube', 'tidal'],
))
assert result == 'td-id'
# ---------------------------------------------------------------------------
# Cin bug 1: alias resolution on cancel_download
# ---------------------------------------------------------------------------
def test_register_plugin_records_aliases():
"""Aliases passed to register_plugin resolve to the canonical plugin
via get_plugin. Cin caught engine.cancel_download routing 'deezer_dl'
to soulseek because the alias never made it to the engine."""
engine = DownloadEngine()
deezer = _FakePlugin('deezer')
engine.register_plugin('deezer', deezer, aliases=('deezer_dl',))
assert engine.get_plugin('deezer') is deezer
assert engine.get_plugin('deezer_dl') is deezer
def test_cancel_download_resolves_alias_to_canonical_plugin():
"""The legacy 'deezer_dl' source_hint must route to the deezer
plugin, not fall through to soulseek. This was Cin's bug 1 —
cancel of a Deezer download silently no-op'd."""
engine = DownloadEngine()
soulseek = _FakePlugin('soulseek')
deezer = _FakePlugin('deezer')
engine.register_plugin('soulseek', soulseek)
engine.register_plugin('deezer', deezer, aliases=('deezer_dl',))
_run_async(engine.cancel_download('dl-1', 'deezer_dl', remove=False))
assert deezer.cancel_calls == [('dl-1', 'deezer_dl', False)]
assert soulseek.cancel_calls == []
# ---------------------------------------------------------------------------
# Cin bug 3: atomic update_record_unless_state
# ---------------------------------------------------------------------------
def test_update_record_unless_state_applies_when_state_not_blocked():
engine = DownloadEngine()
engine.add_record('youtube', 'dl-1', {'state': 'InProgress, Downloading'})
applied = engine.update_record_unless_state(
'youtube', 'dl-1',
{'state': 'Completed, Succeeded', 'progress': 100.0},
skip_if_state_in=('Cancelled',),
)
assert applied is True
assert engine.get_record('youtube', 'dl-1')['state'] == 'Completed, Succeeded'
assert engine.get_record('youtube', 'dl-1')['progress'] == 100.0
def test_update_record_unless_state_skips_when_state_blocked():
"""A worker-thread terminal write must NOT clobber a Cancelled
state set by the user. Returns False so caller knows the patch
was skipped."""
engine = DownloadEngine()
engine.add_record('youtube', 'dl-1', {'state': 'Cancelled'})
applied = engine.update_record_unless_state(
'youtube', 'dl-1',
{'state': 'Completed, Succeeded'},
skip_if_state_in=('Cancelled',),
)
assert applied is False
assert engine.get_record('youtube', 'dl-1')['state'] == 'Cancelled'
def test_update_record_unless_state_returns_false_for_missing_record():
engine = DownloadEngine()
applied = engine.update_record_unless_state(
'youtube', 'never-existed',
{'state': 'Completed, Succeeded'},
skip_if_state_in=('Cancelled',),
)
assert applied is False