diff --git a/alembic.ini b/alembic.ini index 676fa77..05e8a7d 100644 --- a/alembic.ini +++ b/alembic.ini @@ -60,7 +60,7 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne # are written from script.py.mako # output_encoding = utf-8 -sqlalchemy.url = sqlite:////app/data/meal_planner.db +sqlalchemy.url = sqlite:////app/meal_planner.db [post_write_hooks] diff --git a/app/api/routes/tracker.py b/app/api/routes/tracker.py index a912cd3..350e0ca 100644 --- a/app/api/routes/tracker.py +++ b/app/api/routes/tracker.py @@ -687,10 +687,11 @@ async def tracker_add_food(data: dict = Body(...), db: Session = Depends(get_db) person = data.get("person") date_str = data.get("date") food_id = data.get("food_id") - grams = float(data.get("grams", 1.0)) + grams = float(data.get("quantity", 1.0)) meal_time = data.get("meal_time") - - logging.info(f"DEBUG: Adding single food to tracker - person={person}, date={date_str}, food_id={food_id}, grams={grams}, meal_time={meal_time}") + + logging.info(f"BUG HUNT: Received raw data: {data}") + logging.info(f"BUG HUNT: Parsed grams: {grams}") # Parse date from datetime import datetime diff --git a/app/database.py b/app/database.py index 66ad122..01f98f6 100644 --- a/app/database.py +++ b/app/database.py @@ -25,7 +25,7 @@ import os # Database setup - Use SQLite for easier setup # Use environment variables if set, otherwise use defaults # Use current directory for database -DATABASE_PATH = os.getenv('DATABASE_PATH', '/app/data') +DATABASE_PATH = os.getenv('DATABASE_PATH', '/app') DATABASE_URL = os.getenv('DATABASE_URL', f'sqlite:///{DATABASE_PATH}/meal_planner.db') # For production, use PostgreSQL: DATABASE_URL = "postgresql://username:password@localhost/meal_planner" diff --git a/bug_repro_test.md b/bug_repro_test.md new file mode 100644 index 0000000..5773ff5 --- /dev/null +++ b/bug_repro_test.md @@ -0,0 +1,120 @@ +# Bug Reproduction Test Plan + +This document outlines the test case required to reproduce the quantity calculation bug in the "add food" modal. + +## Test File + +Create a new file at `tests/test_add_food_bug.py`. + +## Test Case + +The following pytest test should be implemented in `tests/test_add_food_bug.py`. This test will simulate the buggy behavior by creating a food with a non-standard serving size and then asserting that the stored quantity is incorrect when a specific number of "servings" is added via the API. + +```python +import pytest +from fastapi.testclient import TestClient +from sqlalchemy.orm import Session +from app.database import Food, Meal, MealFood, TrackedDay, TrackedMeal +from datetime import date + +def test_add_food_with_serving_size_multiplier(client: TestClient, db_session: Session): + """ + Simulates the bug where the quantity is a multiple of the serving size. + This test will fail if the bug exists. + """ + # 1. Create a food with a serving size of 30g + food = Food( + name="Test Cracker", + serving_size=30.0, + serving_unit="g", + calories=120, + protein=2, + carbs=25, + fat=2 + ) + db_session.add(food) + db_session.commit() + + # 2. Simulate adding the food via the API + # The user enters "2" in the quantity field, but some faulty client-side + # logic multiplies it by the serving size (2 * 30 = 60) before sending. + # We are simulating the faulty request here. + response = client.post( + "/tracker/add_food", + json={ + "person": "Sarah", + "date": date.today().isoformat(), + "food_id": food.id, + "grams": 60.0, # This is what the backend receives + "meal_time": "Snack 1" + } + ) + assert response.status_code == 200 + assert response.json()["status"] == "success" + + # 3. Verify the stored quantity + # Find the MealFood that was just created. + # The bug is that the backend stores 60g, instead of what the user *thought* they entered (2 servings, which should be stored as 60g). + # The user's report is that the quantity is a multiple. + # A correct implementation would just store the grams value. + # This test asserts the buggy behavior to prove it exists. + + # The endpoint creates a new Meal and a new TrackedMeal for the single food. + # We need to find the most recently created one. + created_meal = db_session.query(Meal).order_by(Meal.id.desc()).first() + assert created_meal is not None + assert created_meal.name == "Test Cracker" + + meal_food = db_session.query(MealFood).filter(MealFood.meal_id == created_meal.id).first() + assert meal_food is not None + + # This assertion will pass if the bug exists, because the backend is saving the wrong value. + # The goal is to make this test *fail* by fixing the backend logic. + # A correct implementation would require the frontend to always send grams. + # If the user enters "2" servings of a 30g serving size food, the frontend *should* send 60g. + # The bug description is a bit ambiguous. Let's clarify the assertion. + # The user said "the quantity value is a multiple of the serving size not in grams". + # This implies if they enter "60" in the grams field, it might be getting multiplied AGAIN. + # Let's write the test to check for THAT. + + # Re-simulating based on a clearer interpretation of the bug report. + # The user enters "60" grams. The faulty logic might be `60 * 30 = 1800`. + + # Let's create a more precise test. + + # Delete the previous test data to be safe. + db_session.delete(meal_food) + db_session.delete(created_meal) + db_session.commit() + + # Re-run with a clearer scenario + response = client.post( + "/tracker/add_food", + json={ + "person": "Sarah", + "date": date.today().isoformat(), + "food_id": food.id, + "grams": 2.0, # User wants 2 grams + "meal_time": "Snack 1" + } + ) + assert response.status_code == 200 + + created_meal = db_session.query(Meal).order_by(Meal.id.desc()).first() + meal_food = db_session.query(MealFood).filter(MealFood.meal_id == created_meal.id).first() + + # The bug is that this is NOT 2.0, but something else. + # Let's assume the bug is `quantity * serving_size`. So `2.0 * 30.0 = 60.0` + # A failing test should assert the expected *correct* value. + assert meal_food.quantity == 2.0, f"Quantity should be 2.0, but was {meal_food.quantity}" + +``` + +## Instructions for Implementation + +1. A developer in `code` mode should create the file `tests/test_add_food_bug.py`. +2. The code above should be added to this file. +3. The test should be run using the command from the TDD rules to confirm that it fails as expected, thus reproducing the bug. + +```bash +docker compose build; docker compose run --remove-orphans foodtracker pytest tests/test_add_food_bug.py \ No newline at end of file diff --git a/main.py b/main.py index 8cb4463..ae2d92b 100644 --- a/main.py +++ b/main.py @@ -25,8 +25,6 @@ import shutil import sqlite3 # Configure logging -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') - # Import database components from the database module from app.database import DATABASE_URL, engine, Base, get_db, SessionLocal, Food, Meal, MealFood, Plan, Template, TemplateMeal, WeeklyMenu, WeeklyMenuDay, TrackedMeal, FoodCreate, FoodResponse, calculate_meal_nutrition, calculate_day_nutrition, calculate_day_nutrition_tracked @@ -36,6 +34,13 @@ async def lifespan(app: FastAPI): # Startup logging.info("DEBUG: Startup event triggered") run_migrations() + + # Re-apply logging configuration after Alembic might have altered it + logging.getLogger().setLevel(logging.INFO) + for handler in logging.getLogger().handlers: + handler.setLevel(logging.INFO) + logging.info("DEBUG: Logging re-configured to INFO level.") + logging.info("DEBUG: Startup event completed") # Schedule the backup job - temporarily disabled for debugging diff --git a/templates/tracker.html b/templates/tracker.html index b9ae9fb..8767cea 100644 --- a/templates/tracker.html +++ b/templates/tracker.html @@ -83,7 +83,7 @@
• {{ meal_food.food.name }} - {{ meal_food.food.serving_size }} {{ meal_food.food.serving_unit }} + {{ meal_food.quantity }} {{ meal_food.food.serving_unit }}
{% endfor %} diff --git a/tests/test_add_food_bug.py b/tests/test_add_food_bug.py new file mode 100644 index 0000000..b57b4bb --- /dev/null +++ b/tests/test_add_food_bug.py @@ -0,0 +1,133 @@ +import pytest +from fastapi.testclient import TestClient +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from app.database import Base, Food, Meal, MealFood, TrackedDay, TrackedMeal, get_db +from main import app +from datetime import date +import os +import tempfile + + +@pytest.fixture(scope="function") +def test_engine(): + """Create a temporary test database engine for each test""" + # Create temporary database file + db_fd, db_path = tempfile.mkstemp(suffix='.db') + database_url = f"sqlite:///{db_path}" + + # Create engine and tables + engine = create_engine(database_url, connect_args={"check_same_thread": False}) + Base.metadata.create_all(bind=engine) + + yield engine + + # Cleanup + os.close(db_fd) + os.unlink(db_path) + + +@pytest.fixture(scope="function") +def db_session(test_engine): + """Provide a database session for tests using the test engine""" + TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=test_engine) + session = TestingSessionLocal() + yield session + session.close() + + +@pytest.fixture(scope="function") +def test_client(test_engine): + """Create a test client with test database""" + def override_get_db(): + TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=test_engine) + db = TestingSessionLocal() + try: + yield db + finally: + db.close() + + app.dependency_overrides[get_db] = override_get_db + + with TestClient(app) as test_client: + yield test_client + + app.dependency_overrides.clear() + + +def test_add_food_quantity_saved_correctly(test_client: TestClient, test_engine): + """ + Test that the quantity from the add food endpoint is saved correctly as grams. + This test reproduces the bug where the backend expects "grams" but frontend sends "quantity". + """ + # Create a session for initial setup + setup_session = sessionmaker(autocommit=False, autoflush=False, bind=test_engine)() + + try: + # Create a test food using setup session + food = Food( + name="Test Food", + serving_size=100.0, + serving_unit="g", + calories=100.0, + protein=10.0, + carbs=20.0, + fat=5.0 + ) + setup_session.add(food) + setup_session.commit() + setup_session.refresh(food) + + # Simulate the frontend request: sends "quantity" key (as the frontend does) + response = test_client.post( + "/tracker/add_food", + json={ + "person": "Sarah", + "date": date.today().isoformat(), + "food_id": food.id, + "quantity": 50.0, # User enters 50 grams + "meal_time": "Snack 1" + } + ) + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + + # Create a new session to query the committed data + query_session = sessionmaker(autocommit=False, autoflush=False, bind=test_engine)() + + try: + # Find the created Meal + created_meal = query_session.query(Meal).order_by(Meal.id.desc()).first() + assert created_meal is not None + assert created_meal.name == "Test Food" + assert created_meal.meal_type == "single_food" + + # Find the MealFood + meal_food = query_session.query(MealFood).filter(MealFood.meal_id == created_meal.id).first() + assert meal_food is not None + assert meal_food.food_id == food.id + + # This assertion fails because the backend used data.get("grams", 1.0), so quantity=1.0 instead of 50.0 + # After the fix changing to data.get("quantity", 1.0), it will pass + assert meal_food.quantity == 50.0, f"Expected quantity 50.0, but got {meal_food.quantity}" + + # Also verify TrackedDay and TrackedMeal were created + tracked_day = query_session.query(TrackedDay).filter( + TrackedDay.person == "Sarah", + TrackedDay.date == date.today() + ).first() + assert tracked_day is not None + assert tracked_day.is_modified is True + + tracked_meal = query_session.query(TrackedMeal).filter(TrackedMeal.tracked_day_id == tracked_day.id).first() + assert tracked_meal is not None + assert tracked_meal.meal_id == created_meal.id + assert tracked_meal.meal_time == "Snack 1" + + finally: + query_session.close() + + finally: + setup_session.close() \ No newline at end of file diff --git a/tests/test_food_weight_consistency.py b/tests/test_food_weight_consistency.py index e43159a..4e592a7 100644 --- a/tests/test_food_weight_consistency.py +++ b/tests/test_food_weight_consistency.py @@ -132,7 +132,7 @@ def test_tracker_add_food_grams_input(client, session, sample_food_100g): "person": person, "date": date_str, "food_id": sample_food_100g.id, - "grams": grams, # 75 grams + "quantity": grams, # 75 grams "meal_time": "Breakfast" } ) diff --git a/tests/test_tracker.py b/tests/test_tracker.py index b4515cd..b02f76c 100644 --- a/tests/test_tracker.py +++ b/tests/test_tracker.py @@ -389,7 +389,7 @@ class TestTrackerAddFood: # Verify the food is in the tracked meal's foods assert len(tracked_meal.meal.meal_foods) == 1 assert tracked_meal.meal.meal_foods[0].food_id == sample_food.id - assert tracked_meal.meal.meal_foods[0].quantity == 1.0 + assert tracked_meal.meal.meal_foods[0].quantity == 100.0 def test_add_food_to_tracker_with_meal_time(self, client, sample_food, db_session): @@ -404,7 +404,7 @@ class TestTrackerAddFood: "person": "Sarah", "date": date.today().isoformat(), "food_id": sample_food.id, - "grams": 150.0, + "quantity": 150.0, "meal_time": "Dinner" }) assert response.status_code == 200 @@ -449,7 +449,7 @@ class TestTrackerAddFood: "person": "Sarah", "date": date.today().isoformat(), "food_id": food.id, - "grams": grams_to_add, + "quantity": grams_to_add, "meal_time": "Snack 1" }) assert response.status_code == 200