d3ro-voice/packages/api-client/__tests__/public-surface-r2-35.test.ts
Yun Chan ba9ef9741e fix: red-team round 3 hardening across desktop, mobile, core and server
Batch of red-team r3 fixes that were in the working tree before the
2026-09-28 design overhaul, committed as one unit with their tests.

- desktop main: STT timeouts and sidecar, voice recording store, sync
  (credentials, audio, knowledge reindex, push gates), runtime
  provisioner, update policy, AltGr keybindings, voice-command policy,
  dictionary file codec/limits, meeting transcript condensing and a
  local recording ledger so interrupted-session recovery only closes
  meetings this device recorded (a phone's live meeting is left alone).
- mobile: login CSRF via implicit token callbacks rejected, account
  deletion/retention, durable queue retention, knowledge realtime
  without unfiltered DELETE, meeting re-record failure paths, cloud STT
  client, preferences store/resync.
- core: text chunking splits long unbroken transcripts to fit, template
  field policy, dictionary limits, meeting markdown inline handling.
- server: payple webhook policy and cancellation order scope, meeting
  document generation quota, team RPC null-role guard, unified LLM
  quota in-flight accounting, knowledge chunk vector index, meeting
  re-record failure paths (migrations 20260929*).
- ci: portable/runtime feed gates, update-policy schema, Forgejo file
  delete and alias planning.

Four older tests are updated to the new contracts rather than the old
behavior: token-pair auth callbacks are rejected, knowledge realtime no
longer subscribes to DELETE, long transcript lines are split, and
meeting recovery requires the local recording ledger for empty rows.
2026-09-28 20:45:52 +09:00

118 lines
4 KiB
TypeScript

// 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<string, ExportConditions | string>
}
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<string, unknown>
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<TranscribeAudioResult>().toHaveProperty('text').toEqualTypeOf<string>()
expectTypeOf<TranscribeAudioResult>().toHaveProperty('durationSeconds').toEqualTypeOf<number>()
})
})
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()
}
})
})