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

@ -2,23 +2,27 @@
// createD3roSupabaseClient 팩토리 유닛 테스트
import { describe, it, expect } from 'vitest'
import { createD3roSupabaseClient, isClientConfigured } from '../src/client'
import {
createD3roSupabaseClient,
isClientConfigured,
SupabaseConfigurationError,
} from '../src/client'
describe('createD3roSupabaseClient', () => {
it('URL과 key가 없으면 placeholder 클라이언트를 반환한다', () => {
const client = createD3roSupabaseClient({ url: undefined, anonKey: undefined })
expect(client).toBeDefined()
// auth, from 등 메서드가 존재해야 함
expect(typeof client.auth.getSession).toBe('function')
expect(typeof client.from).toBe('function')
it('URL과 key가 없으면 설정 오류로 즉시 실패한다', () => {
expect(() => createD3roSupabaseClient({ url: undefined, anonKey: undefined }))
.toThrow(SupabaseConfigurationError)
})
it('URL만 있고 key가 없으면 placeholder 클라이언트를 반환한다', () => {
const client = createD3roSupabaseClient({
it('URL이나 key 중 하나만 있거나 공백이면 설정 오류로 즉시 실패한다', () => {
expect(() => createD3roSupabaseClient({
url: 'https://real.supabase.co',
anonKey: undefined
})
expect(client).toBeDefined()
anonKey: undefined,
})).toThrow(SupabaseConfigurationError)
expect(() => createD3roSupabaseClient({
url: ' ',
anonKey: 'anon-key',
})).toThrow(SupabaseConfigurationError)
})
it('URL과 key가 모두 있으면 실제 클라이언트를 생성한다', () => {
@ -60,5 +64,6 @@ describe('isClientConfigured', () => {
it('빈 문자열은 false로 처리된다', () => {
expect(isClientConfigured({ url: '', anonKey: 'k' })).toBe(false)
expect(isClientConfigured({ url: 'https://x.supabase.co', anonKey: '' })).toBe(false)
expect(isClientConfigured({ url: ' ', anonKey: 'k' })).toBe(false)
})
})

View file

@ -1,108 +1,82 @@
// packages/api-client/__tests__/transcribe.test.ts
// transcribeAudio 함수 단위 및 통합 인터페이스 테스트
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { transcribeAudio } from '../src/transcribe'
describe('transcribeAudio', () => {
beforeEach(() => {
vi.restoreAllMocks()
})
const session = {
supabaseUrl: 'https://project.supabase.co',
anonKey: 'anon-key',
token: 'user-token',
}
it('Blob 입력 시 multipart/form-data로 /api/stt/transcribe를 호출하여 전사 결과를 반환한다', async () => {
const mockAudioBlob = new Blob(['mock-audio-content'], { type: 'audio/webm' })
const mockResponse = {
text: '안녕하세요, 클라우드 음성 전사 테스트입니다.',
function payload(transcript = '안녕하세요') {
return {
transcript,
confidence: 0.99,
language_code: 'ko',
duration_seconds: 2.5,
provider: 'groq',
}
}
describe('transcribeAudio', () => {
beforeEach(() => vi.unstubAllGlobals())
it('uses only the authenticated Supabase quota gateway', async () => {
const fetchMock = vi.fn(async () => ({ ok: true, status: 200, json: async () => payload() }))
vi.stubGlobal('fetch', fetchMock)
const result = await transcribeAudio({
...session,
audio: new Blob(['audio'], { type: 'audio/webm' }),
language: 'ko',
})
const [url, options] = fetchMock.mock.calls[0]
expect(url).toBe('https://project.supabase.co/functions/v1/stt-proxy')
expect(options.headers).toEqual({ Authorization: 'Bearer user-token', apikey: 'anon-key' })
expect(options.body).toBeInstanceOf(FormData)
expect((options.body as FormData).get('language_code')).toBe('ko')
expect(result).toEqual({
text: '안녕하세요',
confidence: 0.99,
language: 'ko',
durationSeconds: 2.5,
provider: 'groq',
modelId: 'whisper-large-v3-turbo',
latencyMs: 145,
cost: 0.000021,
}
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: async () => mockResponse,
latencyMs: expect.any(Number),
})
vi.stubGlobal('fetch', fetchMock)
const result = await transcribeAudio({
audio: mockAudioBlob,
language: 'ko',
apiBaseUrl: 'http://localhost:5000',
token: 'test-token',
})
expect(fetchMock).toHaveBeenCalledTimes(1)
const [url, options] = fetchMock.mock.calls[0]
expect(url).toBe('http://localhost:5000/api/stt/transcribe')
expect(options.method).toBe('POST')
expect(options.headers['Authorization']).toBe('Bearer test-token')
expect(options.body).toBeInstanceOf(FormData)
expect(result.text).toBe('안녕하세요, 클라우드 음성 전사 테스트입니다.')
expect(result.provider).toBe('groq')
expect(result.durationSeconds).toBe(2.5)
expect(result.latencyMs).toBe(145)
})
it('Base64 문자열 입력 시 JSON 페이로드로 전송한다', async () => {
const mockBase64 = 'UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAAAAA='
const mockResponse = {
text: 'Base64 전사 성공',
confidence: 0.98,
language: 'ko',
durationSeconds: 1.0,
provider: 'openai',
modelId: 'whisper-1',
latencyMs: 320,
cost: 0.0001,
}
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: async () => mockResponse,
})
it('converts binary and base64 audio to multipart without a JSON bypass', async () => {
const fetchMock = vi.fn(async () => ({ ok: true, status: 200, json: async () => payload('binary') }))
vi.stubGlobal('fetch', fetchMock)
const result = await transcribeAudio({
audio: mockBase64,
language: 'ko',
prompt: '의료 용어 가이드',
apiBaseUrl: 'http://localhost:5000',
})
await transcribeAudio({ ...session, audio: Uint8Array.from([0, 1, 2, 253, 254, 255]) })
await transcribeAudio({ ...session, audio: 'AAEC/f7/' })
expect(fetchMock).toHaveBeenCalledTimes(1)
const [url, options] = fetchMock.mock.calls[0]
expect(url).toBe('http://localhost:5000/api/stt/transcribe')
expect(options.headers['Content-Type']).toBe('application/json')
const body = JSON.parse(options.body)
expect(body.audioBase64).toBe(mockBase64)
expect(body.language).toBe('ko')
expect(body.initialPrompt).toBe('의료 용어 가이드')
expect(result.text).toBe('Base64 전사 성공')
expect(fetchMock).toHaveBeenCalledTimes(2)
for (const [, options] of fetchMock.mock.calls) {
expect(options.body).toBeInstanceOf(FormData)
expect((options.body as FormData).get('audio')).toBeInstanceOf(Blob)
}
})
it('서버 응답이 4xx/5xx 실패 시 에러를 throw한다', async () => {
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue({
ok: false,
status: 500,
text: async () => 'Internal STT Proxy Failure',
})
)
it('requires token, anon key and a safe gateway origin before network access', async () => {
const fetchMock = vi.fn()
vi.stubGlobal('fetch', fetchMock)
await expect(
transcribeAudio({
audio: new Blob(['data']),
apiBaseUrl: 'http://localhost:5000',
})
).rejects.toThrow('STT transcription failed (500): Internal STT Proxy Failure')
await expect(transcribeAudio({ ...session, token: '', audio: new Blob(['x']) }))
.rejects.toThrow('authenticated Supabase session')
await expect(transcribeAudio({ ...session, supabaseUrl: 'http://public.example.com', audio: new Blob(['x']) }))
.rejects.toThrow('configuration is invalid')
expect(fetchMock).not.toHaveBeenCalled()
})
it('does not expose upstream bodies and rejects malformed success payloads', async () => {
vi.stubGlobal('fetch', vi.fn(async () => ({ ok: false, status: 503, text: async () => 'secret upstream body' })))
await expect(transcribeAudio({ ...session, audio: new Blob(['x']) }))
.rejects.toThrow('STT gateway request failed (503).')
vi.stubGlobal('fetch', vi.fn(async () => ({ ok: true, status: 200, json: async () => ({ transcript: 'x' }) })))
await expect(transcribeAudio({ ...session, audio: new Blob(['x']) }))
.rejects.toThrow('invalid response')
})
})