22import * as fs from "node:fs" ;
33import * as path from "node:path" ;
44import { afterAll , afterEach , beforeEach , describe , expect , it , vi } from "vitest" ;
5- import { createTempDirTracker } from "../../../../../ test/helpers/temp-dir.js " ;
5+ import { withTempDir } from "openclaw/plugin-sdk/ test-env " ;
66
77const ssrfRuntimeMocks = vi . hoisted ( ( ) => ( {
88 fetchWithSsrFGuard : vi . fn ( ) ,
@@ -19,8 +19,6 @@ afterAll(() => {
1919
2020import { resolveSTTConfig , transcribeAudio } from "./stt.js" ;
2121
22- const tempDirs = createTempDirTracker ( ) ;
23-
2422function cancelTrackedResponse (
2523 text : string ,
2624 init : ResponseInit ,
@@ -71,7 +69,6 @@ describe("engine/utils/stt", () => {
7169 } ) ;
7270
7371 afterEach ( ( ) => {
74- tempDirs . cleanup ( ) ;
7572 ssrfRuntimeMocks . fetchWithSsrFGuard . mockReset ( ) ;
7673 vi . unstubAllGlobals ( ) ;
7774 } ) ;
@@ -135,70 +132,19 @@ describe("engine/utils/stt", () => {
135132 } ) ;
136133
137134 it ( "posts audio to OpenAI-compatible transcription endpoint" , async ( ) => {
138- const tmpDir = tempDirs . make ( "openclaw-qqbot-stt-" ) ;
139- const audioPath = path . join ( tmpDir , "voice.wav" ) ;
140- fs . writeFileSync ( audioPath , Buffer . from ( [ 1 , 2 , 3 , 4 ] ) ) ;
141-
142- const release = vi . fn ( async ( ) => { } ) ;
143- ssrfRuntimeMocks . fetchWithSsrFGuard . mockResolvedValueOnce ( {
144- response : Response . json ( {
145- text : "hello from audio" ,
146- } ) ,
147- release,
148- } ) ;
149-
150- const transcript = await transcribeAudio ( audioPath , {
151- channels : {
152- qqbot : {
153- stt : {
154- baseUrl : "https://api.example.test/v1/" ,
155- apiKey : "secret" ,
156- model : "whisper-1" ,
157- } ,
158- } ,
159- } ,
160- } ) ;
161-
162- expect ( transcript ) . toBe ( "hello from audio" ) ;
163- expect ( ssrfRuntimeMocks . fetchWithSsrFGuard ) . toHaveBeenCalledTimes ( 1 ) ;
164- const request = requireFirstSsrfRequest ( ) ;
165- expect ( request . url ) . toBe ( "https://api.example.test/v1/audio/transcriptions" ) ;
166- expect ( request . auditContext ) . toBe ( "qqbot-stt" ) ;
167- expect ( request . init ?. method ) . toBe ( "POST" ) ;
168- expect ( request . init ?. headers ) . toEqual ( { Authorization : "Bearer secret" } ) ;
169- expect ( request . init ?. body ) . toBeInstanceOf ( FormData ) ;
170- const body = request . init ?. body as FormData ;
171- expect ( body . get ( "model" ) ) . toBe ( "whisper-1" ) ;
172- const file = body . get ( "file" ) ;
173- expect ( file ) . toBeInstanceOf ( File ) ;
174- expect ( ( file as File ) . name ) . toBe ( "voice.wav" ) ;
175- expect ( ( file as File ) . type ) . toBe ( "audio/wav" ) ;
176- expect ( new Uint8Array ( await ( file as File ) . arrayBuffer ( ) ) ) . toEqual (
177- new Uint8Array ( [ 1 , 2 , 3 , 4 ] ) ,
178- ) ;
179- expect ( release ) . toHaveBeenCalledTimes ( 1 ) ;
180- } ) ;
181-
182- it ( "bounds STT error bodies without using response.text()" , async ( ) => {
183- const tmpDir = tempDirs . make ( "openclaw-qqbot-stt-error-" ) ;
184- const audioPath = path . join ( tmpDir , "voice.wav" ) ;
185- fs . writeFileSync ( audioPath , Buffer . from ( [ 1 , 2 , 3 , 4 ] ) ) ;
186-
187- const release = vi . fn ( async ( ) => { } ) ;
188- const tracked = cancelTrackedResponse ( `${ "stt provider unavailable " . repeat ( 1024 ) } tail` , {
189- status : 503 ,
190- statusText : "Service Unavailable" ,
191- headers : { "content-type" : "text/plain" } ,
192- } ) ;
193- const textSpy = vi . spyOn ( tracked . response , "text" ) . mockRejectedValue ( new Error ( "unbounded" ) ) ;
194- ssrfRuntimeMocks . fetchWithSsrFGuard . mockResolvedValueOnce ( {
195- response : tracked . response ,
196- release,
197- } ) ;
135+ await withTempDir ( "openclaw-qqbot-stt-" , async ( tmpDir ) => {
136+ const audioPath = path . join ( tmpDir , "voice.wav" ) ;
137+ fs . writeFileSync ( audioPath , Buffer . from ( [ 1 , 2 , 3 , 4 ] ) ) ;
138+
139+ const release = vi . fn ( async ( ) => { } ) ;
140+ ssrfRuntimeMocks . fetchWithSsrFGuard . mockResolvedValueOnce ( {
141+ response : Response . json ( {
142+ text : "hello from audio" ,
143+ } ) ,
144+ release,
145+ } ) ;
198146
199- let error : unknown ;
200- try {
201- await transcribeAudio ( audioPath , {
147+ const transcript = await transcribeAudio ( audioPath , {
202148 channels : {
203149 qqbot : {
204150 stt : {
@@ -209,14 +155,67 @@ describe("engine/utils/stt", () => {
209155 } ,
210156 } ,
211157 } ) ;
212- } catch ( caught ) {
213- error = caught ;
214- }
215-
216- expect ( String ( error ) ) . toContain ( "STT failed (HTTP 503): stt provider unavailable" ) ;
217- expect ( String ( error ) ) . not . toContain ( "tail" ) ;
218- expect ( tracked . wasCanceled ( ) ) . toBe ( true ) ;
219- expect ( textSpy ) . not . toHaveBeenCalled ( ) ;
220- expect ( release ) . toHaveBeenCalledTimes ( 1 ) ;
158+
159+ expect ( transcript ) . toBe ( "hello from audio" ) ;
160+ expect ( ssrfRuntimeMocks . fetchWithSsrFGuard ) . toHaveBeenCalledTimes ( 1 ) ;
161+ const request = requireFirstSsrfRequest ( ) ;
162+ expect ( request . url ) . toBe ( "https://api.example.test/v1/audio/transcriptions" ) ;
163+ expect ( request . auditContext ) . toBe ( "qqbot-stt" ) ;
164+ expect ( request . init ?. method ) . toBe ( "POST" ) ;
165+ expect ( request . init ?. headers ) . toEqual ( { Authorization : "Bearer secret" } ) ;
166+ expect ( request . init ?. body ) . toBeInstanceOf ( FormData ) ;
167+ const body = request . init ?. body as FormData ;
168+ expect ( body . get ( "model" ) ) . toBe ( "whisper-1" ) ;
169+ const file = body . get ( "file" ) ;
170+ expect ( file ) . toBeInstanceOf ( File ) ;
171+ expect ( ( file as File ) . name ) . toBe ( "voice.wav" ) ;
172+ expect ( ( file as File ) . type ) . toBe ( "audio/wav" ) ;
173+ expect ( new Uint8Array ( await ( file as File ) . arrayBuffer ( ) ) ) . toEqual (
174+ new Uint8Array ( [ 1 , 2 , 3 , 4 ] ) ,
175+ ) ;
176+ expect ( release ) . toHaveBeenCalledTimes ( 1 ) ;
177+ } ) ;
178+ } ) ;
179+
180+ it ( "bounds STT error bodies without using response.text()" , async ( ) => {
181+ await withTempDir ( "openclaw-qqbot-stt-error-" , async ( tmpDir ) => {
182+ const audioPath = path . join ( tmpDir , "voice.wav" ) ;
183+ fs . writeFileSync ( audioPath , Buffer . from ( [ 1 , 2 , 3 , 4 ] ) ) ;
184+
185+ const release = vi . fn ( async ( ) => { } ) ;
186+ const tracked = cancelTrackedResponse ( `${ "stt provider unavailable " . repeat ( 1024 ) } tail` , {
187+ status : 503 ,
188+ statusText : "Service Unavailable" ,
189+ headers : { "content-type" : "text/plain" } ,
190+ } ) ;
191+ const textSpy = vi . spyOn ( tracked . response , "text" ) . mockRejectedValue ( new Error ( "unbounded" ) ) ;
192+ ssrfRuntimeMocks . fetchWithSsrFGuard . mockResolvedValueOnce ( {
193+ response : tracked . response ,
194+ release,
195+ } ) ;
196+
197+ let error : unknown ;
198+ try {
199+ await transcribeAudio ( audioPath , {
200+ channels : {
201+ qqbot : {
202+ stt : {
203+ baseUrl : "https://api.example.test/v1/" ,
204+ apiKey : "secret" ,
205+ model : "whisper-1" ,
206+ } ,
207+ } ,
208+ } ,
209+ } ) ;
210+ } catch ( caught ) {
211+ error = caught ;
212+ }
213+
214+ expect ( String ( error ) ) . toContain ( "STT failed (HTTP 503): stt provider unavailable" ) ;
215+ expect ( String ( error ) ) . not . toContain ( "tail" ) ;
216+ expect ( tracked . wasCanceled ( ) ) . toBe ( true ) ;
217+ expect ( textSpy ) . not . toHaveBeenCalled ( ) ;
218+ expect ( release ) . toHaveBeenCalledTimes ( 1 ) ;
219+ } ) ;
221220 } ) ;
222221} ) ;
0 commit comments