\set ON_ERROR_STOP on -- Regression (redteam r2-24): deleting a history entry or meeting (e.g. from the -- phone, which deletes only the parent row) must queue its raw audio for removal -- instead of leaving an unreachable object in the audio bucket forever. -- Requires 20260929000004_audio_retention_on_delete.sql. 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 ( '24000000-0000-4000-8000-000000000001', 'authenticated', 'authenticated', 'audio-retention-owner@example.invalid', crypt('fixture-password', gen_salt('bf')), now(), '{"provider":"email","providers":["email"]}'::jsonb, '{}'::jsonb, now(), now() ), ( '24000000-0000-4000-8000-000000000002', 'authenticated', 'authenticated', 'audio-retention-leaver@example.invalid', crypt('fixture-password', gen_salt('bf')), now(), '{"provider":"email","providers":["email"]}'::jsonb, '{}'::jsonb, now(), now() ); INSERT INTO public.history (id, user_id, original_text, duration) VALUES ('24100000-0000-4000-8000-000000000001', '24000000-0000-4000-8000-000000000001', 'dictation one', 1), ('24100000-0000-4000-8000-000000000002', '24000000-0000-4000-8000-000000000001', 'dictation two', 1), ('24100000-0000-4000-8000-000000000003', '24000000-0000-4000-8000-000000000002', 'leaver dictation', 1); INSERT INTO public.meetings (id, user_id, title, status) VALUES ('24200000-0000-4000-8000-000000000001', '24000000-0000-4000-8000-000000000001', 'Retention meeting', 'completed'); INSERT INTO public.audio_files ( id, user_id, history_id, meeting_id, source, storage_key, mime_type, size_bytes, sha256, upload_status ) VALUES ('24300000-0000-4000-8000-000000000001', '24000000-0000-4000-8000-000000000001', '24100000-0000-4000-8000-000000000001', NULL, 'recording', '24000000-0000-4000-8000-000000000001/history/one.wav', 'audio/wav', 10, repeat('a', 64), 'uploaded'), ('24300000-0000-4000-8000-000000000002', '24000000-0000-4000-8000-000000000001', NULL, '24200000-0000-4000-8000-000000000001', 'recording', '24000000-0000-4000-8000-000000000001/meetings/m.wav', 'audio/wav', 10, repeat('b', 64), 'uploaded'), ('24300000-0000-4000-8000-000000000003', '24000000-0000-4000-8000-000000000001', '24100000-0000-4000-8000-000000000002', NULL, 'file-picker', '24000000-0000-4000-8000-000000000001/' || repeat('c', 64) || '/memo.m4a', 'audio/mp4', 10, repeat('c', 64), 'uploaded'), ('24300000-0000-4000-8000-000000000004', '24000000-0000-4000-8000-000000000002', '24100000-0000-4000-8000-000000000003', NULL, 'recording', '24000000-0000-4000-8000-000000000002/history/leaver.wav', 'audio/wav', 10, repeat('d', 64), 'uploaded'); -- 1) Clients cannot see or drive the purge queue. Checked through the catalog: -- calling a function whose EXECUTE is denied segfaults the local -- supabase/postgres 17.6.1.104 image, which would take the shared DB down. SELECT pg_temp.assert_true( NOT has_table_privilege('authenticated', 'public.audio_purge_queue', 'SELECT') AND NOT has_table_privilege('anon', 'public.audio_purge_queue', 'SELECT') AND NOT has_table_privilege('authenticated', 'public.audio_purge_queue', 'INSERT') AND NOT has_function_privilege('authenticated', 'public.claim_audio_purge_batch_v1(integer, integer)', 'EXECUTE') AND NOT has_function_privilege('anon', 'public.claim_audio_purge_batch_v1(integer, integer)', 'EXECUTE') AND NOT has_function_privilege('authenticated', 'public.complete_audio_purge_v1(bigint[], uuid)', 'EXECUTE') AND NOT has_function_privilege('authenticated', 'public.dispatch_audio_purge_v1()', 'EXECUTE') AND has_function_privilege('service_role', 'public.claim_audio_purge_batch_v1(integer, integer)', 'EXECUTE') AND has_function_privilege('service_role', 'public.complete_audio_purge_v1(bigint[], uuid)', 'EXECUTE'), 'purge queue and worker RPCs are service-only' ); SET LOCAL ROLE authenticated; SELECT set_config( 'request.jwt.claims', '{"sub":"24000000-0000-4000-8000-000000000001","role":"authenticated"}', true ); -- 2) The phone deletes only the parent rows (RLS path, like deleteHistoryRevisionSafe). DELETE FROM public.history WHERE id = '24100000-0000-4000-8000-000000000001'; DELETE FROM public.meetings WHERE id = '24200000-0000-4000-8000-000000000001'; DELETE FROM public.history WHERE id = '24100000-0000-4000-8000-000000000002'; RESET ROLE; SELECT pg_temp.assert_true( (SELECT count(*) FROM public.audio_files WHERE id IN ('24300000-0000-4000-8000-000000000001', '24300000-0000-4000-8000-000000000002', '24300000-0000-4000-8000-000000000003') AND upload_status = 'deleted' AND history_id IS NULL AND meeting_id IS NULL) = 3, 'orphaned audio rows are marked deleted' ); SELECT pg_temp.assert_true( (SELECT array_agg(storage_key ORDER BY storage_key) FROM public.audio_purge_queue WHERE user_id = '24000000-0000-4000-8000-000000000001') = ARRAY[ '24000000-0000-4000-8000-000000000001/' || repeat('c', 64) || '/memo.m4a', '24000000-0000-4000-8000-000000000001/history/one.wav', '24000000-0000-4000-8000-000000000001/meetings/m.wav' ], 'history and meeting deletes queue their audio keys' ); -- 3) Re-importing the same content-addressed file reclaims the key: the pending -- purge is cancelled and the stale deleted row no longer blocks the UNIQUE key. SET LOCAL ROLE authenticated; INSERT INTO public.audio_files ( id, user_id, source, storage_key, mime_type, size_bytes, sha256, upload_status ) VALUES ( '24300000-0000-4000-8000-000000000005', '24000000-0000-4000-8000-000000000001', 'file-picker', '24000000-0000-4000-8000-000000000001/' || repeat('c', 64) || '/memo.m4a', 'audio/mp4', 10, repeat('c', 64), 'pending' ); RESET ROLE; SELECT pg_temp.assert_true( NOT EXISTS (SELECT 1 FROM public.audio_purge_queue WHERE storage_key LIKE '%/memo.m4a'), 'reclaimed key is no longer queued for purge' ); SELECT pg_temp.assert_true( NOT EXISTS (SELECT 1 FROM public.audio_files WHERE id = '24300000-0000-4000-8000-000000000003'), 'stale deleted row for the reclaimed key is dropped' ); -- 4) A key referenced by a live row again is dropped at claim time, never removed. INSERT INTO public.audio_purge_queue (user_id, storage_key) VALUES ('24000000-0000-4000-8000-000000000001', '24000000-0000-4000-8000-000000000001/' || repeat('c', 64) || '/memo.m4a'); SET LOCAL ROLE service_role; CREATE TEMP TABLE claimed ON COMMIT DROP AS SELECT * FROM public.claim_audio_purge_batch_v1(100, 600); RESET ROLE; SELECT pg_temp.assert_true( (SELECT count(*) FROM claimed) = 2 AND NOT EXISTS (SELECT 1 FROM claimed WHERE storage_key LIKE '%/memo.m4a') AND (SELECT count(DISTINCT lease_token) FROM claimed) = 1 AND (SELECT bool_and(leased_until > now() AND attempts = 1) FROM claimed), 'claim leases only keys with no live owner' ); -- 5) While a worker holds the lease, a live row cannot reclaim that key. SET LOCAL ROLE authenticated; DO $$ BEGIN BEGIN INSERT INTO public.audio_files ( user_id, meeting_id, source, storage_key, mime_type, size_bytes, sha256, upload_status ) VALUES ( '24000000-0000-4000-8000-000000000001', NULL, 'recording', '24000000-0000-4000-8000-000000000001/meetings/m.wav', 'audio/wav', 10, repeat('e', 64), 'pending' ); RAISE EXCEPTION 'assertion_failed: leased key was reclaimed'; EXCEPTION WHEN object_in_use THEN NULL; END; END; $$; RESET ROLE; -- 6) A second claim does not hand out leased work. SET LOCAL ROLE service_role; SELECT pg_temp.assert_true( NOT EXISTS (SELECT 1 FROM public.claim_audio_purge_batch_v1(100, 600)), 'leased entries are not claimed twice' ); -- 7) Completing with a wrong lease does nothing; the right lease clears queue and rows. SELECT pg_temp.assert_true( public.complete_audio_purge_v1( (SELECT array_agg(id) FROM claimed), gen_random_uuid() ) = 0, 'foreign lease cannot complete' ); SELECT pg_temp.assert_true( public.complete_audio_purge_v1( (SELECT array_agg(id) FROM claimed), (SELECT min(lease_token::text)::uuid FROM claimed) ) = 2, 'lease holder completes the batch' ); RESET ROLE; SELECT pg_temp.assert_true( NOT EXISTS (SELECT 1 FROM public.audio_purge_queue WHERE user_id = '24000000-0000-4000-8000-000000000001'), 'completed entries leave the queue' ); SELECT pg_temp.assert_true( NOT EXISTS (SELECT 1 FROM public.audio_files WHERE id IN ('24300000-0000-4000-8000-000000000001', '24300000-0000-4000-8000-000000000002')), 'purged audio metadata is removed' ); SELECT pg_temp.assert_true( EXISTS (SELECT 1 FROM public.audio_files WHERE id = '24300000-0000-4000-8000-000000000005' AND upload_status = 'pending'), 'the reclaimed live row survives completion' ); -- 8) Desktop upsert (ON CONFLICT on the UNIQUE key) over a deleted row reclaims it. INSERT INTO public.history (id, user_id, original_text, duration) VALUES ('24100000-0000-4000-8000-000000000004', '24000000-0000-4000-8000-000000000001', 'desktop', 1); INSERT INTO public.audio_files ( id, user_id, history_id, source, storage_key, mime_type, size_bytes, sha256, upload_status ) VALUES ( '24300000-0000-4000-8000-000000000006', '24000000-0000-4000-8000-000000000001', '24100000-0000-4000-8000-000000000004', 'recording', '24000000-0000-4000-8000-000000000001/history/desk.wav', 'audio/wav', 10, repeat('f', 64), 'uploaded' ); DELETE FROM public.history WHERE id = '24100000-0000-4000-8000-000000000004'; INSERT INTO public.history (id, user_id, original_text, duration) VALUES ('24100000-0000-4000-8000-000000000004', '24000000-0000-4000-8000-000000000001', 'desktop again', 1); INSERT INTO public.audio_files ( user_id, history_id, source, storage_key, mime_type, size_bytes, sha256, upload_status ) VALUES ( '24000000-0000-4000-8000-000000000001', '24100000-0000-4000-8000-000000000004', 'recording', '24000000-0000-4000-8000-000000000001/history/desk.wav', 'audio/wav', 10, repeat('f', 64), 'uploaded' ) ON CONFLICT (user_id, sha256, storage_key) DO UPDATE SET history_id = EXCLUDED.history_id, upload_status = EXCLUDED.upload_status; SELECT pg_temp.assert_true( (SELECT count(*) FROM public.audio_files WHERE storage_key = '24000000-0000-4000-8000-000000000001/history/desk.wav') = 1 AND EXISTS (SELECT 1 FROM public.audio_files WHERE storage_key = '24000000-0000-4000-8000-000000000001/history/desk.wav' AND upload_status = 'uploaded' AND history_id = '24100000-0000-4000-8000-000000000004') AND NOT EXISTS (SELECT 1 FROM public.audio_purge_queue WHERE storage_key LIKE '%/desk.wav'), 'desktop upsert reclaims a deleted key' ); -- 9) Account deletion still cascades (the queue has no FK to auth.users). DELETE FROM auth.users WHERE id = '24000000-0000-4000-8000-000000000002'; SELECT pg_temp.assert_true( NOT EXISTS (SELECT 1 FROM public.audio_files WHERE user_id = '24000000-0000-4000-8000-000000000002'), 'account deletion removes the user audio rows' ); -- 10) The dispatcher is a no-op when there is no ready work. SELECT pg_temp.assert_true( public.dispatch_audio_purge_v1() IS NULL OR NOT EXISTS ( SELECT 1 FROM public.audio_purge_queue WHERE user_id = '24000000-0000-4000-8000-000000000001' ), 'dispatcher does not fail' ); ROLLBACK;