cai
2026-07-10 a590c735cfa4ec55ba6b32e297600d83e1a3e46a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
#!/usr/bin/env node
 
import fs from 'node:fs'
import path from 'node:path'
 
const file = process.argv[2] || 'fixtures/turn-stream-happy.ndjson'
const absolute = path.resolve(file)
const raw = fs.readFileSync(absolute, 'utf8')
const lines = raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
 
const allowedEvents = new Set([
  'reply_playback_mode_selected',
  'reply_state',
  'reply_audio_chunk',
  'device_output',
  'turn_completed',
  'turn_failed',
  'turn_cancelled',
  'activity',
])
 
let playbackMode = null
let sawPlaybackStarted = false
let audioChunkCount = 0
let sawCompleted = false
let sawFailed = false
let sawCancelled = false
let lastSeq = 0
let callId = null
let traceId = null
let turnId = null
const supportedAudioFormats = new Set(['pcm_s16le', 'mp3', 'mpeg', 'wav'])
const supportedPcmSampleRates = new Set([16000, 48000])
 
function fail(message) {
  console.error(`turn stream fixture invalid: ${message}`)
  process.exit(1)
}
 
function requireSameIdentity(event) {
  if (!event.callId || !event.traceId || !event.turnId) {
    fail(`${event.event} missing callId/traceId/turnId`)
  }
  callId ??= event.callId
  traceId ??= event.traceId
  turnId ??= event.turnId
  if (event.callId !== callId || event.traceId !== traceId || event.turnId !== turnId) {
    fail(`${event.event} identity mismatch`)
  }
}
 
for (const [index, line] of lines.entries()) {
  let event
  try {
    event = JSON.parse(line)
  } catch (error) {
    fail(`line ${index + 1} is not JSON`)
  }
 
  if (!allowedEvents.has(event.event)) {
    fail(`line ${index + 1} has unsupported event ${event.event}`)
  }
  requireSameIdentity(event)
  if (!Number.isInteger(event.seq) || event.seq <= lastSeq) {
    fail(`${event.event} seq must be strictly increasing`)
  }
  lastSeq = event.seq
 
  if (event.event === 'reply_playback_mode_selected') {
    if (!['streaming_tts', 'full_tts_fallback'].includes(event.replyPlaybackMode)) {
      fail('reply_playback_mode_selected has invalid replyPlaybackMode')
    }
    playbackMode = event.replyPlaybackMode
  }
 
  if (event.event === 'reply_state') {
    if (!event.state) {
      fail('reply_state missing state')
    }
    if (event.state === 'reply_playback_started') {
      sawPlaybackStarted = true
    }
  }
 
  if (event.event === 'reply_audio_chunk') {
    if (!playbackMode) {
      fail('reply_audio_chunk arrived before reply_playback_mode_selected')
    }
    if (!sawPlaybackStarted) {
      fail('reply_audio_chunk arrived before reply_playback_started')
    }
    const chunk = event.audioChunk
    if (!chunk) {
      fail('reply_audio_chunk missing audioChunk')
    }
    if (!supportedAudioFormats.has(chunk.format)) {
      fail(`fixture fast path only accepts ${Array.from(supportedAudioFormats).join('/')}, got ${chunk.format}`)
    }
    const payload = Buffer.from(chunk.payloadBase64 || '', 'base64')
    if (payload.length === 0) {
      fail(`${chunk.format} payload must be non-empty`)
    }
    if (chunk.format === 'pcm_s16le') {
      if (!supportedPcmSampleRates.has(chunk.sampleRate) || chunk.channels !== 1) {
        fail('pcm_s16le chunk must be 16000Hz or 48000Hz mono')
      }
      if (payload.length % 2 !== 0) {
        fail('pcm_s16le payload must be 16-bit aligned')
      }
    } else {
      if (chunk.sampleRate != null && !Number.isInteger(chunk.sampleRate)) {
        fail(`${chunk.format} sampleRate must be an integer when present`)
      }
      if (chunk.channels != null && !Number.isInteger(chunk.channels)) {
        fail(`${chunk.format} channels must be an integer when present`)
      }
    }
    audioChunkCount += 1
  }
 
  if (event.event === 'turn_completed') {
    sawCompleted = true
  }
  if (event.event === 'turn_failed') {
    sawFailed = true
  }
  if (event.event === 'turn_cancelled') {
    sawCancelled = true
  }
}
 
if (lines.length === 0) {
  fail('fixture is empty')
}
if (!sawCompleted && !sawFailed && !sawCancelled) {
  fail('fixture must end with turn_completed, turn_failed or turn_cancelled')
}
if (sawCompleted && audioChunkCount === 0) {
  fail('completed streaming fixture must contain at least one audio chunk')
}
 
console.log(JSON.stringify({
  ok: true,
  file: path.relative(process.cwd(), absolute),
  eventCount: lines.length,
  audioChunkCount,
  terminal: sawCompleted ? 'turn_completed' : sawFailed ? 'turn_failed' : 'turn_cancelled',
}))