"""Reputation store: a resolve that merely ran out of time must never be
recorded as "vidcore does not have this title".
Regression cover for the 2026-08-10 X-Men '97 failures. The resolver's only
"no url" exit was `no m3u8 surfaced`, which fires both when vidcore genuinely
lacks a title AND when the poll window (6 rounds, ~20s) simply expired first.
The addon filed both as a miss and then hid the stream row for 7 days. Measured
that day: S2E7 missed on a 57s cold resolve and hit 9s later; S2E8 and
tmdb:287238 S1E5 were both sitting in the store as misses while resolving fine
in 11.3s and 6s respectively -- i.e. the store had accumulated false entries,
and with fmovies+vsembed breaker-tripped since Jul 28 a hidden row means the
title has no sources at all.
Run: .venv/bin/python3 -m pytest test_vidcore_reputation.py -q
"""
import os
import tempfile
import time
import pytest
import addon
@pytest.fixture(autouse=True)
def isolated_store(monkeypatch):
"""Never touch the live vidcore_reputation.json."""
fd, path = tempfile.mkstemp(suffix=".json")
os.close(fd)
monkeypatch.setattr(addon, "VIDCORE_REP_FILE", path)
monkeypatch.setattr(addon, "_vidcore_rep", {})
yield
os.unlink(path)
KEY = "series:tmdb:138502:2:8"
# --- classifying a resolver result -----------------------------------------
# "hit" -> vidcore served a url
# "miss" -> vidcore positively has nothing (page never left FETCHING YOUR
# CONTENT..., or there is no TMDB match at all)
# None -> inconclusive; stays retryable and is NOT written to the store
def test_url_is_a_hit():
assert addon._rep_outcome({"streams": [{"url": "https://x/master.m3u8"}]}) == "hit"
def test_absent_is_a_miss():
assert addon._rep_outcome({"error": "no m3u8 surfaced", "reason": "absent"}) == "miss"
def test_timeout_is_inconclusive():
"""THE BUG: the poll window expiring is not evidence of absence."""
assert addon._rep_outcome({"error": "no m3u8 surfaced", "reason": "timeout"}) is None
def test_unknown_reason_is_inconclusive():
"""Fail safe: an unrecognised reason must not blacklist a title."""
assert addon._rep_outcome({"error": "boom"}) is None
# --- how many strikes before a row is hidden --------------------------------
def test_one_absent_does_not_hide():
"""A single cold/slow run must not cost the title a week."""
addon._rep_record(KEY, "miss")
assert addon._rep_is_known_miss(KEY) is False
def test_two_absents_hide():
addon._rep_record(KEY, "miss")
addon._rep_record(KEY, "miss")
assert addon._rep_is_known_miss(KEY) is True
def test_a_hit_clears_the_strikes():
addon._rep_record(KEY, "miss")
addon._rep_record(KEY, "hit")
addon._rep_record(KEY, "miss")
assert addon._rep_is_known_miss(KEY) is False
def test_strikes_expire_with_the_ttl():
"""Two misses a fortnight apart are not two consecutive misses."""
addon._rep_record(KEY, "miss")
addon._vidcore_rep[KEY]["ts"] = time.time() - (addon.VIDCORE_MISS_TTL + 60)
addon._rep_record(KEY, "miss")
assert addon._rep_is_known_miss(KEY) is False
# --- entries written under the old conflated rule ---------------------------
def test_legacy_entries_are_not_trusted():
"""Pre-fix entries carry no strike count and cannot be distinguished from
false misses -- two of the three checked on 2026-08-10 were false. They must
re-probe rather than keep hiding a playable title."""
addon._vidcore_rep[KEY] = {"status": "miss", "ts": time.time()}
assert addon._rep_is_known_miss(KEY) is False
# --- the resolver contract this depends on ----------------------------------
def test_resolver_reports_a_reason():
"""addon._rep_outcome is only correct if the resolver actually emits
`reason`; assert the source carries every reason the addon branches on."""
mjs = os.path.join(os.path.dirname(os.path.abspath(addon.__file__)),
"node_helper", "vidcore_resolve.mjs")
with open(mjs) as f:
src = f.read()
assert "reason" in src
for reason in ("absent", "timeout"):
assert f"'{reason}'" in src or f'"{reason}"' in src