fix(oc): reject duplicate recording titles per project (ENG-71)#83
Open
henokteixeira wants to merge 2 commits into
Open
fix(oc): reject duplicate recording titles per project (ENG-71)#83henokteixeira wants to merge 2 commits into
henokteixeira wants to merge 2 commits into
Conversation
POST /api/oc/recordings silently de-duplicated by (project_id, title), returning the existing recording; the client then uploaded to that old recording's deterministic GCS path, overwriting the original blob with no warning (silent data loss). create_recording and update_recording now reject a duplicate title in the same project with ConflictError (409). Match is trimmed, case-sensitive and exact; blank titles normalize to NULL and stay exempt; split-derived and archived-after-split rows are exempt. list_recordings gains a title filter for the client save-time pre-check. 🤖 Generated with [Nori](https://noriagentic.com) Co-Authored-By: Nori <contact@tilework.tech>
3 tasks
Adds a partial unique index on oc_recordings (project_id, title), exempting split-derived rows (split_from_id IS NOT NULL) and archived split parents (splitting_status = 'archived_after_split') — matching the service-layer check. NULL titles may repeat. This closes the concurrent-write race that the service-level check alone leaves open (two simultaneous creates of the same title in one project). create_recording and update_recording now also catch IntegrityError and raise ConflictError, so a race that slips past the pre-check returns a 409, not a 500. The index is mirrored on the ORM model (so the SQLite test DB enforces it and the in-place tests are real) and added in prod via Alembic migration 20260601_0001. That migration carries the pre-deploy duplicate-audit SQL in its docstring and must be run by someone with database access — it is not run or verified here. Part of ENG-71 (backend). 🤖 Generated with [Nori](https://noriagentic.com) Co-Authored-By: Nori <contact@tilework.tech>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
🤖 Generated with Nori
Fixes ENG-71 (Urgent).
POST /api/oc/recordingssilently de-duplicated by(project_id, title)— it returned the existing recording instead of creating a new one. The client then requested an upload URL for that oldrecording_id, whose GCS path is deterministic, so the PUT overwrote the original blob. The old recording kept its metadata but now held the new audio — silent, unrecoverable data loss.create_recordingandupdate_recordingnow reject a duplicate title in the same project withConflictError→ 409{detail, code: "CONFLICT"}(via the existing global handler — routers stay thin).NULLand are exempt. Split-derived (split_from_idset) and archived-after-split rows are exempt (they legitimately reuse a parent title).GET /api/oc/recordingsgains an optionaltitlefilter, used by the client's save-time pre-check.No schema change → no migration in this PR.
Known limitations / fast-follows
IntegrityError→ConflictErrorbackstop, gated on a pre-deploy audit (run against prod first; expect ~0 rows since the old dedup prevented exact duplicates):=is case-sensitive there) and relies on Postgres' default collation in prod; the fast-follow index makes it explicit.oral-collectorPR.Test Plan
uv run pytest tests/test_oc_recording_service.py— 38 passed (12 new ENG-71 cases: create/update reject, trim-on-store, case-sensitive, blank→NULL, per-project scoping allowed, split-child & archived-parent exemption, listtitlefilter)uv run pytest— 665 passed, 2 skippedruff check+ruff format --check+mypyclean