// packages/api-client/__tests__/public-surface-r2-35.test.ts // 루트 barrel은 공유 계약(타입 + 클라이언트 팩토리)만 공개해야 한다. // 앱마다 따로 소유하는 저장소·쿼터·STT 호출 헬퍼가 '정본처럼' 다시 노출되지 않도록 고정한다. import { existsSync, readFileSync } from 'node:fs' import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { describe, expect, expectTypeOf, it, vi } from 'vitest' import * as apiClient from '../src/index' import type { TranscribeAudioResult } from '../src/index' const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') interface ExportConditions { types?: string default?: string } interface PackageManifest { main?: string types?: string exports: Record } function readManifest(): PackageManifest { return JSON.parse(readFileSync(resolve(PACKAGE_ROOT, 'package.json'), 'utf8')) as PackageManifest } function exportTargets(entry: ExportConditions | string): string[] { return typeof entry === 'string' ? [entry] : Object.values(entry).filter((v): v is string => typeof v === 'string') } describe('@d3ro/api-client exports map', () => { it('publishes exactly the shared-contract subpaths', () => { expect(Object.keys(readManifest().exports).sort()).toEqual([ '.', './client', './supabase-browser', './supabase-server', ]) }) it('does not publish app-owned runtime policy subpaths', () => { const subpaths = Object.keys(readManifest().exports) for (const removed of ['./auth', './meetings', './history', './usage', './transcribe']) { expect(subpaths, removed).not.toContain(removed) } }) it('points every declared target (exports, main, types) at an existing file', () => { const manifest = readManifest() const targets = [ ...Object.values(manifest.exports).flatMap(exportTargets), ...(manifest.main ? [manifest.main] : []), ...(manifest.types ? [manifest.types] : []), ] expect(targets.length).toBeGreaterThan(0) for (const target of targets) { expect(existsSync(resolve(PACKAGE_ROOT, target)), target).toBe(true) } }) }) describe('@d3ro/api-client root surface', () => { it('exposes only the client factory runtime values', () => { expect(Object.keys(apiClient).sort()).toEqual([ 'SupabaseConfigurationError', 'createD3roSupabaseClient', 'isClientConfigured', 'requireSupabasePublicConfig', ]) }) it('does not re-export repository, quota, auth or STT call helpers', () => { const exported = apiClient as Record for (const name of [ 'listHistory', 'createHistoryEntry', 'deleteHistoryEntry', 'getTodayUsage', 'listUsageLastDays', 'getSubscription', 'listMeetings', 'updateMeeting', 'appendTranscript', 'signInWithOAuth', 'getSession', 'transcribeAudio', 'getSupabaseBrowserClient', 'createSupabaseServerClient', ]) { expect(exported[name], name).toBeUndefined() } }) it('keeps the STT result contract available as a type', () => { expectTypeOf().toHaveProperty('text').toEqualTypeOf() expectTypeOf().toHaveProperty('durationSeconds').toEqualTypeOf() }) }) describe('transcribeAudio reference client', () => { it('does not fall back to process.env for gateway configuration', async () => { vi.stubEnv('NEXT_PUBLIC_SUPABASE_URL', 'https://env.supabase.co') vi.stubEnv('NEXT_PUBLIC_SUPABASE_ANON_KEY', 'env-anon-key') const fetchMock = vi.fn() vi.stubGlobal('fetch', fetchMock) try { const { transcribeAudio } = await import('../src/transcribe') await expect(transcribeAudio({ token: 'user-token', audio: new Blob(['x']) })) .rejects.toThrow('authenticated Supabase session') expect(fetchMock).not.toHaveBeenCalled() } finally { vi.unstubAllEnvs() vi.unstubAllGlobals() } }) })