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.
118 lines
4 KiB
TypeScript
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()
|
|
}
|
|
})
|
|
})
|