feat(release): prepare 1.1.0 candidate
This commit is contained in:
parent
5a34f66981
commit
5205dcdfa9
736 changed files with 115667 additions and 12203 deletions
134
apps/mobile-rn/__tests__/meeting-recording-lifecycle.test.ts
Normal file
134
apps/mobile-rn/__tests__/meeting-recording-lifecycle.test.ts
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
jest.mock('../src/lib/supabase', () => ({
|
||||
supabase: {
|
||||
rpc: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import type { Meeting, ProcessingJob } from '@d3ro/api-client';
|
||||
import { supabase } from '../src/lib/supabase';
|
||||
import {
|
||||
beginMeetingProcessing,
|
||||
beginMeetingRecording,
|
||||
completeMeetingProcessing,
|
||||
markMeetingProcessingFailure,
|
||||
queueMeetingRecording,
|
||||
} from '../src/features/meetings/meetings-service';
|
||||
|
||||
const USER_ID = '11111111-1111-4111-8111-111111111111';
|
||||
const MEETING_ID = '22222222-2222-4222-8222-222222222222';
|
||||
const AUDIO_ID = '33333333-3333-4333-8333-333333333333';
|
||||
const JOB_ID = '44444444-4444-4444-8444-444444444444';
|
||||
const IDEMPOTENCY = `mobile-meeting:${MEETING_ID}:${'a'.repeat(64)}`;
|
||||
const mockRpc = supabase.rpc as jest.Mock;
|
||||
|
||||
function meeting(status: Meeting['status']): Meeting {
|
||||
return {
|
||||
id: MEETING_ID,
|
||||
user_id: USER_ID,
|
||||
team_id: null,
|
||||
title: 'Mobile meeting',
|
||||
status,
|
||||
started_at: '2026-08-21T00:00:00.000Z',
|
||||
ended_at: status === 'recording' ? null : '2026-08-21T00:01:00.000Z',
|
||||
duration_ms: status === 'recording' ? null : 60_000,
|
||||
raw_transcript: status === 'completed' ? 'verified' : null,
|
||||
edited_transcript: null,
|
||||
minutes_markdown: null,
|
||||
minutes_json: null,
|
||||
stt_model: status === 'completed' ? 'whisper' : null,
|
||||
llm_model: null,
|
||||
stt_latency_ms: status === 'completed' ? 100 : null,
|
||||
llm_latency_ms: null,
|
||||
error_message: null,
|
||||
audio_storage_key: status === 'completed' ? `${USER_ID}/imports/audio.wav` : null,
|
||||
created_at: '2026-08-21T00:00:00.000Z',
|
||||
updated_at: '2026-08-21T00:01:00.000Z',
|
||||
};
|
||||
}
|
||||
|
||||
function job(status: ProcessingJob['status']): ProcessingJob {
|
||||
return {
|
||||
id: JOB_ID,
|
||||
user_id: USER_ID,
|
||||
audio_file_id: AUDIO_ID,
|
||||
history_id: null,
|
||||
meeting_id: MEETING_ID,
|
||||
kind: 'transcription',
|
||||
status,
|
||||
progress: status === 'succeeded' ? 100 : 70,
|
||||
attempt_count: 1,
|
||||
idempotency_key: IDEMPOTENCY,
|
||||
error_code: null,
|
||||
error_message: null,
|
||||
result: null,
|
||||
started_at: '2026-08-21T00:00:00.000Z',
|
||||
completed_at: status === 'succeeded' ? '2026-08-21T00:01:00.000Z' : null,
|
||||
created_at: '2026-08-21T00:00:00.000Z',
|
||||
updated_at: '2026-08-21T00:00:00.000Z',
|
||||
};
|
||||
}
|
||||
|
||||
describe('meeting recording RPC lifecycle', () => {
|
||||
beforeEach(() => mockRpc.mockReset());
|
||||
|
||||
test('moves recording to queued processing with server-confirmed ownership', async () => {
|
||||
mockRpc
|
||||
.mockResolvedValueOnce({ data: meeting('recording'), error: null })
|
||||
.mockResolvedValueOnce({ data: meeting('processing'), error: null })
|
||||
.mockResolvedValueOnce({ data: job('running'), error: null });
|
||||
|
||||
await expect(beginMeetingRecording(USER_ID, MEETING_ID)).resolves.toMatchObject({ status: 'recording' });
|
||||
await expect(queueMeetingRecording(USER_ID, MEETING_ID, 60_000)).resolves.toMatchObject({ status: 'processing' });
|
||||
await expect(beginMeetingProcessing(USER_ID, MEETING_ID, AUDIO_ID, IDEMPOTENCY))
|
||||
.resolves.toMatchObject({ id: JOB_ID, status: 'running' });
|
||||
expect(mockRpc.mock.calls.map(call => call[0])).toEqual([
|
||||
'mobile_begin_meeting_recording',
|
||||
'mobile_queue_meeting_recording',
|
||||
'mobile_begin_meeting_processing',
|
||||
]);
|
||||
});
|
||||
|
||||
test('completes transcript/audio/job atomically and maps transient failure to queued', async () => {
|
||||
mockRpc
|
||||
.mockResolvedValueOnce({ data: meeting('completed'), error: null })
|
||||
.mockResolvedValueOnce({ data: job('queued'), error: null });
|
||||
|
||||
await expect(completeMeetingProcessing(
|
||||
USER_ID,
|
||||
MEETING_ID,
|
||||
AUDIO_ID,
|
||||
IDEMPOTENCY,
|
||||
{
|
||||
transcript: 'verified',
|
||||
language: 'en',
|
||||
provider: 'whisper',
|
||||
durationMs: 60_000,
|
||||
sttLatencyMs: 100,
|
||||
},
|
||||
)).resolves.toMatchObject({ status: 'completed', raw_transcript: 'verified' });
|
||||
expect(mockRpc.mock.calls[0][1]).toMatchObject({
|
||||
p_audio_file_id: AUDIO_ID,
|
||||
p_transcript: 'verified',
|
||||
p_duration_ms: 60_000,
|
||||
});
|
||||
|
||||
await expect(markMeetingProcessingFailure(
|
||||
USER_ID,
|
||||
MEETING_ID,
|
||||
IDEMPOTENCY,
|
||||
'upload',
|
||||
'offline',
|
||||
false,
|
||||
)).resolves.toMatchObject({ status: 'queued' });
|
||||
expect(mockRpc.mock.calls[1][1]).toMatchObject({ p_terminal: false });
|
||||
});
|
||||
|
||||
test('fails closed when the server returns a job linked to another audio file', async () => {
|
||||
mockRpc.mockResolvedValueOnce({
|
||||
data: { ...job('running'), audio_file_id: '55555555-5555-4555-8555-555555555555' },
|
||||
error: null,
|
||||
});
|
||||
await expect(beginMeetingProcessing(USER_ID, MEETING_ID, AUDIO_ID, IDEMPOTENCY))
|
||||
.rejects.toThrow('linkage is invalid');
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue