fix(plex,audit): E30 review round 1 — the people job keeps going, HU genres everywhere, results as data
- people_enrich: a failed TMDB lookup or a dead headshot is skipped, not the end of the run (a dead file is marked and left for a week; three failures in a row end it); portraits wait for the Plex module switch; a no-Plex original keeps its own cache key, so the resize still replaces it; the job entry lives in the plex domain (run_people_enrich) - person-image takes only a headshot-shaped CDN url (no query variants of one file) - every synced row gets Hungarian genres, and a HU genre filter matches the word as picked too; migration 0094 fills the rows no overlay reached - a job run's audit row carries its result as data; audit_clear / demo_reset store their counts - one credit-column list for the prune and the enrichment order; _overlay_pass typed - tests: batch skip + stop, HU bio stored, dead headshots, module switch, the person-image route, the job body, genre filter + facet write + twin fallback, tagline/summary fallbacks, audit data, the reader's alphabet at every remaining collated() call site
This commit is contained in:
@@ -207,3 +207,96 @@ def test_channel_discovery_keeps_that_order_after_enriching(db, monkeypatch, ove
|
||||
_seed_discovery(db, user, synced=False)
|
||||
r = _client(db, user, monkeypatch, override_dep).get("/api/channels/discovery")
|
||||
assert [c["title"] for c in r.json()] == HU
|
||||
|
||||
|
||||
# --- The other lists the server puts in the reader's alphabet (E30 review): each call site, once. ---
|
||||
|
||||
|
||||
def _others(db, n_offset: int = 0) -> list[User]:
|
||||
users = [
|
||||
User(email=f"{w.lower()}@example.com", display_name=w) for w in WORDS
|
||||
] # emails and names in WORDS' order
|
||||
db.add_all(users)
|
||||
db.flush()
|
||||
return users
|
||||
|
||||
|
||||
def test_the_share_and_message_recipients_are_in_the_readers_alphabet(
|
||||
db, monkeypatch, override_dep
|
||||
):
|
||||
from app.models import MessageKey
|
||||
|
||||
user = _user(db, "hu")
|
||||
others = _others(db)
|
||||
db.add_all(
|
||||
MessageKey(
|
||||
user_id=u.id,
|
||||
public_key="k",
|
||||
wrapped_private_key="w",
|
||||
salt="s",
|
||||
wrap_iv="i",
|
||||
key_check="c",
|
||||
)
|
||||
for u in others
|
||||
)
|
||||
db.commit()
|
||||
client = _client(db, user, monkeypatch, override_dep)
|
||||
emails = [r["email"] for r in client.get("/api/downloads/recipients").json()]
|
||||
assert emails == [f"{w.lower()}@example.com" for w in HU]
|
||||
names = [r["name"] for r in client.get("/api/messages/users").json()]
|
||||
assert names == HU
|
||||
|
||||
|
||||
def test_the_blocked_channels_are_in_the_readers_alphabet(db, monkeypatch, override_dep):
|
||||
from app.models import BlockedChannel
|
||||
|
||||
user = _user(db, "hu")
|
||||
db.add_all(Channel(id=f"UC{i}", title=w) for i, w in enumerate(WORDS))
|
||||
db.flush()
|
||||
db.add_all(BlockedChannel(user_id=user.id, channel_id=f"UC{i}") for i in range(len(WORDS)))
|
||||
db.commit()
|
||||
r = _client(db, user, monkeypatch, override_dep).get("/api/channels/blocked/list")
|
||||
assert [c["title"] for c in r.json()] == HU
|
||||
|
||||
|
||||
def test_the_plex_libraries_and_collections_are_in_the_readers_alphabet(
|
||||
db, monkeypatch, override_dep
|
||||
):
|
||||
user = _user(db, "hu")
|
||||
libs = [
|
||||
PlexLibrary(plex_key=str(i), title=w, kind="movie", enabled=True)
|
||||
for i, w in enumerate(WORDS)
|
||||
]
|
||||
db.add_all(libs)
|
||||
db.flush()
|
||||
db.add_all(
|
||||
PlexCollection(rating_key=f"c{i}", library_id=libs[0].id, title=w, child_count=1)
|
||||
for i, w in enumerate(WORDS)
|
||||
)
|
||||
db.commit()
|
||||
client = _client(db, user, monkeypatch, override_dep)
|
||||
assert [lib["title"] for lib in client.get("/api/plex/libraries").json()["libraries"]] == HU
|
||||
cols = client.get("/api/plex/collections").json()["collections"]
|
||||
assert [c["title"] for c in cols] == HU
|
||||
|
||||
|
||||
def test_a_persons_titles_of_one_year_are_in_the_readers_alphabet(db, monkeypatch, override_dep):
|
||||
user = _user(db, "hu")
|
||||
lib = PlexLibrary(plex_key="1", title="Movies", kind="movie", enabled=True)
|
||||
db.add(lib)
|
||||
db.flush()
|
||||
db.add_all(
|
||||
PlexItem(
|
||||
rating_key=f"m{i}",
|
||||
library_id=lib.id,
|
||||
kind="movie",
|
||||
title=f"EN {i}",
|
||||
title_hu=w,
|
||||
year=2000,
|
||||
cast_names=["Anna"],
|
||||
)
|
||||
for i, w in enumerate(WORDS)
|
||||
)
|
||||
db.commit()
|
||||
body = _client(db, user, monkeypatch, override_dep).get("/api/plex/person/Anna?lang=hu").json()
|
||||
assert [m["title"] for m in body["movies"]] == HU
|
||||
|
||||
@@ -13,6 +13,7 @@ from app.auth import current_user
|
||||
from app.db import get_db
|
||||
from app.main import app
|
||||
from app import sysconfig
|
||||
from app.plex.cards import display_summary, display_title
|
||||
from app.models import (
|
||||
PlexCollection,
|
||||
PlexItem,
|
||||
@@ -57,7 +58,9 @@ def _seed(db):
|
||||
library_id=lib.id,
|
||||
kind="movie",
|
||||
title="EN Only",
|
||||
genres=["Comedy"], # no HU twin
|
||||
summary="English only.",
|
||||
tagline="English only tagline.",
|
||||
genres=["Comedy"], # no HU twin, never reached by a sync since: no genres_hu
|
||||
year=2002,
|
||||
added_at=datetime(2024, 1, 2, tzinfo=timezone.utc),
|
||||
people_enriched_at=datetime(2024, 1, 2, tzinfo=timezone.utc),
|
||||
@@ -125,6 +128,33 @@ def test_a_genre_filter_saved_in_english_still_matches_in_hungarian(db, monkeypa
|
||||
assert _titles(resp) == {"Magyar Cím"}
|
||||
|
||||
|
||||
def test_a_genre_the_hungarian_list_offers_in_english_still_matches(db, monkeypatch, override_dep):
|
||||
"""E30 review: a row the sync has not given Hungarian genres yet is listed under its English word
|
||||
(the facets read coalesce(genres_hu, genres)); filtering by that word must find it."""
|
||||
user = _seed(db)
|
||||
facets = _get(db, user, monkeypatch, override_dep, "/api/plex/facets?scope=movie&lang=hu")
|
||||
assert "Comedy" in {g["value"] for g in facets.json()["genres"]}
|
||||
resp = _get(
|
||||
db, user, monkeypatch, override_dep, "/api/plex/library?scope=movie&lang=hu&genres=Comedy"
|
||||
)
|
||||
assert _titles(resp) == {"EN Only"}
|
||||
|
||||
|
||||
def test_a_title_without_a_hungarian_twin_reads_in_english(db, monkeypatch, override_dep):
|
||||
user = _seed(db)
|
||||
body = _get(db, user, monkeypatch, override_dep, "/api/plex/item/m2?lang=hu").json()
|
||||
assert (body["title"], body["summary"], body["tagline"]) == (
|
||||
"EN Only",
|
||||
"English only.",
|
||||
"English only tagline.",
|
||||
)
|
||||
row = PlexItem(title="T", summary="S")
|
||||
assert (display_title(row, "hu"), display_summary(row, "hu")) == ("T", "S")
|
||||
row.title_hu, row.summary_hu = "HT", "HS"
|
||||
assert (display_title(row, "hu"), display_summary(row, "hu")) == ("HT", "HS")
|
||||
assert (display_title(row, "en"), display_summary(row, "en")) == ("T", "S")
|
||||
|
||||
|
||||
def test_item_detail_language_and_rating_split(db, monkeypatch, override_dep):
|
||||
user = _seed(db)
|
||||
resp = _get(db, user, monkeypatch, override_dep, "/api/plex/item/m1?lang=hu")
|
||||
|
||||
@@ -334,6 +334,42 @@ def test_the_overlay_gives_every_row_one_hungarian_word_per_genre(db):
|
||||
assert lone.tagline_hu is None
|
||||
|
||||
|
||||
def test_a_twin_without_genres_keeps_the_rows_own_hungarian_list(db):
|
||||
en_lib = PlexLibrary(plex_key="4", title="Movies", kind="movie")
|
||||
hu_lib = PlexLibrary(plex_key="9", title="Filmek", kind="movie", metadata_lang="hu")
|
||||
db.add_all([en_lib, hu_lib])
|
||||
db.flush()
|
||||
row = PlexItem(
|
||||
rating_key="m1",
|
||||
library_id=en_lib.id,
|
||||
kind="movie",
|
||||
title="Heat",
|
||||
guid="tmdb:1",
|
||||
genres=["Crime"],
|
||||
)
|
||||
db.add(row)
|
||||
db.commit()
|
||||
_sync_overlay(
|
||||
db,
|
||||
_StubPlex([_hu_movie("h1", "Szemtől szemben", "HU.", [], "tmdb://1")]),
|
||||
hu_lib,
|
||||
en_lib.id,
|
||||
{},
|
||||
)
|
||||
db.commit()
|
||||
db.refresh(row)
|
||||
assert (row.title_hu, row.genres_hu) == ("Szemtől szemben", ["Bűnügyi"])
|
||||
|
||||
|
||||
def test_every_synced_row_gets_hungarian_genres():
|
||||
"""E30 review: not only an overlaid row — the HU facets and filter read genres_hu for all."""
|
||||
from app.plex.sync import _apply_facet_fields
|
||||
|
||||
row = PlexItem(title="Solo")
|
||||
_apply_facet_fields(row, {"Genre": [{"tag": "Drama"}, {"tag": "Action"}]})
|
||||
assert (row.genres, row.genres_hu) == (["Drama", "Action"], ["Dráma", "Akció"])
|
||||
|
||||
|
||||
def test_the_genre_map_is_idempotent_and_keeps_an_unknown_word():
|
||||
assert hu_genre("science fiction") == "Sci-Fi" # case-insensitive
|
||||
assert hu_genre("Dráma") == "Dráma"
|
||||
|
||||
@@ -6,6 +6,7 @@ DB lane (Postgres); the portrait cache runs in a temp directory.
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from app import scheduler
|
||||
@@ -32,13 +33,13 @@ def _library(db):
|
||||
class _Tmdb:
|
||||
"""A TmdbClient stand-in: records the ids asked for, fails on `fail_on`."""
|
||||
|
||||
def __init__(self, fail_on=None):
|
||||
def __init__(self, fail_on=()):
|
||||
self.asked: list[int] = []
|
||||
self.fail_on = fail_on
|
||||
self.fail_on = set(fail_on)
|
||||
|
||||
def fetch_person(self, pid):
|
||||
self.asked.append(pid)
|
||||
if pid == self.fail_on:
|
||||
if pid in self.fail_on:
|
||||
raise plex_tmdb.TmdbError("429")
|
||||
return {"biography": f"Bio {pid}."}
|
||||
|
||||
@@ -67,22 +68,52 @@ def test_the_batch_takes_the_most_credited_linked_people_first(db):
|
||||
assert db.query(PlexPerson).filter_by(name="Rare").one().tmdb_enriched_at is None # next run
|
||||
|
||||
|
||||
def test_a_failed_lookup_ends_the_batch_and_keeps_what_landed(db):
|
||||
def test_a_failed_lookup_is_skipped_and_the_rest_still_land(db):
|
||||
db.add_all([PlexPerson(name="A", tmdb_person_id=1), PlexPerson(name="B", tmdb_person_id=2)])
|
||||
db.add(PlexPerson(name="C", tmdb_person_id=3))
|
||||
db.commit()
|
||||
tmdb = _Tmdb(fail_on=2)
|
||||
assert plex_tmdb.enrich_people_batch(db, tmdb, 10) == {"enriched": 1, "failed": 1} # type: ignore[arg-type]
|
||||
assert tmdb.asked == [1, 2] # C is not asked: TMDB is struggling
|
||||
tmdb = _Tmdb(fail_on={2})
|
||||
assert plex_tmdb.enrich_people_batch(db, tmdb, 10) == {"enriched": 2, "failed": 1} # type: ignore[arg-type]
|
||||
assert tmdb.asked == [1, 2, 3] # B refused: skipped, not the end of the batch
|
||||
db.expire_all()
|
||||
stamped = {p.name: p.tmdb_enriched_at is not None for p in db.query(PlexPerson)}
|
||||
assert stamped == {"A": True, "B": False, "C": False}
|
||||
assert stamped == {"A": True, "B": False, "C": True} # B is tried again next run
|
||||
|
||||
|
||||
def test_failures_in_a_row_end_the_batch(db):
|
||||
db.add_all([PlexPerson(name=f"P{i}", tmdb_person_id=i) for i in range(1, 7)])
|
||||
db.commit()
|
||||
tmdb = _Tmdb(fail_on={1, 2, 3, 4, 5, 6}) # TMDB is down
|
||||
assert plex_tmdb.enrich_people_batch(db, tmdb, 10) == {"enriched": 0, "failed": 3} # type: ignore[arg-type]
|
||||
assert tmdb.asked == [1, 2, 3]
|
||||
|
||||
|
||||
def test_the_batch_stores_the_hungarian_biography(db):
|
||||
db.add(PlexPerson(name="A", tmdb_person_id=1))
|
||||
db.commit()
|
||||
|
||||
class _WithHu(_Tmdb):
|
||||
def fetch_person(self, pid):
|
||||
return {
|
||||
"biography": "English.",
|
||||
"translations": {
|
||||
"translations": [{"iso_639_1": "hu", "data": {"biography": "Magyar."}}]
|
||||
},
|
||||
}
|
||||
|
||||
assert plex_tmdb.enrich_people_batch(db, _WithHu(), 10) == {"enriched": 1} # type: ignore[arg-type]
|
||||
db.expire_all()
|
||||
person = db.query(PlexPerson).one()
|
||||
assert (person.biography, person.biography_hu) == ("English.", "Magyar.")
|
||||
|
||||
|
||||
class _Plex:
|
||||
def __init__(self, fail=False):
|
||||
"""A PlexClient stand-in. `down`: Plex does not answer; `dead`: urls Plex answers with an error."""
|
||||
|
||||
def __init__(self, down=False, dead=()):
|
||||
self.asked: list[tuple[str, int | None]] = []
|
||||
self.fail = fail
|
||||
self.down = down
|
||||
self.dead = set(dead)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
@@ -92,8 +123,14 @@ class _Plex:
|
||||
|
||||
def image_bytes(self, url, width=None, height=None):
|
||||
self.asked.append((url, width))
|
||||
if self.fail:
|
||||
raise plex_people.PlexError("down")
|
||||
if self.down:
|
||||
raise plex_people.PlexError("down") from httpx.ConnectError("refused")
|
||||
if url in self.dead:
|
||||
req = httpx.Request("GET", url)
|
||||
status = httpx.HTTPStatusError(
|
||||
"404", request=req, response=httpx.Response(404, request=req)
|
||||
)
|
||||
raise plex_people.PlexError("404") from status
|
||||
return b"small", "image/jpeg"
|
||||
|
||||
|
||||
@@ -103,7 +140,26 @@ def cache_dir(tmp_path, monkeypatch):
|
||||
return tmp_path
|
||||
|
||||
|
||||
def test_portraits_are_cached_small_and_replace_the_full_size_original(db, cache_dir, monkeypatch):
|
||||
@pytest.fixture
|
||||
def plex_on(monkeypatch):
|
||||
"""The Plex module switched on (the job's gate), with `plex` as its client."""
|
||||
state = {"enabled": True, "plex": _Plex()}
|
||||
monkeypatch.setattr(plex_people.sysconfig, "get_bool", lambda d, k: state["enabled"])
|
||||
monkeypatch.setattr(plex_people, "PlexClient", lambda d: state["plex"])
|
||||
return state
|
||||
|
||||
|
||||
def _faces(db, n):
|
||||
db.add_all(
|
||||
[
|
||||
PlexPerson(name=f"P{i}", photo_thumb=plex_people.person_thumb_url(CDN + f"{i}.jpg"))
|
||||
for i in range(n)
|
||||
]
|
||||
)
|
||||
db.commit()
|
||||
|
||||
|
||||
def test_portraits_are_cached_small_and_replace_the_full_size_original(db, cache_dir, plex_on):
|
||||
lib = _library(db)
|
||||
_movie(db, lib, "a", ["Often"])
|
||||
_movie(db, lib, "b", ["Often", "Cached"])
|
||||
@@ -115,23 +171,21 @@ def test_portraits_are_cached_small_and_replace_the_full_size_original(db, cache
|
||||
]
|
||||
)
|
||||
db.commit()
|
||||
small, legacy = plex_people._portrait_keys(CDN + "often.jpg")
|
||||
imgcache.write(legacy, b"a big original", "image/jpeg") # from before E30 S6
|
||||
small, original = plex_people._portrait_keys(CDN + "often.jpg")
|
||||
imgcache.write(original, b"a big original", "image/jpeg") # cached without Plex
|
||||
imgcache.write(plex_people._portrait_keys(CDN + "cached.jpg")[0], b"x", "image/jpeg")
|
||||
plex = _Plex()
|
||||
monkeypatch.setattr(plex_people, "PlexClient", lambda d: plex)
|
||||
plex = plex_on["plex"]
|
||||
|
||||
assert plex_people.warm_portraits(db, 10) == {"cached": 1}
|
||||
assert plex.asked == [
|
||||
(CDN + "often.jpg", plex_people.PORTRAIT_BOX)
|
||||
] # resized, the cached one skipped
|
||||
assert imgcache.read(small) == (b"small", "image/jpeg")
|
||||
assert not imgcache.has(legacy)
|
||||
assert not imgcache.has(original)
|
||||
|
||||
|
||||
def test_portraits_wait_for_plex(db, cache_dir, monkeypatch):
|
||||
db.add(PlexPerson(name="P", photo_thumb=plex_people.person_thumb_url(CDN + "p.jpg")))
|
||||
db.commit()
|
||||
def test_portraits_wait_for_plex(db, cache_dir, plex_on, monkeypatch):
|
||||
_faces(db, 1)
|
||||
|
||||
def not_configured(d):
|
||||
raise plex_people.PlexNotConfigured()
|
||||
@@ -141,18 +195,99 @@ def test_portraits_wait_for_plex(db, cache_dir, monkeypatch):
|
||||
assert list(cache_dir.iterdir()) == [] # no full-size originals behind Plex's back
|
||||
|
||||
|
||||
def test_a_failing_plex_ends_the_portrait_run(db, cache_dir, monkeypatch):
|
||||
db.add_all(
|
||||
[
|
||||
PlexPerson(name=f"P{i}", photo_thumb=plex_people.person_thumb_url(CDN + f"{i}.jpg"))
|
||||
for i in range(8)
|
||||
]
|
||||
)
|
||||
db.commit()
|
||||
plex = _Plex(fail=True)
|
||||
monkeypatch.setattr(plex_people, "PlexClient", lambda d: plex)
|
||||
assert plex_people.warm_portraits(db, 100) == {"cached": 0, "failed": 5}
|
||||
assert len(plex.asked) == 5
|
||||
def test_portraits_wait_for_the_plex_module_to_be_on(db, cache_dir, plex_on):
|
||||
_faces(db, 1)
|
||||
plex_on["enabled"] = False # switched off, its server still set up
|
||||
assert plex_people.warm_portraits(db, 10) == {"cached": 0, "skipped": "disabled"}
|
||||
assert plex_on["plex"].asked == []
|
||||
|
||||
|
||||
def test_plex_not_answering_ends_the_portrait_run(db, cache_dir, plex_on):
|
||||
_faces(db, 8)
|
||||
plex_on["plex"] = _Plex(down=True)
|
||||
assert plex_people.warm_portraits(db, 100) == {"cached": 0, "failed": 3}
|
||||
assert len(plex_on["plex"].asked) == 3
|
||||
assert not list(cache_dir.glob("*.miss")) # Plex was down, the files are not to blame
|
||||
|
||||
|
||||
def test_dead_headshots_are_left_alone_and_the_rest_still_land(db, cache_dir, plex_on):
|
||||
_faces(db, 8)
|
||||
dead = {CDN + f"{i}.jpg" for i in range(6)} # more dead files than any failure limit
|
||||
plex_on["plex"] = _Plex(dead=dead)
|
||||
assert plex_people.warm_portraits(db, 100) == {"cached": 2, "failed": 6}
|
||||
|
||||
plex_on["plex"] = again = _Plex(dead=dead)
|
||||
assert plex_people.warm_portraits(db, 100) == {"cached": 0}
|
||||
assert again.asked == [] # dead ones marked, live ones cached: nothing to ask for
|
||||
|
||||
|
||||
def test_the_person_image_route(db, cache_dir, monkeypatch):
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
import app.state as state
|
||||
from app.main import app
|
||||
from app.routes.plex import playback
|
||||
|
||||
monkeypatch.setattr(state, "_configured_cache", True)
|
||||
monkeypatch.setattr(playback, "_require_session", lambda request: 1)
|
||||
client = TestClient(app)
|
||||
url = CDN + "face.jpg"
|
||||
small, original = plex_people._portrait_keys(url)
|
||||
|
||||
def get(u):
|
||||
return client.get("/api/plex/person-image", params={"u": u})
|
||||
|
||||
# Only a headshot-shaped url on the CDN host: no query variants of one file, no other host.
|
||||
for bad in (url + "?n=1", url + "#x", "https://example.com/people/a.jpg"):
|
||||
assert get(bad).status_code == 400, bad
|
||||
|
||||
# No Plex: the original from the CDN, kept under its own key (the job resizes it later).
|
||||
def not_configured(d):
|
||||
raise plex_people.PlexNotConfigured()
|
||||
|
||||
class _Cdn:
|
||||
def __init__(self, *a, **k):
|
||||
pass
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *a):
|
||||
return False
|
||||
|
||||
def get(self, u):
|
||||
return httpx.Response(
|
||||
200,
|
||||
content=b"original",
|
||||
headers={"content-type": "image/jpeg"},
|
||||
request=httpx.Request("GET", u),
|
||||
)
|
||||
|
||||
monkeypatch.setattr(playback, "PlexClient", not_configured)
|
||||
monkeypatch.setattr(plex_people.httpx, "Client", _Cdn)
|
||||
resp = get(url)
|
||||
assert (resp.status_code, resp.content) == (200, b"original")
|
||||
assert imgcache.has(original) and not imgcache.has(small)
|
||||
|
||||
# With Plex: resized, and it replaces the original.
|
||||
monkeypatch.setattr(playback, "PlexClient", lambda d: _Plex())
|
||||
imgcache.drop(original)
|
||||
resp = get(url)
|
||||
assert (resp.status_code, resp.content) == (200, b"small")
|
||||
assert imgcache.has(small)
|
||||
assert get(url).content == b"small" # the cache answers now
|
||||
|
||||
monkeypatch.setattr(playback, "PlexClient", lambda d: _Plex(down=True))
|
||||
assert get(CDN + "other.jpg").status_code == 502
|
||||
|
||||
|
||||
def test_the_people_job_runs_its_halves_without_a_tmdb_key(db, cache_dir, plex_on, monkeypatch):
|
||||
_faces(db, 1)
|
||||
monkeypatch.setattr(plex_tmdb.sysconfig, "get_str", lambda d, k: "")
|
||||
assert plex_tmdb.run_people_enrich(db) == {
|
||||
"bios": {"skipped": "tmdb_api_key not set"},
|
||||
"portraits": {"cached": 1},
|
||||
}
|
||||
|
||||
|
||||
def test_a_phase_count_is_a_change_worth_an_audit_row():
|
||||
|
||||
@@ -67,3 +67,22 @@ def test_a_failed_run_drops_the_previous_result(db):
|
||||
rss = _snapshot_job("rss_poll")
|
||||
assert rss["last_error"] == "boom"
|
||||
assert rss["result"] is None
|
||||
|
||||
|
||||
def test_a_job_runs_audit_row_carries_its_result_as_data(db):
|
||||
"""E30 review: the audit log names a run's result in the reader's language, as the inbox does —
|
||||
from data, not from the English one-liner (a nested phase there was a Python dict repr)."""
|
||||
from app.models import AuditLog, User
|
||||
|
||||
admin = User(email="auditrun@example.com")
|
||||
db.add(admin)
|
||||
db.commit()
|
||||
scheduler._audit_run(db, None, "people_enrich", "ok", "x", {"bios": {"enriched": 2}})
|
||||
scheduler._audit_run(db, admin.id, "rss_poll", "skipped", "sync paused", scheduler._PAUSED)
|
||||
scheduler._audit_run(db, admin.id, "rss_poll", "error", "boom", None)
|
||||
rows = db.execute(select(AuditLog).order_by(AuditLog.id)).scalars().all()
|
||||
assert [r.after for r in rows] == [
|
||||
{"status": "ok", "summary": "x", "result": {"bios": {"enriched": 2}}},
|
||||
{"status": "skipped", "summary": "sync paused", "result": {"skipped": "sync paused"}},
|
||||
{"status": "error", "summary": "boom"},
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user