| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114 |
- import assert from 'node:assert/strict'
- import { readFile } from 'node:fs/promises'
- import ts from 'typescript'
- const sourceUrl = new URL('../src/views/Editor/EnglishSpeaking/services/articleReadingStream.ts', import.meta.url)
- let source = await readFile(sourceUrl, 'utf8')
- source = source.replace(
- "import { buildArticleWsUrl } from './speakingApiConfig'",
- "const buildArticleWsUrl = path => `wss://example.test/api/speaking/article${path}`",
- )
- const compiled = ts.transpileModule(source, {
- compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 },
- }).outputText
- class FakeWebSocket {
- static CONNECTING = 0
- static OPEN = 1
- static CLOSING = 2
- static CLOSED = 3
- static instances = []
- readyState = FakeWebSocket.CONNECTING
- binaryType = ''
- sent = []
- onopen = null
- onmessage = null
- onerror = null
- onclose = null
- constructor(url) {
- this.url = url
- FakeWebSocket.instances.push(this)
- }
- send(payload) {
- this.sent.push(payload)
- }
- close() {
- this.readyState = FakeWebSocket.CLOSED
- this.onclose?.({})
- }
- }
- globalThis.WebSocket = FakeWebSocket
- const mod = await import(`data:text/javascript,${encodeURIComponent(compiled)}`)
- // ---- 1. 正常时序:start → PCM → stop → ack ----
- // chunk 先于 open 抵达,必须排队而不是丢弃(录音器早于 socket 就绪是常态)
- const stream = mod.openArticleParagraphStream({
- sessionId: 's1',
- paragraphIdx: 2,
- sampleRate: 48000,
- })
- const firstChunk = new Uint8Array([1, 2, 3, 4]).buffer
- stream.pushChunk(firstChunk)
- const ackPromise = stream.stop()
- const ws = FakeWebSocket.instances[0]
- assert.equal(ws.url, 'wss://example.test/api/speaking/article/session/s1/paragraph/2/stream')
- ws.readyState = FakeWebSocket.OPEN
- ws.onopen()
- assert.deepEqual(JSON.parse(ws.sent[0]), {
- type: 'start', sampleRate: 48000, bits: 16, channels: 1,
- })
- assert.equal(ws.sent[1], firstChunk)
- assert.deepEqual(JSON.parse(ws.sent[2]), { type: 'stop' })
- ws.onmessage({ data: JSON.stringify({ type: 'ack', status: 'generating', attempt: 1, truncated: false }) })
- assert.deepEqual(await ackPromise, {
- type: 'ack', status: 'generating', attempt: 1, truncated: false,
- })
- // ---- 2. error frame 以其 message reject ----
- const errStream = mod.openArticleParagraphStream({ sessionId: 's2', paragraphIdx: 0, sampleRate: 16000 })
- const errAck = errStream.stop()
- const errWs = FakeWebSocket.instances[1]
- errWs.readyState = FakeWebSocket.OPEN
- errWs.onopen()
- errWs.onmessage({ data: JSON.stringify({ type: 'error', message: 'assessment failed' }) })
- await assert.rejects(errAck, /assessment failed/)
- // ---- 3. abort() 关闭连接并 reject 未决的 stop ----
- const abortStream = mod.openArticleParagraphStream({ sessionId: 's3', paragraphIdx: 1, sampleRate: 16000 })
- const abortWs = FakeWebSocket.instances[2]
- abortWs.readyState = FakeWebSocket.OPEN
- abortWs.onopen()
- const abortAck = abortStream.stop()
- abortStream.abort('user left the paragraph')
- assert.equal(abortWs.readyState, FakeWebSocket.CLOSED)
- await assert.rejects(abortAck, /user left the paragraph/)
- // ---- 4. 连接在 ack 之前关闭必须 reject,不能静默挂起 ----
- const deadStream = mod.openArticleParagraphStream({ sessionId: 's4', paragraphIdx: 0, sampleRate: 16000 })
- const deadWs = FakeWebSocket.instances[3]
- deadWs.readyState = FakeWebSocket.OPEN
- deadWs.onopen()
- const deadAck = deadStream.stop()
- deadWs.close()
- await assert.rejects(deadAck, /closed before acknowledgement/)
- // ---- 5. stop 之后再推 PCM 是编程错误 ----
- const lateStream = mod.openArticleParagraphStream({ sessionId: 's5', paragraphIdx: 0, sampleRate: 16000 })
- const lateWs = FakeWebSocket.instances[4]
- lateWs.readyState = FakeWebSocket.OPEN
- lateWs.onopen()
- const lateAck = lateStream.stop()
- assert.throws(() => lateStream.pushChunk(new Uint8Array([9]).buffer), /Cannot push PCM after stop/)
- lateWs.onmessage({ data: JSON.stringify({ type: 'ack', status: 'generating', attempt: 1, truncated: false }) })
- await lateAck
|