11const { describe, it, after } = require ( 'node:test' ) ;
22const assert = require ( 'node:assert' ) ;
33const http = require ( 'http' ) ;
4+ const { spawn } = require ( 'child_process' ) ;
5+ const path = require ( 'path' ) ;
46const WebSocket = require ( 'ws' ) ;
57const { createTermBeamServer } = require ( '../src/server' ) ;
68
@@ -59,8 +61,14 @@ function waitForOpen(ws, timeout = 5000) {
5961 return new Promise ( ( resolve , reject ) => {
6062 if ( ws . readyState === WebSocket . OPEN ) return resolve ( ) ;
6163 const timer = setTimeout ( ( ) => reject ( new Error ( 'WebSocket open timeout' ) ) , timeout ) ;
62- ws . on ( 'open' , ( ) => { clearTimeout ( timer ) ; resolve ( ) ; } ) ;
63- ws . on ( 'error' , ( err ) => { clearTimeout ( timer ) ; reject ( err ) ; } ) ;
64+ ws . on ( 'open' , ( ) => {
65+ clearTimeout ( timer ) ;
66+ resolve ( ) ;
67+ } ) ;
68+ ws . on ( 'error' , ( err ) => {
69+ clearTimeout ( timer ) ;
70+ reject ( err ) ;
71+ } ) ;
6472 } ) ;
6573}
6674
@@ -117,7 +125,10 @@ describe('Integration', () => {
117125 port : inst . port ,
118126 path : '/api/auth' ,
119127 method : 'POST' ,
120- headers : { 'Content-Type' : 'application/json' , 'Content-Length' : Buffer . byteLength ( authBody ) } ,
128+ headers : {
129+ 'Content-Type' : 'application/json' ,
130+ 'Content-Length' : Buffer . byteLength ( authBody ) ,
131+ } ,
121132 } ,
122133 authBody ,
123134 ) ;
@@ -176,7 +187,11 @@ describe('Integration', () => {
176187
177188 // Send input and wait for output containing the echo marker
178189 const marker = `helloTB${ Date . now ( ) } ` ;
179- const outputPromise = waitForMessage ( ws , ( m ) => m . type === 'output' && m . data . includes ( marker ) , 15000 ) ;
190+ const outputPromise = waitForMessage (
191+ ws ,
192+ ( m ) => m . type === 'output' && m . data . includes ( marker ) ,
193+ 15000 ,
194+ ) ;
180195 ws . send ( JSON . stringify ( { type : 'input' , data : `echo ${ marker } \r` } ) ) ;
181196 const outputMsg = await outputPromise ;
182197 assert . ok ( outputMsg . data . includes ( marker ) , 'Output should contain the echoed marker' ) ;
@@ -235,7 +250,10 @@ describe('Integration', () => {
235250 port : inst . port ,
236251 path : '/api/sessions' ,
237252 method : 'POST' ,
238- headers : { 'Content-Type' : 'application/json' , 'Content-Length' : Buffer . byteLength ( createBody ) } ,
253+ headers : {
254+ 'Content-Type' : 'application/json' ,
255+ 'Content-Length' : Buffer . byteLength ( createBody ) ,
256+ } ,
239257 } ,
240258 createBody ,
241259 ) ;
@@ -277,7 +295,10 @@ describe('Integration', () => {
277295 resolve ( ) ;
278296 }
279297 } , 100 ) ;
280- setTimeout ( ( ) => { clearInterval ( poll ) ; resolve ( ) ; } , 5000 ) ;
298+ setTimeout ( ( ) => {
299+ clearInterval ( poll ) ;
300+ resolve ( ) ;
301+ } , 5000 ) ;
281302 } ) ;
282303
283304 // GET /api/sessions should only have the default session
@@ -334,7 +355,10 @@ describe('Integration', () => {
334355 port : inst . port ,
335356 path : '/api/auth' ,
336357 method : 'POST' ,
337- headers : { 'Content-Type' : 'application/json' , 'Content-Length' : Buffer . byteLength ( wrongBody ) } ,
358+ headers : {
359+ 'Content-Type' : 'application/json' ,
360+ 'Content-Length' : Buffer . byteLength ( wrongBody ) ,
361+ } ,
338362 } ,
339363 wrongBody ,
340364 ) ;
@@ -348,11 +372,91 @@ describe('Integration', () => {
348372 port : inst . port ,
349373 path : '/api/auth' ,
350374 method : 'POST' ,
351- headers : { 'Content-Type' : 'application/json' , 'Content-Length' : Buffer . byteLength ( wrongBody ) } ,
375+ headers : {
376+ 'Content-Type' : 'application/json' ,
377+ 'Content-Length' : Buffer . byteLength ( wrongBody ) ,
378+ } ,
352379 } ,
353380 wrongBody ,
354381 ) ;
355382 assert . strictEqual ( res6 . statusCode , 429 , '6th attempt should be rate limited' ) ;
356383 } ) ;
357384 } ) ;
385+
386+ describe ( 'CLI entry point produces output (npx simulation)' , ( ) => {
387+ it ( 'should print the banner when invoked via a wrapper script' , async ( ) => {
388+ const entryPoint = path . resolve ( __dirname , '..' , 'bin' , 'termbeam.js' ) ;
389+ const output = await new Promise ( ( resolve , reject ) => {
390+ let buf = '' ;
391+ const child = spawn ( process . execPath , [ entryPoint , '--no-tunnel' , '--no-password' ] , {
392+ env : { ...process . env , TERMBEAM_LOG_LEVEL : 'error' , PORT : '0' } ,
393+ stdio : [ 'ignore' , 'pipe' , 'pipe' ] ,
394+ } ) ;
395+ child . stdout . on ( 'data' , ( d ) => {
396+ buf += d ;
397+ } ) ;
398+ child . stderr . on ( 'data' , ( d ) => {
399+ buf += d ;
400+ } ) ;
401+ const timer = setTimeout ( ( ) => {
402+ child . kill ( 'SIGTERM' ) ;
403+ resolve ( buf ) ;
404+ } , 5000 ) ;
405+ child . on ( 'error' , ( err ) => {
406+ clearTimeout ( timer ) ;
407+ reject ( err ) ;
408+ } ) ;
409+ child . on ( 'exit' , ( ) => {
410+ clearTimeout ( timer ) ;
411+ resolve ( buf ) ;
412+ } ) ;
413+ } ) ;
414+ assert . ok (
415+ output . includes ( 'Beam your terminal' ) || output . includes ( 'TERMBEAM' ) ,
416+ 'Should print banner, got: ' + output . slice ( 0 , 200 ) ,
417+ ) ;
418+ } ) ;
419+ } ) ;
420+
421+ describe ( 'Server rejects invalid shell and falls back gracefully' , ( ) => {
422+ let inst ;
423+ after ( ( ) => inst ?. shutdown ( ) ) ;
424+
425+ it ( 'should start even when defaultShell is an invalid process name' , async ( ) => {
426+ // Simulate the npx bug: defaultShell is garbage but shell is a real shell
427+ inst = await startServer ( {
428+ defaultShell : 'npm exec termbeam@latest --log-level=debug' ,
429+ } ) ;
430+ const res = await httpRequest ( {
431+ hostname : '127.0.0.1' ,
432+ port : inst . port ,
433+ path : '/api/sessions' ,
434+ method : 'GET' ,
435+ } ) ;
436+ assert . strictEqual ( res . statusCode , 200 ) ;
437+ const sessions = JSON . parse ( res . data ) ;
438+ assert . ok ( sessions . length >= 1 , 'Should have at least one session' ) ;
439+ } ) ;
440+
441+ it ( 'POST /api/sessions with invalid shell should return 400' , async ( ) => {
442+ if ( ! inst ) inst = await startServer ( ) ;
443+ const body = JSON . stringify ( { name : 'bad' , shell : 'npm exec termbeam' } ) ;
444+ const res = await httpRequest (
445+ {
446+ hostname : '127.0.0.1' ,
447+ port : inst . port ,
448+ path : '/api/sessions' ,
449+ method : 'POST' ,
450+ headers : {
451+ 'Content-Type' : 'application/json' ,
452+ 'Content-Length' : Buffer . byteLength ( body ) ,
453+ } ,
454+ } ,
455+ body ,
456+ ) ;
457+ assert . strictEqual ( res . statusCode , 400 ) ;
458+ const data = JSON . parse ( res . data ) ;
459+ assert . strictEqual ( data . error , 'Invalid shell' ) ;
460+ } ) ;
461+ } ) ;
358462} ) ;
0 commit comments