diff --git a/backend/shonar/api/v1/recordings.py b/backend/shonar/api/v1/recordings.py index dccf852..65cfaa0 100644 --- a/backend/shonar/api/v1/recordings.py +++ b/backend/shonar/api/v1/recordings.py @@ -441,6 +441,14 @@ async def reprocess_recording( rec.processing_status = ProcessingStatus.processing rec.processing_error = None await session.flush() + # Commit BEFORE poking the queue: the inline worker wakes the moment + # the item lands in the asyncio queue and reads the job row with its + # OWN session, while this request session only commits after the + # response completes. Without this the worker summarizes with the + # PREVIOUS click's tone (TRACE showed every click running one voice + # behind: click Funny -> worker read 'dry, witty'). The rows are the + # real queue; they must be durable before the poke. + await session.commit() await proc.transport_enqueue(job_type, rec.id) rows = await session.scalars( select(ProcessingJob) diff --git a/backend/tests/test_ai_pipeline.py b/backend/tests/test_ai_pipeline.py index 8b0d56d..166535c 100644 --- a/backend/tests/test_ai_pipeline.py +++ b/backend/tests/test_ai_pipeline.py @@ -94,10 +94,12 @@ class FakeLlm: def __init__(self, fail=None): self.fail = fail self.seen = [] + self.seen_tones = [] async def summarize(self, transcript, *, title=None, tone=None, on_progress=None): self.seen.append(transcript) + self.seen_tones.append(tone) if self.fail is not None: raise self.fail return SummaryResult( diff --git a/backend/tests/test_inline_queue.py b/backend/tests/test_inline_queue.py index cd2b6ff..8a6c079 100644 --- a/backend/tests/test_inline_queue.py +++ b/backend/tests/test_inline_queue.py @@ -132,6 +132,50 @@ async def test_reprocess_reruns_in_place_with_model(client, monkeypatch): await inline_queue.stop() +async def test_reprocess_summarize_uses_requested_tone(client, monkeypatch): + """Regression: the inline worker used to read the job row BEFORE the + reprocess request committed, so every re-summarize ran with the + PREVIOUS click's tone (click Funny -> 'dry, witty' summary; click + Neutral -> 'funny' summary). The endpoint now commits before poking + the queue; this pins the worker's tone to the tone in the request.""" + llm = FakeLlm() + use_fakes(monkeypatch, FakeTranscriber(), llm) + use_inline(monkeypatch) + await inline_queue.start() + try: + token = await user_tokens(client, email="reproc-tone@shonar.dev") + h = {"Authorization": f"Bearer {token}"} + rec = await upload_recording(client, token, client_id="reproc-tone-1") + await _wait_terminal(rec["id"], want=2) + + # Re-summarize asking for a NEW voice. + r = await client.post( + f"/api/v1/recordings/{rec['id']}/reprocess" + "?job=summarize&tone=sarcastic", + headers=h, + ) + assert r.status_code == 200, r.text + await _wait_terminal(rec["id"], want=2) + # The LLM call for this job must have carried 'sarcastic', not + # whatever the first (auto) pass used. + assert llm.seen_tones[-1] == "sarcastic", llm.seen_tones + s = await client.get(f"/api/v1/recordings/{rec['id']}/summary", headers=h) + assert s.json()["tone"] == "sarcastic" + + # And Neutral (tone=None) must CLEAR the voice, not inherit it. + r = await client.post( + f"/api/v1/recordings/{rec['id']}/reprocess?job=summarize", + headers=h, + ) + assert r.status_code == 200, r.text + await _wait_terminal(rec["id"], want=2) + assert llm.seen_tones[-1] is None, llm.seen_tones + s = await client.get(f"/api/v1/recordings/{rec['id']}/summary", headers=h) + assert s.json()["tone"] is None + finally: + await inline_queue.stop() + + async def test_reprocess_validation(client, monkeypatch): token = await user_tokens(client, email="reproc2@shonar.dev") h = {"Authorization": f"Bearer {token}"}