cauldron/tests/_testenv.py
Kayos 36aba73f66 v0.3 step 3+4: AI plan generator + /list shopping aggregation
- migrations 012 + 013: cauldron_meal_plan_slots + cauldron_pick_points
- db: list_plan_slots, save_plan_slots, delete_plan_slots, mark_plan_generated,
  clear_plan_generated, award_pick_points, enrich_plan_with_slots; scoreboard
  extended with points (sum from pick_points) and weeks_locked alias
- forge.generate_plan: sonnet prompt builds 7-day plan respecting picks,
  validates slot count + day uniqueness + slug-in-pool, fills picker_subs
  from ground-truth picks (model output is advisory)
- POST /api/plan/generate: race-safe (existing slots → 409 with plan),
  lock-aware (locked → 409), idempotent
- POST /api/plan/regenerate: re-roll for the original generator, gated by
  ownership + lock; wipes slots + pick_points then re-runs generate
- plan.html: generate CTA + 7 day cards with picker chips + AI reason +
  re-roll button (generator-only, pre-lock); scoreboard now shows points + wins
- /list: pulls plan slots, queries Mealie for ingredients, runs aggregator,
  renders 48px-tall checkbox shopping list with localStorage state per plan_id
- tests: 13 new tests across forge.generate_plan + /api/plan/generate routes
  + /list view + scoreboard SQL inspection. conftest+_testenv stub
  pymysql/oidc/foods at import time so tests run against module-level app
  without a live DB. Both pytest and `unittest discover` paths green (27/27).

Defers: bulk sterilizer admin (A1), foods dedupe (A2), Mealie shopping-list-
export (button rendered but disabled). 7-slot count is fixed at the
endpoint (no UI for slot-count selection yet).

Spec: memory/spec-cauldron-v0.3.md
2026-04-29 06:26:54 -07:00

85 lines
2.7 KiB
Python

"""Test environment bootstrap.
Cauldron's server module instantiates a DB + OAuth client + Mealie client at
import time, so tests must stub those before importing cauldron.server.
This module performs that stubbing exactly once (idempotent — safe to import
multiple times). Both pytest's conftest.py and direct `unittest discover`
runs route through here.
"""
import os
from unittest.mock import MagicMock, patch
from cryptography.fernet import Fernet
_BOOTSTRAPPED = False
def _stub_env() -> None:
os.environ.setdefault("SECRET_KEY", "test-secret")
os.environ.setdefault("MEALIE_BASE_URL", "http://mealie.test")
os.environ.setdefault("MEALIE_API_TOKEN", "mealie-test-token")
os.environ.setdefault("CLAWDFORGE_URL", "http://forge.test")
os.environ.setdefault("CLAWDFORGE_TOKEN", "forge-test-token")
os.environ.setdefault("ADMIN_BEARER", "admin-test-bearer")
os.environ.setdefault("OIDC_ISSUER", "http://authentik.test/application/o/cauldron")
os.environ.setdefault("OIDC_CLIENT_ID", "test-client")
os.environ.setdefault("OIDC_CLIENT_SECRET", "test-secret-client")
os.environ.setdefault("OIDC_REDIRECT_URI", "http://localhost/auth/callback")
os.environ.setdefault("DB_HOST", "localhost")
os.environ.setdefault("DB_NAME", "cauldron_test")
os.environ.setdefault("DB_USER", "cauldron")
os.environ.setdefault("DB_PASSWORD", "test")
os.environ.setdefault("CAULDRON_FERNET_KEY", Fernet.generate_key().decode())
class _FakeCursor:
def __init__(self):
self.lastrowid = 0
self.rowcount = 0
def execute(self, *a, **kw): pass
def fetchone(self): return None
def fetchall(self): return []
def __enter__(self): return self
def __exit__(self, *a): return False
class _FakeConn:
def __init__(self, *a, **kw): pass
def cursor(self): return _FakeCursor()
def commit(self): pass
def rollback(self): pass
def close(self): pass
def bootstrap() -> None:
"""Apply env + import-time monkey patches. Idempotent."""
global _BOOTSTRAPPED
if _BOOTSTRAPPED:
return
_stub_env()
# pymysql.connect → no-op fake
patch("pymysql.connect", _FakeConn).start()
# Pre-import dotted modules so the next patches resolve
import cauldron.db # noqa: F401
import cauldron.oidc # noqa: F401
import cauldron.foods # noqa: F401
# Skip migrations
patch("cauldron.db.DB.migrate", lambda self: []).start()
# Skip OIDC metadata fetch
oauth_obj = MagicMock()
oauth_obj.cauldron = MagicMock()
patch("cauldron.oidc.init_oauth", lambda *a, **kw: oauth_obj).start()
# Skip foods seed loader
patch("cauldron.foods.load_seed_if_empty", lambda db: 0).start()
_BOOTSTRAPPED = True
bootstrap()