@@ -10,27 +10,72 @@ type OutboundMessage = {
1010 code ?: number ;
1111} ;
1212
13- async function readAllStdout (
14- stream : ReadableStream < Uint8Array > ,
15- ) : Promise < OutboundMessage [ ] > {
16- const events : OutboundMessage [ ] = [ ] ;
17- const reader = stream . getReader ( ) ;
18- const decoder = new TextDecoder ( ) ;
19- let buf = "" ;
20-
21- while ( true ) {
22- const { done, value } = await reader . read ( ) ;
23- if ( done ) break ;
24- buf += decoder . decode ( value , { stream : true } ) ;
25- let nl : number ;
26- while ( ( nl = buf . indexOf ( "\n" ) ) >= 0 ) {
27- const line = buf . slice ( 0 , nl ) . trim ( ) ;
28- buf = buf . slice ( nl + 1 ) ;
29- if ( line ) events . push ( JSON . parse ( line ) as OutboundMessage ) ;
13+ class StdoutCollector {
14+ readonly events : OutboundMessage [ ] = [ ] ;
15+ private buf = "" ;
16+ private readonly waiters : Array < ( ) => void > = [ ] ;
17+ private done = false ;
18+ private readonly pump : Promise < void > ;
19+
20+ constructor ( stream : ReadableStream < Uint8Array > ) {
21+ this . pump = this . read ( stream ) ;
22+ }
23+
24+ private notify ( ) : void {
25+ const waiters = this . waiters . splice ( 0 ) ;
26+ for ( const wake of waiters ) wake ( ) ;
27+ }
28+
29+ private async read ( stream : ReadableStream < Uint8Array > ) : Promise < void > {
30+ const reader = stream . getReader ( ) ;
31+ const decoder = new TextDecoder ( ) ;
32+ try {
33+ while ( true ) {
34+ const { done, value } = await reader . read ( ) ;
35+ if ( done ) break ;
36+ this . buf += decoder . decode ( value , { stream : true } ) ;
37+ let nl : number ;
38+ while ( ( nl = this . buf . indexOf ( "\n" ) ) >= 0 ) {
39+ const line = this . buf . slice ( 0 , nl ) . trim ( ) ;
40+ this . buf = this . buf . slice ( nl + 1 ) ;
41+ if ( line ) this . events . push ( JSON . parse ( line ) as OutboundMessage ) ;
42+ }
43+ this . notify ( ) ;
44+ }
45+ } finally {
46+ this . done = true ;
47+ this . notify ( ) ;
3048 }
3149 }
3250
33- return events ;
51+ async waitUntil (
52+ predicate : ( events : OutboundMessage [ ] ) => boolean ,
53+ label : string ,
54+ timeoutMs = 10_000 ,
55+ ) : Promise < void > {
56+ const deadline = Date . now ( ) + timeoutMs ;
57+ while ( ! predicate ( this . events ) ) {
58+ if ( this . done ) {
59+ throw new Error (
60+ `stdout ended before ${ label } ; events=${ JSON . stringify ( this . events ) } ` ,
61+ ) ;
62+ }
63+ if ( Date . now ( ) >= deadline ) {
64+ throw new Error (
65+ `timed out waiting for ${ label } ; events=${ JSON . stringify ( this . events ) } ` ,
66+ ) ;
67+ }
68+ await new Promise < void > ( ( resolve ) => {
69+ this . waiters . push ( resolve ) ;
70+ setTimeout ( resolve , 50 ) ;
71+ } ) ;
72+ }
73+ }
74+
75+ async finish ( ) : Promise < OutboundMessage [ ] > {
76+ await this . pump ;
77+ return this . events ;
78+ }
3479}
3580
3681describe ( "runWebSocketSession" , ( ) => {
@@ -43,6 +88,7 @@ describe("runWebSocketSession", () => {
4388
4489 test ( "emits sent for initial body and stdin send" , async ( ) => {
4590 server = Bun . serve ( {
91+ hostname : "127.0.0.1" ,
4692 port : 0 ,
4793 fetch ( req , srv ) {
4894 if ( srv . upgrade ( req , { data : undefined } ) ) return ;
@@ -68,25 +114,66 @@ describe("runWebSocketSession", () => {
68114
69115 const cliPath = join ( import . meta. dir , "../../cli.ts" ) ;
70116 const child = Bun . spawn (
71- [ "bun" , cliPath , "--websocket" , "-i" , connectFile ] ,
117+ [ "bun" , "run" , cliPath , "--websocket" , "-i" , connectFile ] ,
72118 {
73- cwd : join ( import . meta. dir , "../../../.." ) ,
119+ cwd : join ( import . meta. dir , "../../../../../ " ) ,
74120 stdin : "pipe" ,
75121 stdout : "pipe" ,
76122 stderr : "pipe" ,
77123 } ,
78124 ) ;
79125
80- const eventsPromise = readAllStdout ( child . stdout ) ;
126+ const stderrChunks : string [ ] = [ ] ;
127+ void child . stderr . pipeTo (
128+ new WritableStream ( {
129+ write ( chunk ) {
130+ stderrChunks . push ( new TextDecoder ( ) . decode ( chunk ) ) ;
131+ } ,
132+ } ) ,
133+ ) ;
134+
135+ const collector = new StdoutCollector ( child . stdout ) ;
136+
137+ try {
138+ await collector . waitUntil (
139+ ( events ) =>
140+ events . some ( ( e ) => e . type === "ready" ) &&
141+ events . filter ( ( e ) => e . type === "sent" ) . length >= 1 &&
142+ events . filter ( ( e ) => e . type === "message" ) . length >= 1 ,
143+ "initial sent+echo" ,
144+ ) ;
81145
82- await Bun . sleep ( 200 ) ;
83- child . stdin . write ( `${ JSON . stringify ( { op : "send" , data : '{"foo":1}' } ) } \n` ) ;
84- await Bun . sleep ( 200 ) ;
85- child . stdin . write ( `${ JSON . stringify ( { op : "close" } ) } \n` ) ;
86- child . stdin . end ( ) ;
146+ child . stdin . write (
147+ `${ JSON . stringify ( { op : "send" , data : '{"foo":1}' } ) } \n` ,
148+ ) ;
149+
150+ await collector . waitUntil (
151+ ( events ) =>
152+ events . filter ( ( e ) => e . type === "sent" ) . length >= 2 &&
153+ events . filter ( ( e ) => e . type === "message" ) . length >= 2 ,
154+ "stdin sent+echo" ,
155+ ) ;
156+
157+ child . stdin . write ( `${ JSON . stringify ( { op : "close" } ) } \n` ) ;
158+ child . stdin . end ( ) ;
159+
160+ await collector . waitUntil (
161+ ( events ) => events . some ( ( e ) => e . type === "closed" ) ,
162+ "closed" ,
163+ ) ;
164+ } catch ( error ) {
165+ child . kill ( ) ;
166+ const stderr = stderrChunks . join ( "" ) . trim ( ) ;
167+ throw new Error (
168+ `${ error instanceof Error ? error . message : String ( error ) } ${
169+ stderr ? `\nstderr: ${ stderr } ` : ""
170+ } `,
171+ ) ;
172+ }
87173
88- const events = await eventsPromise ;
89- expect ( await child . exited ) . toBe ( 0 ) ;
174+ const events = await collector . finish ( ) ;
175+ const exitCode = await child . exited ;
176+ expect ( exitCode ) . toBe ( 0 ) ;
90177
91178 expect ( events . map ( ( e ) => e . type ) ) . toEqual ( [
92179 "ready" ,
0 commit comments