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
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue