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.
341 lines
12 KiB
PL/PgSQL
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;
|