fix(cart): report cart failures without a traceback

A closed window or a failed cart request during the fill now prints a short failure and the Added/Failed/Skipped summary instead of crashing.
This commit is contained in:
Adam Moussa 2026-09-26 16:21:49 -04:00
parent dce33918b7
commit ca04a117f5
No known key found for this signature in database
2 changed files with 136 additions and 59 deletions

View file

@ -104,65 +104,7 @@ def _fill(plan: FillPlan, menu_url: str) -> int:
return 1
page = browser.new_page()
try:
page.goto(menu_url, wait_until="domcontentloaded", timeout=60000)
cart = _cart_request(page, "GET", "/api/cart")
existing = cart_lines(cart)
if existing:
print("The cart already has items:")
for line in existing:
print(f" {line.name} x{line.quantity}")
if not _confirm_clear():
print("Exiting without adding.")
return 1
cleared = _cart_request(page, "POST", "/api/cart/clear", {})
if cart_lines(cleared):
print(
"The cart still has items after clear. Exiting without adding."
)
return 1
failed: list[str] = []
posted = []
for item in plan.adds:
try:
_cart_request(
page,
"POST",
"/api/cart/add",
{
"uuid": item.uuid,
"quantity": item.quantity,
"properties": None,
},
)
except RuntimeError as exc:
failed.append(f"{item.name}: {exc}")
print(f"Failed: {item.name}: {exc}")
continue
posted.append(item)
added = 0
try:
current = cart_lines(_cart_request(page, "GET", "/api/cart"))
except RuntimeError as exc:
failed.append(f"Could not read the cart: {exc}")
print(f"Failed: Could not read the cart: {exc}")
else:
for item in posted:
found = quantity_for(current, item.uuid)
if found != item.quantity:
message = (
f"{item.name}: requested {item.quantity}, cart has {found}"
)
failed.append(message)
print(f"Failed: {message}")
else:
added += 1
origin = _origin(page.url or menu_url)
page.goto(f"{origin}/cart", wait_until="domcontentloaded", timeout=60000)
print(f"Added {added}. Failed {len(failed)}. Skipped {len(plan.skipped)}.")
_wait_for_review(page)
return 1 if failed or plan.skipped else 0
return _fill_page(page, plan, menu_url)
finally:
try:
browser.close()
@ -170,6 +112,73 @@ def _fill(plan: FillPlan, menu_url: str) -> int:
pass
def _fill_page(page, plan: FillPlan, menu_url: str) -> int:
failed: list[str] = []
added = 0
ready = False
try:
page.goto(menu_url, wait_until="domcontentloaded", timeout=60000)
cart = _cart_request(page, "GET", "/api/cart")
existing = cart_lines(cart)
if existing:
print("The cart already has items:")
for line in existing:
print(f" {line.name} x{line.quantity}")
if not _confirm_clear():
print("Exiting without adding.")
return 1
cleared = _cart_request(page, "POST", "/api/cart/clear", {})
if cart_lines(cleared):
print("The cart still has items after clear. Exiting without adding.")
return 1
posted = []
for item in plan.adds:
try:
_cart_request(
page,
"POST",
"/api/cart/add",
{
"uuid": item.uuid,
"quantity": item.quantity,
"properties": None,
},
)
except RuntimeError as exc:
failed.append(f"{item.name}: {exc}")
print(f"Failed: {item.name}: {exc}")
continue
posted.append(item)
try:
current = cart_lines(_cart_request(page, "GET", "/api/cart"))
except RuntimeError as exc:
failed.append(f"Could not read the cart: {exc}")
print(f"Failed: Could not read the cart: {exc}")
else:
for item in posted:
found = quantity_for(current, item.uuid)
if found != item.quantity:
message = (
f"{item.name}: requested {item.quantity}, cart has {found}"
)
failed.append(message)
print(f"Failed: {message}")
else:
added += 1
origin = _origin(page.url or menu_url)
page.goto(f"{origin}/cart", wait_until="domcontentloaded", timeout=60000)
ready = True
except (RuntimeError, PlaywrightError) as exc:
failed.append(str(exc))
print(f"Failed: {exc}")
print(f"Added {added}. Failed {len(failed)}. Skipped {len(plan.skipped)}.")
if ready:
_wait_for_review(page)
return 1 if failed or plan.skipped else 0
def _cart_request(page, method: str, path: str, body: object | None = None) -> object:
result = page.evaluate(_CART_JS, {"method": method, "path": path, "body": body})
if not isinstance(result, dict) or not result.get("ok"):

View file

@ -1,8 +1,19 @@
"""CSV parsing and menu matching for the Redefine cart filler."""
import importlib.util
from pathlib import Path
from playwright.sync_api import Error as PlaywrightError
from scraper.fill_cart import build_plan, cart_lines, error_text, quantity_for
from scraper.parse_menu import extract_catalog_products
_SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "fill_redefine_cart.py"
_spec = importlib.util.spec_from_file_location("fill_redefine_cart", _SCRIPT)
fill_redefine_cart = importlib.util.module_from_spec(_spec)
assert _spec.loader is not None
_spec.loader.exec_module(fill_redefine_cart)
PRODUCTS = [
{
"name": "Korean Steak Bowl",
@ -123,6 +134,63 @@ def test_cart_lines_use_the_product_uuid_and_quantity():
assert quantity_for(lines, "line-1") == 0
class _CartPage:
def __init__(self, responses):
self._responses = list(responses)
def goto(self, url, **kwargs):
return None
def evaluate(self, script, payload):
response = self._responses.pop(0)
if isinstance(response, Exception):
raise response
return response
def test_fill_page_prints_a_summary_when_the_window_closes_mid_add(capsys):
plan = build_plan("Item,Quantity\nKorean Steak Bowl,1\n", PRODUCTS)
page = _CartPage(
[
{"ok": True, "status": 200, "data": {"items": []}},
PlaywrightError("Target page, context or browser has been closed"),
]
)
code = fill_redefine_cart._fill_page(
page, plan, "https://www.redefinemeals.com/menu"
)
output = capsys.readouterr().out
assert code == 1
assert "Traceback" not in output
assert "Added 0. Failed 1. Skipped 0." in output
assert "Cart is ready" not in output
def test_fill_page_prints_a_summary_when_the_first_cart_read_fails(capsys):
plan = build_plan("Item,Quantity\nKorean Steak Bowl,1\n", PRODUCTS)
page = _CartPage(
[
{
"ok": False,
"status": 500,
"data": {"message": "cart unavailable", "trace": [{"file": "x"}]},
}
]
)
code = fill_redefine_cart._fill_page(
page, plan, "https://www.redefinemeals.com/menu"
)
output = capsys.readouterr().out
assert code == 1
assert "HTTP 500: cart unavailable" in output
assert "x" not in output.split("Failed:", 1)[-1]
assert "Added 0. Failed 1. Skipped 0." in output
def test_error_text_keeps_the_message_and_drops_the_trace():
body = {
"message": "Product is not available.",