import assert from "node:assert/strict"; import { once } from "node:events"; import { createServer, type Server } from "node:http"; import { after, before, test } from "node:test"; import { AsrError, downloadTranscript, extractTranscript, getAsrConfig, MAX_BYTES, MAX_DURATION_MS, queryTranscription, submitTranscription, validateMedia, } from "../lib/stt/dashscope"; import { transcriptionState } from "../lib/stt/types"; let server: Server; let base: string; let response: unknown = { output: { task_id: "task-1" } }; let status = 200; const calls: { url?: string; headers: Record; body: string; }[] = []; const savedEnv = { ...process.env }; before(async () => { server = createServer(async (req, res) => { let body = ""; for await (const chunk of req) body += chunk; calls.push({ url: req.url, headers: req.headers, body }); res.writeHead(status, { "Content-Type": "application/json" }); res.end(JSON.stringify(response)); }).listen(0, "127.0.0.1"); await once(server, "listening"); base = `http://127.0.0.1:${(server.address() as { port: number }).port}`; process.env.DASHSCOPE_API_KEY = "test-key"; process.env.DASHSCOPE_BASE_URL = `${base}/api/v1`; delete process.env.DASHSCOPE_ASR_MODEL; }); after(async () => { process.env = savedEnv; server.closeAllConnections(); await new Promise((resolve) => server.close(() => resolve())); }); test("native ASR submission sends only audio URL and recognition options", async () => { assert.equal( await submitTranscription("https://storage.test/audio.mp3"), "task-1", ); const call = calls.at(-1); assert.ok(call); assert.equal(call.url, "/api/v1/services/audio/asr/transcription"); assert.equal(call.headers.authorization, "Bearer test-key"); assert.equal(call.headers["x-dashscope-async"], "enable"); assert.deepEqual(JSON.parse(call.body), { model: "qwen3-asr-flash-filetrans", input: { file_url: "https://storage.test/audio.mp3" }, parameters: { channel_id: [0], enable_itn: false, enable_words: false }, }); }); test("query pending, running, success, failure and unknown", async () => { for (const state of ["PENDING", "RUNNING"] as const) { response = { output: { task_status: state } }; assert.deepEqual(await queryTranscription("task-1"), { status: state }); } response = { output: { task_status: "SUCCEEDED", result: { transcription_url: `${base}/result` }, }, }; assert.deepEqual(await queryTranscription("task-1"), { status: "SUCCEEDED", resultURL: `${base}/result`, }); response = { output: { task_status: "FAILED", code: "FILE_403_FORBIDDEN", message: "secret-url", }, }; await assert.rejects(queryTranscription("task-1"), /FILE_403_FORBIDDEN/); response = { output: { task_status: "UNKNOWN" } }; await assert.rejects(queryTranscription("task-1"), /UNKNOWN/); }); test("extract only text, retain order, fall back to full text", async () => { response = { transcripts: [ { sentences: [ { text: " 第一段 ", emotion: "happy", language: "zh" }, { text: "第二段" }, ], text: "全文", }, { sentences: [], text: "备用全文" }, ], }; assert.deepEqual(await downloadTranscript(`${base}/result`), [ "第一段", "第二段", "备用全文", ]); assert.equal(calls.at(-1)?.headers.authorization, undefined); assert.deepEqual(extractTranscript({ transcripts: [] }), []); assert.deepEqual( extractTranscript({ transcripts: [{ text: " ", sentences: [] }] }), [], ); assert.throws(() => extractTranscript({}), /transcripts/); assert.throws( () => extractTranscript({ transcripts: [{ sentences: [{ emotion: "happy" }] }], }), /文字/, ); }); test("limits are inclusive and oversized media fails without truncation", () => { validateMedia(MAX_DURATION_MS, MAX_BYTES); assert.throws(() => validateMedia(MAX_DURATION_MS + 1, 1), /12 小时/); assert.throws(() => validateMedia(1, MAX_BYTES + 1), /2 GB/); assert.throws(() => validateMedia(1, 0), /为空/); }); test("HTTP throttling can retry; authentication cannot; no provider error leakage", async () => { response = { code: "InvalidApiKey", message: "sensitive" }; status = 401; await assert.rejects( queryTranscription("task-1"), (e) => e instanceof AsrError && !e.retryable && !e.message.includes("sensitive"), ); status = 429; await assert.rejects( queryTranscription("task-1"), (e) => e instanceof AsrError && e.retryable, ); status = 200; }); test("missing key and incompatible model produce explicit errors", () => { delete process.env.DASHSCOPE_API_KEY; assert.throws(getAsrConfig, /DASHSCOPE_API_KEY/); process.env.DASHSCOPE_API_KEY = "test-key"; process.env.DASHSCOPE_ASR_MODEL = "gemini-2.5-flash"; assert.throws(getAsrConfig, /qwen3-asr-flash-filetrans/); delete process.env.DASHSCOPE_ASR_MODEL; }); test("historical empty transcript is complete; failed retry retains old text", () => { assert.equal( transcriptionState({ transcript: [] }, null).status, "SUCCEEDED", ); assert.deepEqual( transcriptionState( { transcript: ["旧文字"] }, { status: "FAILED", lastError: "失败" }, ), { status: "FAILED", error: "失败", transcript: ["旧文字"], }, ); });