"""#40: after a reload, the quotes already measured in the cycle come back. The cycle has no column of its own: it opens at the latest start, cycle_completed or remeasure-of-this-task event of the run, and a task_measured of the task after that boundary means the task is closed in this cycle. """ from datetime import datetime, timedelta from httpx import AsyncClient from sqlalchemy import select from src.backend.models.orm.measurement import Measurement from src.backend.models.orm.production import ProductionEvent, ProductionRun from src.backend.models.orm.recipe import RecipeVersion from src.backend.models.orm.station import Station from src.backend.models.orm.task import RecipeSubtask, RecipeTask from src.backend.tests.conftest import auth_headers, create_test_recipe T0 = datetime(2026, 10, 1, 8, 0, 0) async def _setup(db, user): recipe = await create_test_recipe(db, user.id) version = (await db.execute( select(RecipeVersion).where( RecipeVersion.recipe_id == recipe.id, RecipeVersion.is_current == True, # noqa: E712 ) )).scalar_one() task = (await db.execute( select(RecipeTask).where(RecipeTask.version_id == version.id) )).scalars().first() first = (await db.execute( select(RecipeSubtask).where(RecipeSubtask.task_id == task.id) )).scalars().first() second = RecipeSubtask( task_id=task.id, marker_number=2, description="Quota 2", nominal=10.0, utl=10.5, uwl=10.3, lwl=9.7, ltl=9.5, unit="mm", ) db.add(second) station = Station(code="ST-CYC", name="St", active=True, created_by=user.id) db.add(station) await db.commit() run = ProductionRun( station_id=station.id, recipe_id=recipe.id, version_id=version.id, operator_id=user.id, status="running", active_station_id=station.id, ) db.add(run) await db.commit() return run, version, task, first, second async def _event(db, run, user, kind, minutes, task_id=None): db.add(ProductionEvent( run_id=run.id, event_type=kind, user_id=user.id, task_id=task_id, created_at=T0 + timedelta(minutes=minutes), )) await db.commit() async def _meas(db, run, version, subtask, user, minutes, value=10.0): db.add(Measurement( subtask_id=subtask.id, version_id=version.id, measured_by=user.id, value=value, pass_fail="pass", production_run_id=run.id, measured_at=T0 + timedelta(minutes=minutes), )) await db.commit() async def _get(client, user, run, task): resp = await client.get( "/api/measurements/current-cycle", headers=auth_headers(user), params={"production_run_id": run.id, "task_id": task.id}, ) assert resp.status_code == 200 return resp.json()["measurements"] async def test_only_second_cycle_after_cycle_completed( client: AsyncClient, db_session, measurement_tec_user, ): u = measurement_tec_user run, version, task, q1, q2 = await _setup(db_session, u) await _event(db_session, run, u, "start", 0) await _meas(db_session, run, version, q1, u, 1, 10.1) await _meas(db_session, run, version, q2, u, 2, 10.2) await _event(db_session, run, u, "cycle_completed", 3) await _meas(db_session, run, version, q1, u, 4, 10.3) rows = await _get(client, u, run, task) assert [(r["subtask_id"], r["value"]) for r in rows] == [(q1.id, 10.3)] assert rows[0]["pass_fail"] == "pass" async def test_last_reading_of_a_quote_wins( client: AsyncClient, db_session, measurement_tec_user, ): u = measurement_tec_user run, version, task, q1, q2 = await _setup(db_session, u) await _event(db_session, run, u, "start", 0) await _meas(db_session, run, version, q1, u, 1, 10.1) await _meas(db_session, run, version, q1, u, 2, 10.4) rows = await _get(client, u, run, task) assert [(r["subtask_id"], r["value"]) for r in rows] == [(q1.id, 10.4)] async def test_remeasure_of_the_task_empties_the_list( client: AsyncClient, db_session, measurement_tec_user, ): u = measurement_tec_user run, version, task, q1, q2 = await _setup(db_session, u) await _event(db_session, run, u, "start", 0) await _meas(db_session, run, version, q1, u, 1) await _event(db_session, run, u, "remeasure", 2, task_id=task.id) assert await _get(client, u, run, task) == [] async def test_remeasure_of_another_task_does_not_move_the_boundary( client: AsyncClient, db_session, measurement_tec_user, ): u = measurement_tec_user run, version, task, q1, q2 = await _setup(db_session, u) other = RecipeTask( version_id=version.id, title="Altro", task_type=task.task_type, order_index=9, ) db_session.add(other) await db_session.commit() await _event(db_session, run, u, "start", 0) await _meas(db_session, run, version, q1, u, 1) await _event(db_session, run, u, "remeasure", 2, task_id=other.id) assert len(await _get(client, u, run, task)) == 1 async def test_task_measured_after_boundary_means_closed( client: AsyncClient, db_session, measurement_tec_user, ): u = measurement_tec_user run, version, task, q1, q2 = await _setup(db_session, u) await _event(db_session, run, u, "start", 0) await _meas(db_session, run, version, q1, u, 1) await _meas(db_session, run, version, q2, u, 2) await _event(db_session, run, u, "task_measured", 3, task_id=task.id) assert await _get(client, u, run, task) == [] async def test_task_measured_before_boundary_does_not_close_next_cycle( client: AsyncClient, db_session, measurement_tec_user, ): u = measurement_tec_user run, version, task, q1, q2 = await _setup(db_session, u) await _event(db_session, run, u, "start", 0) await _event(db_session, run, u, "task_measured", 1, task_id=task.id) await _event(db_session, run, u, "cycle_completed", 2) await _meas(db_session, run, version, q1, u, 3) assert len(await _get(client, u, run, task)) == 1 async def test_permissions(client: AsyncClient, db_session, measurement_tec_user, maker_user): run, _v, task, _q1, _q2 = await _setup(db_session, measurement_tec_user) params = {"production_run_id": run.id, "task_id": task.id} no_key = await client.get("/api/measurements/current-cycle", params=params) assert no_key.status_code in (401, 403) forbidden = await client.get( "/api/measurements/current-cycle", headers=auth_headers(maker_user), params=params, ) assert forbidden.status_code == 403