fix(measurements): value NaN/Inf/scala e coerenza quota/versione/run (#5, #20)

#20 - models/api/measurement.py: value accettava NaN/Infinity/fuori scala,
ogni confronto tornava falso ("pass") e poi l'INSERT falliva. Il controllo
sposta in measurement_service._validate_measured_value (math.isfinite +
|value|<1e6): un Field pydantic (allow_inf_nan/gt/lt) rifiuta la stessa cosa
ma FastAPI, costruendo l'errore 422, riecheggia il NaN nel campo "input" e
Starlette lo nega con allow_nan=False, producendo un 500 al posto del 422 -
verificato con una chiamata reale, non solo a livello di modello.
calculate_pass_fail e save_measurement arrotondano value a 6 decimali (stessa
scala di DECIMAL(12,6)) prima di calcolare e salvare, cosi' il verdetto e il
valore persistito non divergono mai.

#5 - measurement_service.save_measurement: prima dell'insert verifica che la
quota appartenga a un task della version_id dichiarata (422 altrimenti), e se
production_run_id e' dato usa la nuova validate_run_for_version(db, run_id,
version_id): il run deve esistere (404), essere della stessa ricetta della
versione (422), essere misurabile - riusa production_service._require_measurable
(409 se chiuso o in pausa). Import locale per evitare un ciclo con
production_service, che gia' importa measurement_service.

Test: gia' presenti in test_measurements.py (TestMeasurementValueBounds,
TestSaveMeasurementCoherence) - finiti nel commit 0dce229 di un altro agente
che lavorava in parallelo sullo stesso file (export CSV, #22); confermato con
`git show 0dce229 -- test_measurements.py`, contenuto intatto.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-21 22:47:58 +02:00
parent befdce5e6a
commit 7b015078e9
2 changed files with 97 additions and 0 deletions
+7
View File
@@ -9,6 +9,13 @@ class MeasurementCreate(BaseModel):
"""Schema for creating a single measurement."""
subtask_id: int
version_id: int
# Finiteness and the DECIMAL(12,6) scale bound are enforced in
# measurement_service.save_measurement, not with a Field constraint here:
# a NaN/Infinity that fails a pydantic Field constraint gets echoed back
# in the validation error's "input", and FastAPI's default error response
# then crashes with a 500 trying to JSON-encode that same NaN/Infinity
# (json.dumps(allow_nan=False) in Starlette's JSONResponse). A plain
# HTTPException raised after parsing sidesteps that.
value: float
lot_number: Optional[str] = Field(None, max_length=100)
serial_number: Optional[str] = Field(None, max_length=100)