feat(release): prepare 1.1.0 candidate

This commit is contained in:
Yun Chan 2026-08-29 18:33:45 +09:00
parent 5a34f66981
commit 5205dcdfa9
736 changed files with 115667 additions and 12203 deletions

View file

@ -0,0 +1,169 @@
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',
});
});
});