From 87826bc427ae975c8d748bfc5f6bd6d59f5df148 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Mon, 28 Sep 2026 00:54:20 +0400 Subject: [PATCH] test: live AITUNNEL integration test, skipped without AITUNNEL_API_KEY Refs #5 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QKHeGuAYh4W1jNdjMConZa --- tests/integration/aitunnel.test.ts | 39 ++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 tests/integration/aitunnel.test.ts diff --git a/tests/integration/aitunnel.test.ts b/tests/integration/aitunnel.test.ts new file mode 100644 index 0000000..1151638 --- /dev/null +++ b/tests/integration/aitunnel.test.ts @@ -0,0 +1,39 @@ +import { readFileSync } from 'node:fs'; +import { describe, expect, it } from 'vitest'; +import { DEFAULT_OPENAI, OpenAiTranscriber } from '../../src/core/openai'; +import type { HttpClient } from '../../src/core/ports'; + +// Ключ — только из окружения, никогда в репозитории. +const KEY = process.env.AITUNNEL_API_KEY ?? ''; + +// Под Node нет requestUrl: тот же порт на fetch. contentType уходит заголовком. +const fetchHttp: HttpClient = { + async post(req) { + const res = await fetch(req.url, { + method: 'POST', + headers: { ...req.headers, 'Content-Type': req.contentType }, + body: req.body, + }); + return { status: res.status, text: await res.text() }; + }, +}; + +// Характерные слова эталона tests/fixtures/test-01.txt. +const WORDS = ['тестовая', 'запись', 'проверки', 'распознавания', 'речи', 'whisper']; + +describe.skipIf(!KEY)('AITUNNEL gpt-4o-transcribe (live)', () => { + it('transcribes test-01.m4a', { timeout: 120_000 }, async () => { + const audio = new Uint8Array(readFileSync(new URL('../fixtures/test-01.m4a', import.meta.url))).buffer; + const reference = readFileSync(new URL('../fixtures/test-01.txt', import.meta.url), 'utf8').toLowerCase(); + for (const word of WORDS) expect(reference).toContain(word); + + const t = new OpenAiTranscriber(fetchHttp, () => ({ ...DEFAULT_OPENAI, apiKey: KEY }), Math.random); + const result = await t.transcribe(audio, 'audio/mp4', new AbortController().signal); + + expect(result).toMatchObject({ ok: true }); + const text = result.ok ? result.text.toLowerCase() : ''; + expect(text.trim()).not.toBe(''); + const hits = WORDS.filter((w) => text.includes(w)); + expect(hits.length, `распознано: ${text}`).toBeGreaterThanOrEqual(4); + }); +});