jest.mock('react-native', () => { const nativeModule = { peekPendingImport: jest.fn(), consumePendingImport: jest.fn(), addListener: jest.fn(), removeListeners: jest.fn(), }; let incomingListener: (() => void) | null = null; class NativeEventEmitter { addListener(_eventName: string, listener: () => void): { remove: jest.Mock } { incomingListener = listener; return { remove: jest.fn() }; } } return { NativeEventEmitter, NativeModules: { D3ROIncomingMedia: nativeModule }, Platform: { OS: 'android' }, __incomingMediaTestDoubles: { nativeModule, emitIncoming: () => incomingListener?.(), clearIncoming: () => { incomingListener = null; }, }, }; }); jest.mock('react-native-file-access', () => { const fileSystem = { exists: jest.fn(), unlink: jest.fn(), stat: jest.fn(), readFileChunk: jest.fn(), }; return { Dirs: { CacheDir: '/data/user/0/com.d3ro.voice/cache' }, FileSystem: fileSystem, __incomingMediaFileSystem: fileSystem, }; }); import { consumeIncomingMediaAudio, getPendingIncomingMediaId, onIncomingMediaAvailable, } from '../src/features/import/incoming-media-intent'; import { AudioPipelineError } from '../src/features/import/audio-import-types'; const testDoubles = (jest.requireMock('react-native') as { __incomingMediaTestDoubles: { nativeModule: { peekPendingImport: jest.Mock; consumePendingImport: jest.Mock }; emitIncoming: () => void; clearIncoming: () => void; }; }).__incomingMediaTestDoubles; const mockFileSystem = (jest.requireMock('react-native-file-access') as { __incomingMediaFileSystem: { exists: jest.Mock; unlink: jest.Mock; stat: jest.Mock; readFileChunk: jest.Mock; }; }).__incomingMediaFileSystem; const PATH = '/data/user/0/com.d3ro.voice/cache/d3ro-incoming-media/incoming-123e4567-e89b-42d3-a456-426614174000.m4a'; function wavHeader(): string { return Buffer.from('RIFF\u0001\u0002\u0003\u0004WAVE', 'binary').toString('base64'); } describe('incoming Android media intent bridge', () => { beforeEach(() => { jest.clearAllMocks(); testDoubles.clearIncoming(); testDoubles.nativeModule.peekPendingImport.mockResolvedValue(null); testDoubles.nativeModule.consumePendingImport.mockResolvedValue(null); mockFileSystem.exists.mockResolvedValue(false); mockFileSystem.unlink.mockResolvedValue(undefined); mockFileSystem.stat.mockResolvedValue({ type: 'file', size: 12 }); mockFileSystem.readFileChunk.mockResolvedValue(wavHeader()); }); test('routes a valid app-owned Korean filename through the normal audio validation contract', async () => { testDoubles.nativeModule.consumePendingImport.mockResolvedValue({ kind: 'media', id: '123e4567-e89b-42d3-a456-426614174000', path: PATH, uri: `file://${PATH}`, fileName: '회의 녹음 원본.m4a', mimeType: 'audio/wav', sizeBytes: 12, createdAtMs: Date.now(), }); const input = await consumeIncomingMediaAudio(); expect(input).not.toBeNull(); expect(input?.source).toBe('share-intent'); expect(input?.fileName).toBe('회의-녹음-원본.m4a'); expect(mockFileSystem.stat).toHaveBeenCalledWith(PATH); }); test('refuses a malformed path before it can be read or deleted', async () => { testDoubles.nativeModule.consumePendingImport.mockResolvedValue({ kind: 'media', id: '123e4567-e89b-42d3-a456-426614174000', path: '/sdcard/Download/voice.m4a', uri: 'file:///sdcard/Download/voice.m4a', fileName: 'voice.m4a', mimeType: 'audio/mp4', sizeBytes: 12, createdAtMs: Date.now(), }); await expect(consumeIncomingMediaAudio()).rejects.toBeInstanceOf(AudioPipelineError); expect(mockFileSystem.stat).not.toHaveBeenCalled(); expect(mockFileSystem.unlink).not.toHaveBeenCalled(); }); test('uses a one-way native pending id and subscribes to warm deliveries', async () => { testDoubles.nativeModule.peekPendingImport.mockResolvedValue('123e4567-e89b-42d3-a456-426614174000'); await expect(getPendingIncomingMediaId()).resolves.toBe('123e4567-e89b-42d3-a456-426614174000'); const listener = jest.fn(); const subscription = onIncomingMediaAvailable(listener); testDoubles.emitIncoming(); expect(listener).toHaveBeenCalledTimes(1); subscription.remove(); }); test('fails closed when a native pending id has the wrong shape', async () => { testDoubles.nativeModule.peekPendingImport.mockResolvedValue('../../replay'); await expect(getPendingIncomingMediaId()).resolves.toBeNull(); }); test.each([ ['provider-unavailable', 'file-read', 'revoked access'], ['video-no-audio', 'unsupported-mime', 'does not contain an audio track'], ['video-audio-unsupported', 'unsupported-mime', 'format is not supported'], ['file-too-large', 'file-too-large', 'too large'], ])('turns native one-shot failure %s into a visible public pipeline error', async ( errorCode, expectedCode, message, ) => { testDoubles.nativeModule.consumePendingImport.mockResolvedValue({ kind: 'error', id: '123e4567-e89b-42d3-a456-426614174000', errorCode, createdAtMs: Date.now(), }); await expect(consumeIncomingMediaAudio()).rejects.toMatchObject({ code: expectedCode, message: expect.stringContaining(message), }); expect(mockFileSystem.stat).not.toHaveBeenCalled(); }); test('rejects unknown native failure codes instead of exposing native details', async () => { testDoubles.nativeModule.consumePendingImport.mockResolvedValue({ kind: 'error', id: '123e4567-e89b-42d3-a456-426614174000', errorCode: 'java.lang.SecurityException: secret-provider', createdAtMs: Date.now(), }); await expect(consumeIncomingMediaAudio()).rejects.toMatchObject({ code: 'file-read', message: 'Shared media failure metadata is invalid', }); }); });