From 689e30c4938403713c2eb6dd5ddc983dedb31e5b Mon Sep 17 00:00:00 2001 From: Thomas Howe Date: Thu, 24 Sep 2026 16:32:09 -0400 Subject: [PATCH] fix(api): accept empty-string dialog.url and party tel at ingest The CON-365 field validators reject dialog.url "" and parties[].tel "", which producers send for calls with no recording and for unknown numbers. Against one day of a production corpus (9,029 vCons) this bounced 420 (4.6%) with 422, all of which the previous release accepted. Treat the empty string as absent: skip the format check and keep the value unchanged, so stored vCons stay identical to what was posted. Malformed values ("not-a-url", "Anonymous") are still rejected. Co-Authored-By: Claude Opus 5.5 --- api/api.py | 6 ++++-- common/tests/test_ingest_empty_fields.py | 25 ++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 common/tests/test_ingest_empty_fields.py diff --git a/api/api.py b/api/api.py index 79ed9f5..7cbbd65 100644 --- a/api/api.py +++ b/api/api.py @@ -327,7 +327,8 @@ def duration_non_negative(cls, v): @field_validator("url") @classmethod def url_valid(cls, v): - if v is not None and not _URL_RE.match(v): + # Empty string means "no recording"; producers send it, treat it as absent. + if v and not _URL_RE.match(v): raise ValueError(f"url does not look like a valid URL: {v!r}") return v @@ -355,7 +356,8 @@ class PartyEntry(BaseModel): @field_validator("tel") @classmethod def tel_valid(cls, v): - if v is not None and not _TEL_RE.match(v): + # Empty string means "no number"; treat it as absent. "Anonymous" etc. still fail. + if v and not _TEL_RE.match(v): raise ValueError(f"tel has invalid format: {v!r}") return v diff --git a/common/tests/test_ingest_empty_fields.py b/common/tests/test_ingest_empty_fields.py new file mode 100644 index 0000000..b67aa5a --- /dev/null +++ b/common/tests/test_ingest_empty_fields.py @@ -0,0 +1,25 @@ +"""Empty-string url/tel are accepted as absent at ingest; malformed values still fail. + +Producers in the field send dialog.url "" for calls with no recording and party +tel "" for unknown numbers. Rejecting those bounced about 4.6% of one +production day's vCons with 422. +""" + +import pytest +from pydantic import ValidationError + +from api import DialogEntry, PartyEntry + + +def test_empty_url_and_tel_accepted_unchanged(): + assert DialogEntry(url="").url == "" + assert PartyEntry(tel="").tel == "" + + +@pytest.mark.parametrize("model,field,value", [ + (DialogEntry, "url", "not-a-url"), + (PartyEntry, "tel", "Anonymous"), +]) +def test_malformed_values_still_rejected(model, field, value): + with pytest.raises(ValidationError): + model(**{field: value})