Files
periscope/tests/test_finding_schema.py
T
micheleandCursor e4fd6c3849 Ship Sprint 0 CAD foundations so findings, KiCad hierarchy, BOM match, eval, and optional PCB ingest have a stable contract.
Extend Finding with rule_id/net/pins, flatten multi-sheet .kicad_sch, flag MPN mismatches, score simple_project, and parse .kicad_pcb without running layout DRC.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-10 21:54:37 +02:00

85 lines
2.3 KiB
Python

"""Finding schema — optional CAD/plugin fields, backward compatible.
Favor: legacy JSON still validates; new fields round-trip; pin-mux
fills rule_id + net + pins.
Against: invalid status rejected; pins must be a list; extra junk status
does not silently coerce.
"""
from __future__ import annotations
import json
import pytest
from pydantic import ValidationError
from backend.pinscopex.models import Finding
def test_legacy_json_without_new_fields_still_validates():
raw = {
"designator": "U3",
"mpn": "MSPM0G3507SPTR",
"finding": "Missing decoupling",
"why": "Datasheet requires 100n close to VDD",
"status": "ERROR",
"reference": "p.12",
}
f = Finding.model_validate(raw)
assert f.net is None
assert f.pins == []
assert f.rule_id is None
assert f.cad_sheet is None
assert f.cad_uuid is None
assert f.variant is None
def test_new_fields_round_trip_json():
f = Finding(
designator="U1",
mpn="SPX3819",
finding="Cin too far",
status="WARNING",
net="VIN",
pins=["U1.1", "C1.1"],
rule_id="PS-DEC-001",
cad_sheet="power.kicad_sch",
cad_uuid="aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
variant="DNP",
)
dumped = json.loads(f.model_dump_json())
again = Finding.model_validate(dumped)
assert again.net == "VIN"
assert again.pins == ["U1.1", "C1.1"]
assert again.rule_id == "PS-DEC-001"
assert again.cad_sheet == "power.kicad_sch"
assert again.cad_uuid == "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
assert again.variant == "DNP"
def test_unknown_extra_keys_do_not_break_legacy_payloads():
f = Finding.model_validate(
{
"designator": "R1",
"finding": "ok",
"status": "INFO",
"future_field_from_old_report": True,
}
)
assert f.designator == "R1"
def test_invalid_status_is_rejected():
with pytest.raises(ValidationError):
Finding(designator="U1", finding="x", status="error")
def test_pins_must_be_a_list_not_a_string():
with pytest.raises(ValidationError):
Finding(designator="U1", finding="x", status="INFO", pins="U1.1")
def test_status_ok_is_not_silently_accepted():
with pytest.raises(ValidationError):
Finding(designator="U1", finding="x", status="OK")