import { expect, test } from '@playwright/test' import { transcribeWebAudio, WebSttError } from '../src/lib/web-stt-client' const audio = new Blob([new Uint8Array([0x1a, 0x45, 0xdf, 0xa3])], { type: 'audio/webm;codecs=opus' }) test.describe('web STT fail-closed client', () => { test('sends one authenticated request and accepts only a strict real transcript', async () => { let calls = 0 const result = await transcribeWebAudio({ audio, accessToken: 'access-token', supabaseUrl: 'https://project.supabase.co', fetchImpl: async (input, init) => { calls += 1 expect(String(input)).toBe('https://project.supabase.co/functions/v1/stt-proxy') expect(init?.headers).toEqual({ Authorization: 'Bearer access-token' }) expect(init?.body).toBeInstanceOf(FormData) return new Response(JSON.stringify({ transcript: '실제 전사 결과', confidence: 0.97, language_code: 'ko', duration_seconds: 1.25, provider: 'whisper.cpp-local', }), { status: 200, headers: { 'Content-Type': 'application/json' } }) }, }) expect(calls).toBe(1) expect(result.transcript).toBe('실제 전사 결과') }) test('maps auth, quota, provider and upstream failures without a fallback request', async () => { for (const [status, error, code] of [ [401, 'invalid_token', 'auth_required'], [429, 'quota_exceeded', 'quota_exceeded'], [503, 'stt_provider_unavailable', 'provider_unavailable'], [502, 'stt_upstream_failed', 'upstream_failed'], ] as const) { let calls = 0 const rejection = transcribeWebAudio({ audio, accessToken: 'token', supabaseUrl: 'https://project.supabase.co', fetchImpl: async () => { calls += 1 return new Response(JSON.stringify({ error }), { status }) }, }) await expect(rejection).rejects.toMatchObject({ code }) expect(calls).toBe(1) } }) test('rejects empty, oversized, wrong MIME, malformed success and insecure endpoints', async () => { const noFetch = async (): Promise => { throw new Error('must not fetch') } const invalidCalls = [ transcribeWebAudio({ audio: new Blob([], { type: 'audio/webm' }), accessToken: 't', supabaseUrl: 'https://p.supabase.co', fetchImpl: noFetch }), transcribeWebAudio({ audio: new Blob([new Uint8Array(25 * 1024 * 1024 + 1)], { type: 'audio/webm' }), accessToken: 't', supabaseUrl: 'https://p.supabase.co', fetchImpl: noFetch }), transcribeWebAudio({ audio: new Blob(['x'], { type: 'audio/mpeg' }), accessToken: 't', supabaseUrl: 'https://p.supabase.co', fetchImpl: noFetch }), transcribeWebAudio({ audio, accessToken: 't', supabaseUrl: 'http://remote.example', fetchImpl: noFetch }), transcribeWebAudio({ audio, accessToken: 't', supabaseUrl: 'https://p.supabase.co', fetchImpl: async () => new Response(JSON.stringify({ transcript: 'placeholder' }), { status: 200 }), }), ] for (const call of invalidCalls) await expect(call).rejects.toBeInstanceOf(WebSttError) }) test('aborts the only provider request as cancelled', async () => { const controller = new AbortController() const promise = transcribeWebAudio({ audio, accessToken: 'token', supabaseUrl: 'https://project.supabase.co', signal: controller.signal, fetchImpl: async (_input, init) => new Promise((_resolve, reject) => { init?.signal?.addEventListener('abort', () => reject(new DOMException('aborted', 'AbortError')), { once: true }) }), }) controller.abort() await expect(promise).rejects.toMatchObject({ code: 'cancelled' }) }) })