@@ -199,11 +199,46 @@ async function startBin(
199
199
} ;
200
200
}
201
201
202
+ async function runStdioDiscovery (
203
+ modulePath : string
204
+ ) : Promise < { stdout : string ; stderr : string ; exitCode : number | null } > {
205
+ return new Promise ( ( resolve , reject ) => {
206
+ const proc = subprocess . spawn ( "npx" , [ "firebase-functions" ] , {
207
+ cwd : path . resolve ( modulePath ) ,
208
+ env : {
209
+ PATH : process . env . PATH ,
210
+ GCLOUD_PROJECT : "test-project" ,
211
+ FUNCTIONS_CONTROL_API : "true" ,
212
+ FUNCTIONS_DISCOVERY_MODE : "stdio" ,
213
+ } ,
214
+ } ) ;
215
+
216
+ let stdout = "" ;
217
+ let stderr = "" ;
218
+
219
+ proc . stdout ?. on ( "data" , ( chunk : Buffer ) => {
220
+ stdout += chunk . toString ( "utf8" ) ;
221
+ } ) ;
222
+
223
+ proc . stderr ?. on ( "data" , ( chunk : Buffer ) => {
224
+ stderr += chunk . toString ( "utf8" ) ;
225
+ } ) ;
226
+
227
+ proc . on ( "close" , ( code ) => {
228
+ resolve ( { stdout, stderr, exitCode : code } ) ;
229
+ } ) ;
230
+
231
+ proc . on ( "error" , ( err ) => {
232
+ reject ( err ) ;
233
+ } ) ;
234
+ } ) ;
235
+ }
236
+
202
237
describe ( "functions.yaml" , function ( ) {
203
238
// eslint-disable-next-line @typescript-eslint/no-invalid-this
204
239
this . timeout ( TIMEOUT_XL ) ;
205
240
206
- function runTests ( tc : Testcase ) {
241
+ function runHttpDiscoveryTests ( tc : Testcase ) {
207
242
let port : number ;
208
243
let cleanup : ( ) => Promise < void > ;
209
244
@@ -233,6 +268,31 @@ describe("functions.yaml", function () {
233
268
} ) ;
234
269
}
235
270
271
+ function runStdioDiscoveryTests ( tc : Testcase ) {
272
+ it ( "discovers functions via stdio" , async function ( ) {
273
+ // eslint-disable-next-line @typescript-eslint/no-invalid-this
274
+ this . timeout ( TIMEOUT_M ) ;
275
+
276
+ const result = await runStdioDiscovery ( tc . modulePath ) ;
277
+
278
+ // Should exit successfully
279
+ expect ( result . exitCode ) . to . equal ( 0 ) ;
280
+
281
+ // Should not start HTTP server
282
+ expect ( result . stdout ) . to . not . contain ( "Serving at port" ) ;
283
+
284
+ // Should output manifest to stderr
285
+ const manifestMatch = result . stderr . match ( / _ _ F I R E B A S E _ F U N C T I O N S _ M A N I F E S T _ _ : ( .+ ) / ) ;
286
+ expect ( manifestMatch ) . to . not . be . null ;
287
+
288
+ // Decode and verify manifest
289
+ const base64 = manifestMatch ! [ 1 ] ;
290
+ const manifestJson = Buffer . from ( base64 , "base64" ) . toString ( "utf8" ) ;
291
+ const manifest = JSON . parse ( manifestJson ) ;
292
+ expect ( manifest ) . to . deep . equal ( tc . expected ) ;
293
+ } ) ;
294
+ }
295
+
236
296
describe ( "commonjs" , function ( ) {
237
297
// eslint-disable-next-line @typescript-eslint/no-invalid-this
238
298
this . timeout ( TIMEOUT_L ) ;
@@ -320,7 +380,13 @@ describe("functions.yaml", function () {
320
380
321
381
for ( const tc of testcases ) {
322
382
describe ( tc . name , ( ) => {
323
- runTests ( tc ) ;
383
+ describe ( "http discovery" , ( ) => {
384
+ runHttpDiscoveryTests ( tc ) ;
385
+ } ) ;
386
+
387
+ describe ( "stdio discovery" , ( ) => {
388
+ runStdioDiscoveryTests ( tc ) ;
389
+ } ) ;
324
390
} ) ;
325
391
}
326
392
} ) ;
@@ -350,8 +416,48 @@ describe("functions.yaml", function () {
350
416
351
417
for ( const tc of testcases ) {
352
418
describe ( tc . name , ( ) => {
353
- runTests ( tc ) ;
419
+ describe ( "http discovery" , ( ) => {
420
+ runHttpDiscoveryTests ( tc ) ;
421
+ } ) ;
422
+
423
+ describe ( "stdio discovery" , ( ) => {
424
+ runStdioDiscoveryTests ( tc ) ;
425
+ } ) ;
354
426
} ) ;
355
427
}
356
428
} ) ;
429
+
430
+ describe ( "stdio discovery error handling" , function ( ) {
431
+ it ( "outputs error for broken module" , async function ( ) {
432
+ // Create a temporary broken module
433
+ const fs = require ( "fs" ) ;
434
+ const brokenModulePath = path . join ( __dirname , "temp-broken-module" ) ;
435
+
436
+ try {
437
+ // Create directory and files
438
+ fs . mkdirSync ( brokenModulePath , { recursive : true } ) ;
439
+ fs . writeFileSync (
440
+ path . join ( brokenModulePath , "package.json" ) ,
441
+ JSON . stringify ( { name : "broken-module" , main : "index.js" } )
442
+ ) ;
443
+ fs . writeFileSync (
444
+ path . join ( brokenModulePath , "index.js" ) ,
445
+ "const functions = require('firebase-functions');\nsyntax error here"
446
+ ) ;
447
+
448
+ const result = await runStdioDiscovery ( brokenModulePath ) ;
449
+
450
+ // Should exit with error
451
+ expect ( result . exitCode ) . to . equal ( 1 ) ;
452
+
453
+ // Should output error to stderr
454
+ const errorMatch = result . stderr . match ( / _ _ F I R E B A S E _ F U N C T I O N S _ M A N I F E S T _ E R R O R _ _ : ( .+ ) / ) ;
455
+ expect ( errorMatch ) . to . not . be . null ;
456
+ expect ( errorMatch ! [ 1 ] ) . to . contain ( "Unexpected identifier" ) ;
457
+ } finally {
458
+ // Cleanup
459
+ fs . rmSync ( brokenModulePath , { recursive : true , force : true } ) ;
460
+ }
461
+ } ) ;
462
+ } ) ;
357
463
} ) ;
0 commit comments