Commit d761b72
authored
Allow completed conversations to enter merging so conversation merge works (#9955)
POST /v1/conversations/merge has two gates that disagree, so every merge request fails.
1. routers/conversations.py:1279 calls validate_merge_compatibility, which rejects the
request with a 400 unless EVERY conversation has status == 'completed'
(merge_conversations.py:124-128).
2. routers/conversations.py:1285 then calls lifecycle_service.begin_merge on each
conversation, which is transition(..., merging) with expected=None. _STATUS_TRANSITIONS
has no 'completed' key, so _STATUS_TRANSITIONS.get('completed', set()) is empty and
transition raises LifecycleTransitionError at lifecycle.py:164.
Validation admits only 'completed'; the table allowed entering 'merging' only from
'in_progress'. Those are mutually exclusive, so a merge that passes validation always
raises. LifecycleTransitionError subclasses ValueError and main.py registers no handler
for it, so it surfaces as an unhandled 500. begin_merge has exactly one production caller,
that endpoint.
This regressed in 61c9b04 ("refactor(backend): centralize conversation lifecycle
writes"), which introduced _STATUS_TRANSITIONS and replaced the previous unconditional
write (conversations_db.update_conversation_status(..., ConversationStatus.merging)) with
begin_merge. The new table never got a 'completed' row.
That completed -> merging is the intended edge is already documented in the codebase: the
failure rollback in merge_conversations.py says "Since source conversations were set to
'merging' status, we need to reset them back to 'completed'", and the reverse edge
merging -> completed is already declared.
Fix: declare the edge.
ConversationStatus.completed.value: {ConversationStatus.merging.value},
Tests (tests/unit/test_conversation_lifecycle_contract.py):
- Adds test_merge_admission_and_lifecycle_agree_on_completed, asserting the two gates
agree: validate_merge_compatibility accepts completed conversations, and begin_merge
then moves one to merging.
- Retargets the negative assertion in test_lifecycle_service_allows_only_declared_transitions.
It previously asserted that a completed conversation could NOT begin merge, which pinned
this bug. It now asserts on processing -> merging, which is genuinely undeclared, so the
test keeps its original purpose.
Verification (run in backend/):
- pytest tests/unit/test_conversation_lifecycle_contract.py: 14 passed
- prove-fail: with lifecycle.py reverted to main, the new test fails with
"LifecycleTransitionError: invalid lifecycle transition completed->merging" at lifecycle.py:164
- pytest test_merge_validation.py (38 passed), test_merge_conversations_canonical_delete.py
(3 passed), test_check_conversation_lifecycle_writes.py (6 passed)
- black --line-length 120 --skip-string-normalization --check: clean
- pyright utils/conversations/lifecycle.py: 0 errors
- scripts/check_module_stub_pollution.py: 0 violations
I did not exercise the endpoint against a live backend. The contract test drives
begin_merge through the same lifecycle seam the endpoint calls.1 parent bee280f commit d761b72
2 files changed
Lines changed: 28 additions & 2 deletions
Lines changed: 24 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
115 | 115 | | |
116 | 116 | | |
117 | 117 | | |
118 | | - | |
119 | | - | |
| 118 | + | |
120 | 119 | | |
121 | 120 | | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
122 | 144 | | |
123 | 145 | | |
124 | 146 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
44 | 44 | | |
45 | 45 | | |
46 | 46 | | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
47 | 51 | | |
48 | 52 | | |
49 | 53 | | |
| |||
0 commit comments