d3ro-voice/server/supabase/tests/mobile-rerecord-failure-paths.integration.sql
Yun Chan ba9ef9741e fix: red-team round 3 hardening across desktop, mobile, core and server
Batch of red-team r3 fixes that were in the working tree before the
2026-09-28 design overhaul, committed as one unit with their tests.

- desktop main: STT timeouts and sidecar, voice recording store, sync
  (credentials, audio, knowledge reindex, push gates), runtime
  provisioner, update policy, AltGr keybindings, voice-command policy,
  dictionary file codec/limits, meeting transcript condensing and a
  local recording ledger so interrupted-session recovery only closes
  meetings this device recorded (a phone's live meeting is left alone).
- mobile: login CSRF via implicit token callbacks rejected, account
  deletion/retention, durable queue retention, knowledge realtime
  without unfiltered DELETE, meeting re-record failure paths, cloud STT
  client, preferences store/resync.
- core: text chunking splits long unbroken transcripts to fit, template
  field policy, dictionary limits, meeting markdown inline handling.
- server: payple webhook policy and cancellation order scope, meeting
  document generation quota, team RPC null-role guard, unified LLM
  quota in-flight accounting, knowledge chunk vector index, meeting
  re-record failure paths (migrations 20260929*).
- ci: portable/runtime feed gates, update-policy schema, Forgejo file
  delete and alias planning.

Four older tests are updated to the new contracts rather than the old
behavior: token-pair auth callbacks are rejected, knowledge realtime no
longer subscribes to DELETE, long transcript lines are split, and
meeting recovery requires the local recording ledger for empty rows.
2026-09-28 20:45:52 +09:00

341 lines
12 KiB
PL/PgSQL

\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;