merge: issue #3 fix
This commit is contained in:
@@ -934,6 +934,10 @@ def test_p3_points_and_band():
|
||||
br = eng.breakdown(1)
|
||||
assert {b['source'] for b in br} <= {'rubric', 'taste', 'agent'}
|
||||
assert br[0]['source'] == 'rubric'
|
||||
# regression (#3): a listing OUTSIDE the engine's pool (delisted
|
||||
# mid-batch, never swiped) must degrade quietly, not crash the render
|
||||
assert eng.points(9999) is None
|
||||
assert eng.breakdown(9999) == []
|
||||
|
||||
|
||||
def test_p3_payload_additions():
|
||||
@@ -959,6 +963,39 @@ def test_p3_payload_additions():
|
||||
shutil.rmtree(d, ignore_errors=True)
|
||||
|
||||
|
||||
def test_issue3_section_survives_midsection_delist():
|
||||
"""#3 regression: one card delisted after compose must not 500 the deck.
|
||||
|
||||
The section render asks the taste engine for a breakdown of every
|
||||
card. A listing that left the active pool mid-batch AND has no swipe
|
||||
history is not in the engine's by_id map; the unguarded lookup used
|
||||
to raise, killing GET /api/section and GET /api/listing/{id} with a
|
||||
500 (live incident 2026-11-15: section 57 + delisted listing 298).
|
||||
"""
|
||||
d, c, db_path = p3_env()
|
||||
try:
|
||||
sec = c.get('/api/section')
|
||||
assert sec.status_code == 200, sec.status_code
|
||||
body = sec.json()
|
||||
conn = _sq_conn(db_path)
|
||||
ev_ids = {r['listing_id'] for r in conn.execute(
|
||||
'SELECT DISTINCT listing_id FROM swipe_events')}
|
||||
it = next((x for x in body['items'] if x['id'] not in ev_ids), None)
|
||||
assert it is not None, 'fixture section has no unswiped card'
|
||||
conn.execute("UPDATE listings SET status='gone' WHERE id=?",
|
||||
(it['id'],))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
r = c.get('/api/section')
|
||||
assert r.status_code == 200, r.status_code
|
||||
assert it['id'] in [x['id'] for x in r.json()['items']]
|
||||
one = c.get('/api/listing/%d' % it['id'])
|
||||
assert one.status_code == 200, one.status_code
|
||||
assert one.json()['points_breakdown'] == []
|
||||
finally:
|
||||
shutil.rmtree(d, ignore_errors=True)
|
||||
|
||||
|
||||
# --- B1 checks (photo experience v2, UI-SPEC §11 / DATA-MODEL B1) -----------
|
||||
|
||||
def make_b1_item(title, imgs=None, img=None, price='$5,000', km='8000'):
|
||||
@@ -1751,6 +1788,8 @@ P3_API_TESTS = [
|
||||
('p3: agentpass mock determinism + live fallback', test_p3_agentpass_mock_and_fallback),
|
||||
('p3: points/band function known answers', test_p3_points_and_band),
|
||||
('p3: payload additions (list/listing/section)', test_p3_payload_additions),
|
||||
('p3: mid-section delist keeps the deck up (#3)',
|
||||
test_issue3_section_survives_midsection_delist),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -379,8 +379,13 @@ class TasteEngine:
|
||||
"""Source-labeled breakdown lines for the dossier."""
|
||||
row = self.by_id.get(listing_id)
|
||||
lines = []
|
||||
lines.append({'source': 'rubric', 'label': 'rubric fit',
|
||||
'value': int(row['score'] or 0)})
|
||||
# issue #3: a section can outlive an ad. A listing that left the
|
||||
# active pool mid-batch (and has no swipe history) is not in
|
||||
# by_id; its breakdown degrades to no-signal lines instead of a
|
||||
# TypeError that would 500 the whole section render.
|
||||
if row is not None:
|
||||
lines.append({'source': 'rubric', 'label': 'rubric fit',
|
||||
'value': int(row['score'] or 0)})
|
||||
for label, s, _d, _v in self.contributions(listing_id)[:3]:
|
||||
lines.append({'source': 'taste', 'label': label,
|
||||
'value': round(s, 1)})
|
||||
|
||||
Reference in New Issue
Block a user