import React from 'react' import { act, create, type ReactTestInstance, type ReactTestRenderer } from 'react-test-renderer' const USER_ID = '11111111-1111-4111-8111-111111111111' let mockSession: { user: { id: string }, access_token: string } | null = { user: { id: USER_ID }, access_token: 'token-1', } const mockAcquire = jest.fn() const mockRequestPermission = jest.fn(async () => true) const mockLegacyStart = jest.fn(async () => { throw new Error('Cannot start recorder while it is recording') }) const mockGlobalCancel = jest.fn(async () => undefined) const mockStreamTalkResponse = jest.fn() jest.mock('@react-navigation/native', () => { const ReactValue = require('react') as typeof React return { useNavigation: () => ({ goBack: jest.fn() }), useFocusEffect: (effect: () => void | (() => void)) => { ReactValue.useEffect(effect, [effect]) }, } }) jest.mock('react-native-safe-area-context', () => ({ useSafeAreaInsets: () => ({ top: 0, bottom: 0, left: 0, right: 0 }), })) jest.mock('@d3ro/i18n', () => ({ useI18n: () => ({ t: (key: string) => key }), })) jest.mock('../src/lib/auth-context', () => ({ useAuth: () => ({ session: mockSession }), })) jest.mock('../src/lib/preferences-context', () => ({ useMobilePreferences: () => ({ palette: { bg: { app: 'app', sidebar: 'sidebar', card: 'card', inset: 'inset' }, text: { primary: 'primary', muted: 'muted' }, border: { default: 'border' }, accent: { main: 'accent', dim: 'dim', pressed: 'pressed' }, tag: { red: 'red' }, }, preferences: { preferredLlmModel: 'default', locale: 'en' }, }), })) jest.mock('../src/theme/themed-components', () => { const { Pressable, Text } = require('react-native') as typeof import('react-native') return { ThemeText: ({ children, ...props }: React.PropsWithChildren>) => ( {children} ), ThemeButton: ({ label, onPress, ...props }: { label: string, onPress?: () => void }) => ( {label} ), } }) jest.mock('../src/components/ContentReportSheet', () => ({ __esModule: true, default: () => null, })) jest.mock('../src/features/chat/chat-service', () => { class MockChatServiceError extends Error { code: string retryable: boolean constructor(mockCode: string, mockRetryable: boolean) { super(mockCode) this.code = mockCode this.retryable = mockRetryable } } return { ChatServiceError: MockChatServiceError } }) jest.mock('../src/features/talk/llm-stream-service', () => ({ streamTalkResponse: (...args: unknown[]) => mockStreamTalkResponse(...args), })) jest.mock('../src/features/talk/talk-transcription-service', () => ({ transcribeTalkRecording: jest.fn(), })) jest.mock('../src/features/talk/talk-tts', () => { class MockTalkTtsError extends Error { code = 'FAILED' } return { shutdownTalkSpeech: jest.fn(async () => undefined), speakTalkText: jest.fn(async () => ({ status: 'completed' })), stopTalkSpeech: jest.fn(async () => true), TalkTtsError: MockTalkTtsError, } }) jest.mock('../src/features/talk/talk-debug-audio', () => ({ isDeterministicTalkAudioAvailable: () => false, synthesizeDeterministicTalkAudio: jest.fn(), })) jest.mock('../src/lib/audio-recorder', () => { class MockRecorderBusyError extends Error { code = 'recorder_busy' holder: string constructor(mockHolder: string) { super(`The recorder is in use by ${mockHolder}`) this.holder = mockHolder } } return { RecorderBusyError: MockRecorderBusyError, audioRecorder: { acquire: (...args: unknown[]) => mockAcquire(...args), requestPermission: () => mockRequestPermission(), // The pre-lease API: a global start/cancel any screen could call. start: () => mockLegacyStart(), cancel: () => mockGlobalCancel(), }, } }) import TalkScreen from '../src/screens/TalkScreen' import { RecorderBusyError } from '../src/lib/audio-recorder' async function flush(): Promise { for (let index = 0; index < 5; index += 1) { await act(async () => { await Promise.resolve() }) } } function byTestId(root: ReactTestInstance, testID: string, prop: string): ReactTestInstance { const match = root.findAll((node) => ( node.props.testID === testID && typeof node.props[prop] === 'function' ))[0] if (match === undefined) throw new Error(`Missing ${testID} with ${prop}`) return match } function textContent(root: ReactTestInstance): string { return root.findAll((node) => typeof node.props.children === 'string') .map((node) => node.props.children as string) .join('\n') } let renderer: ReactTestRenderer | null = null beforeEach(() => { mockSession = { user: { id: USER_ID }, access_token: 'token-1' } mockAcquire.mockReset() mockGlobalCancel.mockClear() mockLegacyStart.mockClear() mockStreamTalkResponse.mockReset() }) afterEach(() => { act(() => renderer?.unmount()) renderer = null }) describe('TalkScreen redteam r1-24', () => { test('pressing the mic while a meeting recording holds the recorder never cancels it', async () => { mockAcquire.mockRejectedValue(new RecorderBusyError('record')) await act(async () => { renderer = create() }) await act(async () => { byTestId(renderer!.root, 'talk-ptt', 'onPressIn').props.onPressIn() }) await flush() expect(mockAcquire).toHaveBeenCalledWith('talk') expect(mockGlobalCancel).not.toHaveBeenCalled() expect(textContent(renderer!.root)).toContain('mobile.talk.error.recording') }) test('an access-token refresh for the same user keeps the in-flight reply', async () => { let resolveStream: (value: { text: string, generationId: string }) => void = () => undefined let streamSignal: AbortSignal | null = null mockStreamTalkResponse.mockImplementation((_messages: unknown, options: { signal: AbortSignal }) => { streamSignal = options.signal return new Promise((resolve) => { resolveStream = resolve }) }) await act(async () => { renderer = create() }) await act(async () => { byTestId(renderer!.root, 'talk-input', 'onChangeText').props.onChangeText('hello') }) await act(async () => { byTestId(renderer!.root, 'talk-send', 'onPress').props.onPress() }) await flush() expect(mockStreamTalkResponse).toHaveBeenCalledTimes(1) // supabase-js TOKEN_REFRESHED: same user, rotated access token. mockSession = { user: { id: USER_ID }, access_token: 'token-2' } await act(async () => { renderer!.update() }) expect(streamSignal!.aborted).toBe(false) await act(async () => { resolveStream({ text: 'the finished reply', generationId: 'gen-1' }) }) await flush() const text = textContent(renderer!.root) expect(text).toContain('the finished reply') expect(text).not.toContain('mobile.talk.error.auth') }) test('switching to a different user still cancels the in-flight reply', async () => { let streamSignal: AbortSignal | null = null mockStreamTalkResponse.mockImplementation((_messages: unknown, options: { signal: AbortSignal }) => { streamSignal = options.signal return new Promise(() => undefined) }) await act(async () => { renderer = create() }) await act(async () => { byTestId(renderer!.root, 'talk-input', 'onChangeText').props.onChangeText('hello') }) await act(async () => { byTestId(renderer!.root, 'talk-send', 'onPress').props.onPress() }) await flush() mockSession = { user: { id: '33333333-3333-4333-8333-333333333333' }, access_token: 'token-3' } await act(async () => { renderer!.update() }) expect(streamSignal!.aborted).toBe(true) }) })