Files
muse/tests/test_acquisition.py
T
Aryan SinghandClaude Opus 5 e9fba3bb1d Check the audio's length, since the filename can agree and still be wrong
The file that got filed as Michael Jackson's "Chicago" had every textual signal
in its favour: the title matched exactly, and once approved it landed under the
right artist, album and track number. The only thing that disagreed was the
running time -- 359 seconds against Deezer's 245 -- because it was The Fall.

Duration is the one signal a stranger's filename cannot fake, and muse already
had it: Deezer supplies it at request time and it has been sitting unused in the
jobs table. On import the real length is read off the downloaded audio and
compared; a drift past MUSE_LENGTH_TOLERANCE multiplies confidence down and
explains itself in the review queue rather than leaving a bare number.

For this case that is 0.75 -> 0.26, which falls below REVIEW_CONFIDENCE and so
reads as "almost certainly wrong" instead of "worth a look". It runs on import
rather than in confidence() because the file does not exist when the candidate
is chosen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 14:12:16 -05:00

370 lines
16 KiB
Python

"""The acquisition ladder: query variants, candidate fallback, stall handling.
This is the path that had no coverage at all, and it is where every production
failure lived. A fake slskd stands in for the network so the branches that only
happen on a bad day -- a peer that rejects, a transfer slskd forgets, a search
the server filters -- can be exercised deterministically.
Run standalone: python tests/test_acquisition.py
"""
import asyncio, json, os, sys
from pathlib import Path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
os.environ.setdefault("MUSE_AUTH", "off")
os.environ.setdefault("MUSE_DB", "dev/acquisition.db")
os.environ.setdefault("MUSE_SEARCH_POLLS", "2")
for f in ("dev/acquisition.db", "dev/acquisition.db-wal", "dev/acquisition.db-shm"):
if os.path.exists(f):
os.remove(f)
import app # noqa: E402
ok = fail = 0
def check(name, cond, detail=""):
global ok, fail
if cond:
ok += 1
print(f" PASS {name}")
else:
fail += 1
print(f" FAIL {name} {detail}")
# Searches poll on a 2s cadence; nothing here is actually waiting on a network.
async def _instant(_):
return None
app.asyncio.sleep = _instant
app.init_db() # normally done in lifespan; there is no server here
# ── a slskd that behaves however the test needs ──────────────────────────
class Resp:
def __init__(self, data=None, status=200):
self._data, self.status = data, status
def raise_for_status(self):
if self.status >= 400:
raise RuntimeError(f"Server error '{self.status} Internal Server Error'")
def json(self):
return self._data
class FakeSlskd:
"""Records what was asked of it and answers from a script."""
def __init__(self, per_query=None, enqueue_status=None, transfers=None):
self.per_query = per_query or {} # query text -> list of responses
self.enqueue_status = enqueue_status or {} # username -> http status
self.transfers = transfers or []
self.searched = []
self.enqueued = []
self.deleted = []
self._last = None
def __call__(self, timeout=30.0):
return self
async def __aenter__(self):
return self
async def __aexit__(self, *a):
return False
async def post(self, url, json=None):
if url == "/api/v0/searches":
self._last = json["searchText"]
self.searched.append(self._last)
return Resp({})
user = url.rsplit("/", 1)[-1]
st = self.enqueue_status.get(user, 200)
self.enqueued.append(user)
return Resp({}, st)
async def get(self, url, params=None):
if url.startswith("/api/v0/searches/"):
responses = self.per_query.get(self._last, [])
return Resp({"state": "Completed, ResponseLimitReached",
"responses": responses,
"fileCount": sum(len(r["files"]) for r in responses)})
return Resp(self.transfers)
async def delete(self, url, params=None):
self.deleted.append(url)
return Resp({})
def peer(username, filename, *, slot=True, queue=0, size=5_000_000, bitrate=320):
return {"username": username, "hasFreeUploadSlot": slot,
"queueLength": queue, "uploadSpeed": 1_000_000,
"files": [{"filename": filename, "size": size, "bitRate": bitrate}]}
def make_job(job_id, artist="Michael Jackson", title="Billie Jean"):
with app.db() as c:
c.execute("INSERT OR REPLACE INTO jobs (id, state, artist, title, album)"
" VALUES (?,?,?,?,?)", (job_id, "searching", artist, title, None))
def job_row(job_id):
with app.db() as c:
return c.execute("SELECT * FROM jobs WHERE id=?", (job_id,)).fetchone()
def main() -> int:
print("\nnormalisation")
check("accents fold to ASCII", app.norm("Beyoncé") == "beyonce",
repr(app.norm("Beyoncé")))
check("umlauts fold", app.norm("Björk") == "bjork", repr(app.norm("Björk")))
check("slashed o folds", app.norm("margø") == "margo", repr(app.norm("margø")))
check("apostrophes vanish, not split",
app.norm("Don't Stop") == "dont stop", repr(app.norm("Don't Stop")))
check("ampersand still separates", app.norm("R&B") == "r b", repr(app.norm("R&B")))
print("\nquery ladder")
qs = app.search_queries("Michael Jackson", "Billie Jean")
check("falls back to the bare title", "Billie Jean" in qs, qs)
check("full query is still tried first", qs[0] == "Michael Jackson Billie Jean", qs)
qs = app.search_queries("Frank Ocean", "Novacane (Explicit Version)")
check("strips a parenthetical suffix", "Frank Ocean Novacane" in qs, qs)
qs = app.search_queries("Post Sex Nachos", "Pop Tarts (feat. MARIS)")
check("strips a featuring credit", "Post Sex Nachos Pop Tarts" in qs, qs)
qs = app.search_queries("The Weeknd", "House Of Balloons / Glass Table Girls")
check("splits a slashed title", "House Of Balloons" in qs, qs)
check("ladder is capped", len(app.search_queries("A", "B")) <= app.MAX_QUERIES)
print("\nscoring")
avail_mp3 = app.score({"filename": r"x\Radiohead - Creep.mp3", "size": 1,
"bitRate": 320},
{"username": "u", "hasFreeUploadSlot": True,
"queueLength": 0, "uploadSpeed": 0},
"Radiohead", "Creep")
queued_flac = app.score({"filename": r"x\Radiohead - Creep.flac", "size": 1,
"bitRate": 1000},
{"username": "v", "hasFreeUploadSlot": False,
"queueLength": 400, "uploadSpeed": 0},
"Radiohead", "Creep")
check("an available MP3 beats a FLAC behind 400 people",
avail_mp3 > queued_flac, f"{avail_mp3:.0f} vs {queued_flac:.0f}")
check("artist found in the folder, not the filename",
app.match_ratio(r"Radiohead\Pablo Honey\02 Creep.flac",
"Radiohead", "Creep") == 1.0)
check("a matching artist cannot rescue the wrong song",
app.match_ratio(r"Radiohead\Pablo Honey\04 Karma Police.flac",
"Radiohead", "Creep") == 0.0)
check("reordered filename still matches",
app.match_ratio(r"x\Creep - Radiohead.mp3", "Radiohead", "Creep") == 1.0)
print("\nthe artist gate")
check("artist named in the folder is confirmed",
app.artist_confirmed(r"m\Michael Jackson\Xscape\02 Chicago.flac",
"Michael Jackson"))
check("a different band's song with the same title is not",
not app.artist_confirmed(
r"@@jwbvv\Music\The Fall\1990-Extricate\08-Chicago, Now!.flac",
"Michael Jackson"))
check("out-of-order naming still counts",
app.artist_confirmed(r"x\Jackson, Michael - Chicago.flac",
"Michael Jackson"))
# The real regression: bare-title search returns every song called
# "Chicago", and an available stranger used to outrank a queued match.
gate = FakeSlskd(per_query={"Chicago": [
peer("stranger", r"@@x\Music\The Fall\1990-Extricate\08-Chicago, Now!.flac",
slot=True, queue=0),
peer("correct", r"m\Michael Jackson\XSCAPE\02 - Chicago.flac",
slot=False, queue=300),
]})
app.slskd_client = gate
make_job("j6", "Michael Jackson", "Chicago")
asyncio.run(app.pick_and_enqueue("j6", "Michael Jackson", "Chicago"))
r = job_row("j6")
check("the right artist wins even from a peer with 300 queued",
r["username"] == "correct", f"picked {r['username']}")
check("and the wrong-artist file is dropped, not merely ranked lower",
len(json.loads(r["candidates"] or "[]")) == 0, r["candidates"])
print("\ncandidate ladder")
fake = FakeSlskd(per_query={
"Billie Jean": [peer("alice", r"m\Michael Jackson\Billie Jean.flac"),
peer("bob", r"n\Michael Jackson\Billie Jean.mp3"),
peer("carol", r"o\Michael Jackson\Billie Jean.flac")],
})
app.slskd_client = fake
make_job("j1")
asyncio.run(app.pick_and_enqueue("j1", "Michael Jackson", "Billie Jean"))
r = job_row("j1")
check("blocklisted artist recovered by the title rung",
"Billie Jean" in fake.searched, fake.searched)
check("job is downloading", r["state"] == "downloading", r["detail"])
check("runners-up were kept", len(json.loads(r["candidates"] or "[]")) == 2,
r["candidates"])
check("consecutive candidates are different peers",
len({c["username"] for c in json.loads(r["candidates"])}) == 2)
print("\nfallback on failure")
fake.enqueue_status = {}
asyncio.run(app.abandon(job_row("j1"), "Transfer rejected: File not shared."))
r = job_row("j1")
check("a rejection advances instead of failing",
r["state"] == "downloading", f"{r['state']} / {r['detail']}")
check("the failed peer is recorded",
"File not shared" in (r["tried"] or ""), r["tried"])
check("attempt counter advanced", r["attempts"] == 2, r["attempts"])
asyncio.run(app.abandon(job_row("j1"), "timed out"))
asyncio.run(app.abandon(job_row("j1"), "timed out"))
r = job_row("j1")
check("only fails once the ladder is exhausted", r["state"] == "failed", r["detail"])
check("failure names every attempt, not just the last",
"tried 3 peers" in (r["detail"] or ""), r["detail"])
# A row carried over from before the ladder has attempts=0 but a real
# failure recorded, which must not render as "tried 0 peers: 1x ...".
check("a migrated row counts its evidence, not its counter",
app.exhausted_detail(
[{"username": "u1", "reason": "no bytes in 20m"}], 0)
== "tried 1 peer: 1x no bytes in 20m",
app.exhausted_detail([{"username": "u1", "reason": "no bytes in 20m"}], 0))
print("\nattempt cap")
fake2 = FakeSlskd(per_query={"Creep": [
peer(f"u{i}", rf"x\Radiohead\Creep.flac") for i in range(8)]})
# every peer refuses to accept the transfer
fake2.enqueue_status = {f"u{i}": 500 for i in range(8)}
app.slskd_client = fake2
# The cap defaults to the ladder size, so lower it here or this only ever
# proves the list ran out -- which is a different branch.
cap, app.MAX_ATTEMPTS = app.MAX_ATTEMPTS, 3
make_job("j2", "Radiohead", "Creep")
asyncio.run(app.pick_and_enqueue("j2", "Radiohead", "Creep"))
r = job_row("j2")
check("gives up at MAX_ATTEMPTS, not at the end of the list",
r["state"] == "failed" and r["attempts"] == 3,
f"state={r['state']} attempts={r['attempts']}")
check("and leaves the untried remainder alone",
len(json.loads(r["candidates"] or "[]")) > 0, r["candidates"])
app.MAX_ATTEMPTS = cap
check("unreachable peers are reported as such",
"could not reach peer" in (r["tried"] or ""), r["detail"])
print("\nsystemic failures do not burn the ladder")
# slskd timing out says nothing about any particular file, so walking the
# whole list to learn it once is waste -- and it is what took a real
# request down: five enqueues in twenty-five seconds during one slskd stall.
fake4 = FakeSlskd(per_query={"Chicago": [
peer(f"p{i}", r"x\Michael Jackson\Xscape\Chicago.flac") for i in range(8)]})
class Timeouts(FakeSlskd):
async def post(self, url, json=None):
if url != "/api/v0/searches":
raise RuntimeError("The wait timed out after 5000 milliseconds")
return await FakeSlskd.post(self, url, json=json)
t = Timeouts(per_query=fake4.per_query)
app.slskd_client = t
make_job("j5", "Michael Jackson", "Chicago")
asyncio.run(app.pick_and_enqueue("j5", "Michael Jackson", "Chicago"))
r = job_row("j5")
check("stops after SYSTEMIC_LIMIT instead of walking the whole list",
r["attempts"] == app.SYSTEMIC_LIMIT, r["attempts"])
left = json.loads(r["candidates"] or "[]")
check("untried candidates are preserved for a retry", len(left) > 0, len(left))
check("the message says a retry will resume",
"retry resumes" in (r["detail"] or ""), r["detail"])
print("\nempty results")
fake3 = FakeSlskd(per_query={})
app.slskd_client = fake3
make_job("j3", "Nobody", "Nothing")
asyncio.run(app.pick_and_enqueue("j3", "Nobody", "Nothing"))
r = job_row("j3")
check("says the search was empty, not that matching failed",
"Soulseek returned nothing" in (r["detail"] or ""), r["detail"])
check("names the queries it tried", "'Nothing'" in (r["detail"] or ""), r["detail"])
print("\ntransient slskd")
class Flaky(FakeSlskd):
def __init__(self):
super().__init__(per_query={"Creep": [peer("z", r"x\Creep.flac")]})
self.n = 0
async def post(self, url, json=None):
if url == "/api/v0/searches":
self.n += 1
if self.n < 3:
raise RuntimeError("") # httpx timeouts stringify to nothing
return await FakeSlskd.post(self, url, json=json)
flaky = Flaky()
app.slskd_client = flaky
make_job("j4", "Radiohead", "Creep")
asyncio.run(app.pick_and_enqueue("j4", "Radiohead", "Creep"))
check("a blip is retried rather than failing the job",
job_row("j4")["state"] == "downloading", job_row("j4")["detail"])
print("\nsearch hygiene")
check("finished searches are cleaned up", len(flaky.deleted) >= 1)
print("\nlength cross-check")
# The signal a filename cannot fake. Every textual cue agreed on the file
# that got filed as Michael Jackson; only the running time disagreed.
class FakeInfo:
def __init__(self, n): self.length = n
class FakeAudio:
def __init__(self, n): self.info = FakeInfo(n)
real_file = mutagen_file = app.mutagen.File
try:
app.mutagen.File = lambda p: FakeAudio(359.0) # The Fall
got = app.length_check(Path("x.flac"), 245 * 1000) # Michael Jackson
check("a 47% length disagreement is caught", got is not None, got)
check("and it drops the file below the review floor",
got and round(0.75 * got[0], 3) < app.REVIEW_CONFIDENCE,
got and round(0.75 * got[0], 3))
check("the reason names both lengths",
got and "5:59" in got[1] and "4:05" in got[1], got and got[1])
app.mutagen.File = lambda p: FakeAudio(247.0)
check("a two-second drift is not flagged",
app.length_check(Path("x.flac"), 245 * 1000) is None)
app.mutagen.File = lambda p: FakeAudio(359.0)
check("nothing to compare against is not a failure",
app.length_check(Path("x.flac"), None) is None)
finally:
app.mutagen.File = mutagen_file
print("\nstall clock")
from datetime import datetime, timedelta, timezone
with app.db() as c:
c.execute("UPDATE jobs SET started_at=? WHERE id=?",
((datetime.now(timezone.utc) - timedelta(minutes=90)).isoformat(),
"j4"))
check("a 90-minute-old transfer reads as stalled",
app.stalled_for(job_row("j4")) > app.STALL_MINUTES,
app.stalled_for(job_row("j4")))
# A row written before started_at existed still has to age out, or the
# jobs that have been stuck longest are the only ones exempt from the check.
with app.db() as c:
c.execute("UPDATE jobs SET started_at=NULL, created_at=? WHERE id=?",
((datetime.now(timezone.utc) - timedelta(days=2))
.isoformat(sep=" ")[:19], "j4"))
check("a pre-upgrade row falls back to created_at",
app.stalled_for(job_row("j4")) > app.STALL_MINUTES,
app.stalled_for(job_row("j4")))
print(f"\n{ok} passed, {fail} failed")
return 1 if fail else 0
if __name__ == "__main__":
sys.exit(main())