import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import test from 'node:test'; import vm from 'node:vm'; import ts from 'typescript'; const loadModule = async (sandbox = {}) => { const service = await readFile(new URL('../src/services/speech-audio-source.ts', import.meta.url), 'utf8'); const compiled = ts.transpileModule(service, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 } }); const runtimeModule = { exports: {} }; vm.runInNewContext(compiled.outputText, { exports: runtimeModule.exports, module: runtimeModule, atob: (value) => Buffer.from(value, 'base64').toString('binary'), Blob, URL, Date, ...sandbox }); return runtimeModule.exports; }; // 'ID3' 是 mp3 文件头标识,base64 为 SUQz const MP3_DATA_URI = `data:audio/mp3;base64,${Buffer.from('ID3').toString('base64')}`; test('识别音频 data: URI,拒绝 http URL 与非音频 data URI', async () => { const runtime = await loadModule(); assert.equal(runtime.isDataAudioUri(MP3_DATA_URI), true); assert.equal(runtime.isDataAudioUri('data:audio/mpeg;base64,SUQz'), true); assert.equal(runtime.isDataAudioUri('https://example.com/a.mp3'), false); assert.equal(runtime.isDataAudioUri('data:image/png;base64,SUQz'), false); assert.equal(runtime.isDataAudioUri(''), false); }); test('解码 data: URI 为字节,保留 mime 类型', async () => { const runtime = await loadModule(); const decoded = runtime.decodeDataAudioUri(MP3_DATA_URI); assert.equal(decoded.mime, 'audio/mp3'); assert.deepEqual(Array.from(decoded.bytes.slice(0, 3)), [0x49, 0x44, 0x33]); assert.equal(runtime.decodeDataAudioUri('data:audio/mp3;base64,!!!'), null); assert.equal(runtime.decodeDataAudioUri('not-a-data-uri'), null); }); test('H5 环境转换为 Blob URL 并可回收', async () => { const created = []; const fakeURL = { createObjectURL: (blob) => { created.push(blob); return 'blob:mock-1'; } }; const runtime = await loadModule({ URL: fakeURL }); const blobUrl = runtime.dataUriToBlobUrl(MP3_DATA_URI); assert.equal(blobUrl, 'blob:mock-1'); assert.equal(created.length, 1); assert.equal(created[0].type, 'audio/mp3'); assert.equal(created[0].size, 6); }); test('App 环境:base64 落临时文件后返回 file:// 播放路径', async () => { const writes = []; const plus = { io: { resolveLocalFileSystemURL: (path, ok) => { assert.equal(path, '_doc/'); ok({ getFile: (name, flags, onFile) => { assert.equal(flags.create, true); assert.match(name, /^aihr-tts-\d+\.mp3$/); onFile({ fullPath: `_doc/${name}`, createWriter: (onWriter) => onWriter({ onwriteend: null, onerror: null, write(blob) { writes.push(blob); this.onwriteend(); } }) }); } }); }, convertLocalFileSystemURL: (path) => `file:///storage/emulated/0/${path}` } }; const runtime = await loadModule({ plus }); const fileUrl = await runtime.writeNativeTempAudio(MP3_DATA_URI); assert.match(fileUrl, /^file:\/\/\/storage\/emulated\/0\/_doc\/aihr-tts-\d+\.mp3$/); assert.equal(writes.length, 1); assert.equal(writes[0].type, 'audio/mp3'); assert.equal(runtime.hasNativeAudioFileSupport(), true); }); test('App 环境:写盘失败或无 plus 环境时返回 null,清理尽力而为', async () => { const failingPlus = { io: { resolveLocalFileSystemURL: (path, ok, fail) => fail(new Error('io error')), convertLocalFileSystemURL: (path) => path } }; const runtime = await loadModule({ plus: failingPlus }); assert.equal(await runtime.writeNativeTempAudio(MP3_DATA_URI), null); const webRuntime = await loadModule(); assert.equal(webRuntime.hasNativeAudioFileSupport(), false); assert.equal(await webRuntime.writeNativeTempAudio(MP3_DATA_URI), null); webRuntime.removeNativeTempAudio('file:///nonexistent.mp3'); });