\set ON_ERROR_STOP on -- A re-record that fails after upload restores the meeting it re-records -- (migration 20260929040000). Local only: psql against the local Supabase -- stack. Runs in a transaction and rolls back. BEGIN; CREATE OR REPLACE FUNCTION pg_temp.assert_true(condition boolean, message text) RETURNS void LANGUAGE plpgsql AS $$ BEGIN IF condition IS NOT TRUE THEN RAISE EXCEPTION 'assertion_failed: %', message; END IF; END; $$; INSERT INTO auth.users ( id, aud, role, email, encrypted_password, email_confirmed_at, raw_app_meta_data, raw_user_meta_data, created_at, updated_at ) VALUES ( '91000000-0000-4000-8000-000000000001', 'authenticated', 'authenticated', 'rerecord-failure-owner@example.invalid', crypt('fixture-password', gen_salt('bf')), now(), '{"provider":"email","providers":["email"]}'::jsonb, '{}'::jsonb, now(), now() ); -- M1: completed by a mobile capture (A1, job J1). -- M2: first recording, no content yet. -- M3: content from elsewhere (desktop): transcript rows, no mobile job, no audio row. INSERT INTO public.meetings ( id, user_id, title, status, raw_transcript, minutes_markdown, duration_ms, audio_storage_key ) VALUES ( '92000000-0000-4000-8000-000000000001', '91000000-0000-4000-8000-000000000001', 'Completed meeting', 'completed', 'old transcript', 'old minutes', 1000, '91000000-0000-4000-8000-000000000001/imports/a1.wav' ), ( '92000000-0000-4000-8000-000000000002', '91000000-0000-4000-8000-000000000001', 'First recording', 'recording', NULL, NULL, NULL, NULL ), ( '92000000-0000-4000-8000-000000000003', '91000000-0000-4000-8000-000000000001', 'Desktop meeting', 'completed', 'desktop transcript', NULL, 5000, 'desktop/original.wav' ); INSERT INTO public.transcripts (meeting_id, segment_index, timestamp_ms, duration_ms, text) VALUES ('92000000-0000-4000-8000-000000000001', 0, 0, 1000, 'old transcript'), ('92000000-0000-4000-8000-000000000003', 0, 0, 2000, 'desktop segment 0'), ('92000000-0000-4000-8000-000000000003', 1, 2000, 3000, 'desktop segment 1'); INSERT INTO public.audio_files ( id, user_id, meeting_id, source, original_name, storage_key, mime_type, size_bytes, duration_ms, sha256, upload_status ) VALUES ( '93000000-0000-4000-8000-000000000001', '91000000-0000-4000-8000-000000000001', '92000000-0000-4000-8000-000000000001', 'recording', 'a1.wav', '91000000-0000-4000-8000-000000000001/imports/a1.wav', 'audio/wav', 32044, 1000, repeat('1', 64), 'uploaded' ), ( '93000000-0000-4000-8000-000000000002', '91000000-0000-4000-8000-000000000001', '92000000-0000-4000-8000-000000000001', 'recording', 'a2.wav', '91000000-0000-4000-8000-000000000001/imports/a2.wav', 'audio/wav', 64044, 2000, repeat('2', 64), 'uploaded' ), ( '93000000-0000-4000-8000-000000000003', '91000000-0000-4000-8000-000000000001', '92000000-0000-4000-8000-000000000001', 'recording', 'a3.wav', '91000000-0000-4000-8000-000000000001/imports/a3.wav', 'audio/wav', 96044, 3000, repeat('3', 64), 'uploaded' ), ( '93000000-0000-4000-8000-000000000004', '91000000-0000-4000-8000-000000000001', '92000000-0000-4000-8000-000000000002', 'recording', 'first.wav', '91000000-0000-4000-8000-000000000001/imports/first.wav', 'audio/wav', 32044, 1000, repeat('4', 64), 'uploaded' ), ( '93000000-0000-4000-8000-000000000005', '91000000-0000-4000-8000-000000000001', '92000000-0000-4000-8000-000000000003', 'recording', 'desktop-rerecord.wav', '91000000-0000-4000-8000-000000000001/imports/desktop-rerecord.wav', 'audio/wav', 32044, 9000, repeat('5', 64), 'failed' ); INSERT INTO public.processing_jobs ( user_id, audio_file_id, meeting_id, kind, status, progress, attempt_count, idempotency_key, result, started_at, completed_at ) VALUES ( '91000000-0000-4000-8000-000000000001', '93000000-0000-4000-8000-000000000001', '92000000-0000-4000-8000-000000000001', 'transcription', 'succeeded', 100, 1, 'mobile-meeting:m1:' || repeat('1', 64), jsonb_build_object('audio_file_id', '93000000-0000-4000-8000-000000000001', 'duration_ms', 1000), now() - interval '1 day', now() - interval '1 day' ); SET LOCAL ROLE authenticated; SELECT set_config( 'request.jwt.claims', '{"sub":"91000000-0000-4000-8000-000000000001","role":"authenticated"}', true ); -- 1) Re-record M1 with A2; STT fails terminally after upload. SELECT public.mobile_begin_meeting_recording('92000000-0000-4000-8000-000000000001'); SELECT public.mobile_queue_meeting_recording('92000000-0000-4000-8000-000000000001', 2000); SELECT public.mobile_begin_meeting_processing( '92000000-0000-4000-8000-000000000001', '93000000-0000-4000-8000-000000000002', 'mobile-meeting:m1:' || repeat('2', 64) ); SELECT pg_temp.assert_true( (SELECT audio_storage_key FROM public.meetings WHERE id = '92000000-0000-4000-8000-000000000001') = '91000000-0000-4000-8000-000000000001/imports/a2.wav', 'precondition: processing points the meeting at the new capture' ); SELECT public.mobile_mark_meeting_processing_failure( '92000000-0000-4000-8000-000000000001', 'mobile-meeting:m1:' || repeat('2', 64), 'transcription', 'STT failed', true ); SELECT pg_temp.assert_true( EXISTS ( SELECT 1 FROM public.meetings WHERE id = '92000000-0000-4000-8000-000000000001' AND status = 'completed' AND error_message IS NULL AND raw_transcript = 'old transcript' AND minutes_markdown = 'old minutes' AND duration_ms = 1000 AND audio_storage_key = '91000000-0000-4000-8000-000000000001/imports/a1.wav' ), 'terminal failure of a re-record restores the completed meeting and its audio' ); SELECT pg_temp.assert_true( EXISTS ( SELECT 1 FROM public.processing_jobs WHERE idempotency_key = 'mobile-meeting:m1:' || repeat('2', 64) AND status = 'failed' AND error_message = 'STT failed' ), 'the failed job still records the failure' ); RESET ROLE; SELECT pg_temp.assert_true( EXISTS ( SELECT 1 FROM public.audio_files WHERE id = '93000000-0000-4000-8000-000000000002' AND meeting_id IS NULL AND upload_status = 'deleted' ), 'the failed capture audio is detached' ); SELECT pg_temp.assert_true( EXISTS ( SELECT 1 FROM public.audio_purge_queue WHERE user_id = '91000000-0000-4000-8000-000000000001' AND storage_key = '91000000-0000-4000-8000-000000000001/imports/a2.wav' ), 'the failed capture audio is queued for purge' ); SELECT pg_temp.assert_true( EXISTS ( SELECT 1 FROM public.audio_files WHERE id = '93000000-0000-4000-8000-000000000001' AND meeting_id = '92000000-0000-4000-8000-000000000001' AND upload_status = 'uploaded' ), 'the audio behind the kept transcript stays linked' ); SELECT pg_temp.assert_true( (SELECT meeting_id FROM public.audio_files WHERE id = '93000000-0000-4000-8000-000000000003') = '92000000-0000-4000-8000-000000000001', 'uploaded audio no failed job owns is not detached by an unrelated failure' ); -- 2) Re-record M1 with A3; the user discards the queued item. SET LOCAL ROLE authenticated; SELECT set_config( 'request.jwt.claims', '{"sub":"91000000-0000-4000-8000-000000000001","role":"authenticated"}', true ); SELECT public.mobile_begin_meeting_recording('92000000-0000-4000-8000-000000000001'); SELECT public.mobile_queue_meeting_recording('92000000-0000-4000-8000-000000000001', 3000); SELECT public.mobile_begin_meeting_processing( '92000000-0000-4000-8000-000000000001', '93000000-0000-4000-8000-000000000003', 'mobile-meeting:m1:' || repeat('3', 64) ); SELECT pg_temp.assert_true( (SELECT status FROM public.mobile_fail_meeting_recording( '92000000-0000-4000-8000-000000000001', 'Queued audio processing was cancelled' )) = 'completed', 'fail_recording reports the restored status' ); SELECT pg_temp.assert_true( EXISTS ( SELECT 1 FROM public.meetings WHERE id = '92000000-0000-4000-8000-000000000001' AND status = 'completed' AND error_message IS NULL AND duration_ms = 1000 AND audio_storage_key = '91000000-0000-4000-8000-000000000001/imports/a1.wav' ), 'a discarded re-record restores the completed meeting and its audio' ); RESET ROLE; SELECT pg_temp.assert_true( (SELECT meeting_id FROM public.audio_files WHERE id = '93000000-0000-4000-8000-000000000003') IS NULL, 'the discarded capture audio is detached' ); -- 3) A first recording keeps the error outcome and its audio for retry. SET LOCAL ROLE authenticated; SELECT set_config( 'request.jwt.claims', '{"sub":"91000000-0000-4000-8000-000000000001","role":"authenticated"}', true ); SELECT public.mobile_queue_meeting_recording('92000000-0000-4000-8000-000000000002', 1000); SELECT public.mobile_begin_meeting_processing( '92000000-0000-4000-8000-000000000002', '93000000-0000-4000-8000-000000000004', 'mobile-meeting:m2:' || repeat('4', 64) ); SELECT public.mobile_mark_meeting_processing_failure( '92000000-0000-4000-8000-000000000002', 'mobile-meeting:m2:' || repeat('4', 64), 'transcription', 'STT failed', true ); SELECT pg_temp.assert_true( EXISTS ( SELECT 1 FROM public.meetings WHERE id = '92000000-0000-4000-8000-000000000002' AND status = 'error' AND error_message = 'STT failed' ), 'terminal failure of a first recording still reports an error' ); SELECT pg_temp.assert_true( (SELECT status FROM public.mobile_fail_meeting_recording( '92000000-0000-4000-8000-000000000002', 'Recording failed' )) = 'error', 'fail_recording of a first recording still reports an error' ); RESET ROLE; SELECT pg_temp.assert_true( EXISTS ( SELECT 1 FROM public.audio_files WHERE id = '93000000-0000-4000-8000-000000000004' AND meeting_id = '92000000-0000-4000-8000-000000000002' AND upload_status = 'uploaded' ), 'a failed first recording keeps its audio linked for retry' ); -- 4) Content from elsewhere: a re-record whose upload failed (no job) keeps the -- original playback key and restores the duration from the transcript span. SET LOCAL ROLE authenticated; SELECT set_config( 'request.jwt.claims', '{"sub":"91000000-0000-4000-8000-000000000001","role":"authenticated"}', true ); SELECT public.mobile_begin_meeting_recording('92000000-0000-4000-8000-000000000003'); SELECT public.mobile_queue_meeting_recording('92000000-0000-4000-8000-000000000003', 9000); SELECT public.mobile_fail_meeting_recording( '92000000-0000-4000-8000-000000000003', 'Audio upload failed' ); SELECT pg_temp.assert_true( EXISTS ( SELECT 1 FROM public.meetings WHERE id = '92000000-0000-4000-8000-000000000003' AND status = 'completed' AND error_message IS NULL AND raw_transcript = 'desktop transcript' AND duration_ms = 5000 AND audio_storage_key = 'desktop/original.wav' ), 'a failed re-record of foreign content keeps its key and transcript duration' ); RESET ROLE; SELECT pg_temp.assert_true( (SELECT meeting_id FROM public.audio_files WHERE id = '93000000-0000-4000-8000-000000000005') IS NULL, 'the failed upload row of the re-record is detached' ); -- 5) Cancel before processing keeps the 20260929002700 behavior. SET LOCAL ROLE authenticated; SELECT set_config( 'request.jwt.claims', '{"sub":"91000000-0000-4000-8000-000000000001","role":"authenticated"}', true ); SELECT public.mobile_begin_meeting_recording('92000000-0000-4000-8000-000000000001'); SELECT pg_temp.assert_true( (SELECT status FROM public.mobile_cancel_meeting_recording( '92000000-0000-4000-8000-000000000001' )) = 'completed', 'cancel of a re-record still returns to completed' ); -- 6) The helpers are not reachable through the API roles. RESET ROLE; SELECT pg_temp.assert_true( NOT has_function_privilege('authenticated', 'public.mobile_meeting_has_content_v1(uuid)', 'EXECUTE') AND NOT has_function_privilege('anon', 'public.mobile_meeting_has_content_v1(uuid)', 'EXECUTE') AND NOT has_function_privilege( 'authenticated', 'public.mobile_restore_meeting_after_failed_capture_v1(uuid, uuid, uuid)', 'EXECUTE' ) AND NOT has_function_privilege( 'anon', 'public.mobile_restore_meeting_after_failed_capture_v1(uuid, uuid, uuid)', 'EXECUTE' ), 'restore helpers are internal' ); ROLLBACK;